Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |
The third type of parameter a template can accept is another
class template. If you are going to use a template type parameter itself as a
template in your code, the compiler needs to know that the parameter is a
template in the first place. The following example illustrates a template
template parameter:
//: C05:TempTemp.cpp
// Illustrates a template template parameter.
#include <cstddef>
#include <iostream>
using namespace std;
template<class T>
class Array { // A simple, expandable sequence
enum { INIT = 10 };
T* data;
size_t capacity;
size_t count;
public:
Array() {
count = 0;
data = new T[capacity = INIT];
}
~Array() { delete [] data; }
void push_back(const T& t) {
if(count == capacity) {
// Grow underlying array
size_t newCap = 2 * capacity;
T* newData = new T[newCap];
for(size_t i = 0; i < count; ++i)
newData[i] = data[i];
delete [] data;
data = newData;
capacity = newCap;
}
data[count++] = t;
}
void pop_back() {
if(count > 0)
--count;
}
T* begin() { return data; }
T* end() { return data + count; }
};
template<class T,
template<class> class Seq>
class Container {
Seq<T> seq;
public:
void append(const T& t) { seq.push_back(t); }
T* begin() { return seq.begin(); }
T* end() { return seq.end(); }
};
int main() {
Container<int, Array> container;
container.append(1);
container.append(2);
int* p = container.begin();
while(p != container.end())
cout << *p++ << endl;
} ///:~
The Array class template is a trivial sequence
container. The Container template takes two parameters: the type that it
holds, and a sequence data structure to do the holding. The following line in
the implementation of the Container class requires that we inform the
compiler that Seq is a template:
If we hadn t declared Seq to be a template template
parameter, the compiler would complain here that Seq is not a template,
since we re using it as such. In main( ) a Container is
instantiated to use an Array to hold integers, so Seq stands for Array
in this example.
Note that it is not necessary in this case to name the
parameter for Seq inside Container s declaration. The line in
question is:
template<class T, template<class> class Seq>
Although we could have written
template<class T, template<class U> class Seq>
the parameter U is not needed anywhere. All that
matters is that Seq is a class template that takes a single type
parameter. This is analogous to omitting the names of function parameters when
they re not needed, such as when you overload the post-increment operator:
The int here is merely a placeholder and so needs no
name.
The following program uses a fixed-size array, which has an
extra template parameter representing the array length:
//: C05:TempTemp2.cpp
// A multi-variate template template
parameter.
#include <cstddef>
#include <iostream>
using namespace std;
template<class T, size_t N> class Array {
T data[N];
size_t count;
public:
Array() { count = 0; }
void push_back(const T& t) {
if(count < N)
data[count++] = t;
}
void pop_back() {
if(count > 0)
--count;
}
T* begin() { return data; }
T* end() { return data + count; }
};
template<class T,size_t
N,template<class,size_t> class Seq>
class Container {
Seq<T,N> seq;
public:
void append(const T& t) { seq.push_back(t); }
T* begin() { return seq.begin(); }
T* end() { return seq.end(); }
};
int main() {
const size_t N = 10;
Container<int, N, Array> container;
container.append(1);
container.append(2);
int* p = container.begin();
while(p != container.end())
cout << *p++ << endl;
} ///:~
Once again, parameter names are not needed in the
declaration of Seq inside Container s declaration, but we need
two parameters to declare the data member seq, hence the appearance of
the non-type parameter N at the top level.
Combining default arguments with template template parameters
is slightly more problematic. When the compiler looks at the inner parameters
of a template template parameter, default arguments are not considered, so you
have to repeat the defaults in order to get an exact match. The following
example uses a default argument for the fixed-size Array template and
shows how to accommodate this quirk in the language:
//: C05:TempTemp3.cpp {-bor}{-msc}
// Template template parameters and default arguments.
#include <cstddef>
#include <iostream>
using namespace std;
template<class T, size_t N = 10> // A default
argument
class Array {
T data[N];
size_t count;
public:
Array() { count = 0; }
void push_back(const T& t) {
if(count < N)
data[count++] = t;
}
void pop_back() {
if(count > 0)
--count;
}
T* begin() { return data; }
T* end() { return data + count; }
};
template<class T, template<class, size_t = 10>
class Seq>
class Container {
Seq<T> seq; // Default used
public:
void append(const T& t) { seq.push_back(t); }
T* begin() { return seq.begin(); }
T* end() { return seq.end(); }
};
int main() {
Container<int, Array> container;
container.append(1);
container.append(2);
int* p = container.begin();
while(p != container.end())
cout << *p++ << endl;
} ///:~
The default dimension of 10 is required in the line:
template<class T, template<class, size_t = 10> class Seq>
Both the definition of seq in Container and container
in main( ) use the default. The only way to use something other
than the default value was shown in TempTemp2.cpp. This is the only
exception to the rule stated earlier that default arguments should appear only
once in a compilation unit.
Since the standard sequence containers (vector, list,
and deque, discussed in depth in Chapter 7) have a default allocator
argument, the technique shown above is helpful should you ever want to pass one
of these sequences as a template parameter. The following program passes a vector
and then a list to two instances of Container:
//: C05:TempTemp4.cpp {-bor}{-msc}
// Passes standard sequences as template arguments.
#include <iostream>
#include <list>
#include <memory> // Declares allocator<T>
#include <vector>
using namespace std;
template<class T,
template<class U, class = allocator<U> >
class Seq>
class Container {
Seq<T> seq; // Default of allocator<T>
applied implicitly
public:
void push_back(const T& t) { seq.push_back(t); }
typename Seq<T>::iterator begin() { return
seq.begin(); }
typename Seq<T>::iterator end() { return
seq.end(); }
};
int main() {
// Use a vector
Container<int, vector> vContainer;
vContainer.push_back(1);
vContainer.push_back(2);
for(vector<int>::iterator p = vContainer.begin();
p != vContainer.end();
++p) {
cout << *p <<
endl;
}
// Use a list
Container<int, list> lContainer;
lContainer.push_back(3);
lContainer.push_back(4);
for(list<int>::iterator p2 = lContainer.begin();
p2 != lContainer.end(); ++p2) {
cout << *p2 << endl;
}
} ///:~
Here we name the first parameter of the inner template Seq
(with the name U) because the allocators in the standard sequences must
themselves be parameterized with the same type as the contained objects in the
sequence. Also, since the default allocator parameter is known, we can
omit it in the subsequent references to Seq<T>, as we did in the
previous program. To fully explain this example, however, we have to discuss
the semantics of the typename keyword.
Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |