1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
|
Template hi(template template parameter) template parameters allow us to
specify a em(class template) as a template parameter. By specifying a class
template, it is possible to add a certain kind of behavior (called a
emi(policy)) to an existing class template.
To specify an allocation em(policy), rather than an allocation tt(type)
for the class tt(Storage) we rephrase its class template header:
definition starts as follows:
verb( template <typename Data, template <typename> class Policy>
class Storage...)
The second template parameter is new. It is a em(template template
parameter). It has the following elements:
itemization(
it() The keyword tt(template), starting the template template parameter;
it() The keyword tt(template) is followed (between angle brackets) by a
list of template parameters that must be specified for the template
template parameter. These parameters em(may) be given names, but names
are usually omitted as those names cannot be used in subsequent
template definitions. On the other hand, providing formal names may
help the reader of the template to understand the kind of templates
that must be specified with the template template parameter.
it() Template template parameters must match, in numbers and types (i.e.,
template type parameters, template non-type parameters, template
template parameters) the template parameters that must be specified
for the policy. This can be tricky, as some templates use default
parameters that are hardly ever changed (like the allocation schemes
for containers). When specifying template template parameters those
default types are used. If they should be specifiable then they must
be added to the template template parameter's types. In the following
example tt(fun) can only use the default allocator, tt(gun) em(must)
specify the allocator when defining tt(vd), and tt(hun) defines a
default allocator type, offering the option to either specify the
allocator type or use the default type:
verbinsert(-as4 examples/vector1.cc)
Alternatively, em(alias templates), introduced in section
ref(ALIASES), can be used to define names for templates using
predefined parameter types.
it() Following the bracketed list the keyword ti(class) or tt(typename)
em(must) be specified.
it() All parameters may be provided with default arguments. This is shown
in the next example of a hypothetical class template:
verb(template <
template <
typename = std::string,
int = 12,
template <typename = int> class Inner = std::vector
>
class Policy
>
class Demo
{
...
};)
Here, the class template tt(Demo) expects a template template parameter
named tt(Policy), expecting three template parameters: a template type
parameter (by default tt(std::string)); a template non-type parameter
(by default having value 12); and tt(Policy) itself expects a template
template parameter, called tt(Inner), by default using an tt(int) as
its template type parameter.
)
Policy classes are often an integral part of the class under
consideration. Because of this they are often deployed as base classes. In the
example the class tt(Policy) could be used as a base class of the class
tt(Storage).
The policy operates on the class tt(Storage)'s data type. Therefore the
policy is informed about that data type as well. Our class tt(Storage) now
begins like this:
verb( template <typename Data, template <typename> class Policy>
class Storage: public Policy<Data>)
This automatically allows us to use tt(Policy)'s members when implementing
the members of the class tt(Storage).
Our home-made allocation classes do not really provide us with many useful
members. Except for the extraction operator they offer no immediate access to
the data. This can easily be repaired by adding more members. E.g., the class
tt(NewAlloc) could be augmented with operators allowing access to and
modification of stored data:
verb( operator Data &() // optionally add a `const' member too
{
return *d_data;
}
NewAlloc &operator=(Data const &data)
{
*d_data = data;
})
The other allocation classes could be given comparable members.
Let's use the allocation schemes in some real code. The next
example shows how tt(Storage) can be defined using some data type and an
allocation scheme. We start out again with a class tt(Storage):
verb( template <typename Data, template <typename> class Allocate>
class Storage: public std::vector<Data, Allocate<Data>>
{};)
That's all we have to do. Note that tt(std::vector) formally has two
template parameters. The first one is the vector's data type, which is always
specified; the second one is the allocator used by the vector. Usually the
allocator is left unspecified (in which case the default STL allocator is
used), but here it is mentioned explicitly, allowing us to pass our own
allocation policy to tt(Storage).
All required functionality is inherited from the tt(vector) base class, while
the policy is `factored into the equation' using a template template
parameter. Here's an example showing how this is done:
verb( Storage<std::string, NewAlloc> storage;
copy(istream_iterator<std::string>(cin), istream_iterator<std::string>(),
back_inserter(storage));
cout << "Element index 1 is " << storage[1] << '\n';
storage[1] = "hello";
copy(storage.begin(), storage.end(),
ostream_iterator<NewAlloc<std::string> >(cout, "\n"));)
Since tt(Storage) objects are also tt(std::vector) objects the STL tt(copy)
function can be used in combination with the em(back_inserter) iterator to add
some data to the tt(storage) object. Its elements can be accessed and modified
using the index operator. Then tt(NewAlloc<std::string>) objects are inserted
into tt(cout) (also using the tt(copy) function).
Interestingly, this is not the end of the story. Remember that our
intention was to create a class allowing us to specify the em(storage type) as
well. What if we don't want to use a tt(vector), but instead would like to use
a tt(list)?
It's easy to change tt(Storage)'s setup so that a completely different
storage type can be used on request, like a tt(deque). To implement this, the
storage class is parameterized as well, using yet another template template
parameter:
verb( template <typename Data, template <typename> class AllocationPolicy,
template <typename, typename> class Container = std::vector>
class Storage: public Container<Data, AllocationPolicy<Data>>
{};)
The earlier example using a tt(Storage) object can be used again without
requiring any modifications at all (except for the above redefinition). It
clearly can't be used with a tt(list) container, as the tt(list) lacks
tt(operator[]). Trying to do so is immediately recognized by the compiler,
producing an error if an attempt is made to use tt(operator[]) on, e.g., a
tt(list).footnote(A complete example showing the definition of the
allocation classes and the class tt(Storage) as well as its use is provided in
the annotations()'s distribution in the file
tt(yo/advancedtemplates/examples/storage.cc).) A tt(list) container, however
can still be specified as the container to use. In that case a tt(Storage) is
implemented as a tt(list), offering tt(list)'s interface, rather than
tt(vector)'s interface, to its users.
|