Don't hesitate to send in feedback: send an e-mail if you like the C++ Annotations; if you think that important material was omitted; if you find errors or typos in the text or the code examples; or if you just feel like e-mailing. Send your e-mail to Frank B. Brokken.Please state the document version you're referring to, as found in the title (in this document: 8.3.1) and please state chapter and paragraph name or number you're referring to.
All received mail is processed conscientiously, and received suggestions for improvements will usually have been processed by the time a new version of the Annotations is released. Except for the incidental case I will normally not acknowledge the receipt of suggestions for improvements. Please don't interpret this as me not appreciating your efforts.
The main purpose of templates is to provide a generic definition of classes and functions that may then be tailored to specific types.
But templates allow us to do more than that. If not for compiler implementation limitations, templates could be used to program, at compile-time, just about anything we use computers for. This remarkable feat, offered by no other current-day computer language, stems from the fact that templates allow us to do three things at compile-time:
Of course, asking the compiler to compute, e.g., prime numbers, is one thing. But it's a completely different thing to do so in an award winning way. Don't expect speed records to be broken when the compiler performs complex calculations for us. But that's al beside the point. In the end we can ask the compiler to compute virtually anything using C++'s template language, including prime numbers....
In this chapter these remarkable features of templates are discussed. Following a short overview of subtleties related to templates the main characteristics of template meta programming are introduced.
In addition to template type and template non-type parameters there is a third kind of template parameter, the template template parameter. This kind of template parameter is introduced next, laying the groundwork for the discusion of trait classes and policy classes.
This chapter ends with the discussion of several additional and interesting applications of templates: adapting compiler error messages, conversions to class types and an elaborate example discussing compile-time list processing.
Much of the inspiration for this chapter came from two highly recommended books: Andrei Alexandrescu's 2001 book Modern C++ design (Addison-Wesley) and Nicolai Josutis and David Vandevoorde's 2003 book Templates (Addison-Wesley)
typename
was discussed. There we learned that it is not only used to define a name for
a (complex) type, but also to distinguish types defined by class templates
from members defined by class templates. In this section two more
applications of typedef are introduced:
typename to situations
where types nested in templates are returned from member functions of class
templates;
typedef section
22.1.3 introduces some new syntax that is related to the extended use of
the keyword typedef: ::template, .template and ->template are used
to inform the compiler that a name used inside a template is itself a class
template.
nested returns an object of this nested class. The example uses a
(deprecated) in-class member implementation. The reason for this will become
clear shortly.
template <typename T>
class Outer
{
public:
class Nested
{};
Nested nested() const
{
return Nested();
}
};
The above example compiles flawlessly. Inside the class Outer there is
no ambiguity with respect to the meaning of nested's return type.
However, following good practices inline and template members should be implemented below their class interfaces (see section 7.6.1). So we remove the implementation from the interface and put it below the interface:
template <typename T>
class Outer
{
public:
class Nested
{};
Nested nested() const;
};
template <typename T>
Outer<T>::Nested Outer<T>::nested() const
{
return Nested();
}
Suddenly the compiler refuses to compile the nested member, producing an
error message like
error: expected constructor, destructor, or type conversion before 'Outer'.Now that the implementation is moved out of the interface the return type (i.e.,
Outer<T>::Nested) refers to a type defined by Outer<T> rather
than to a member of Outer<T>.
Here, too, typename must be used. The general rule being: the keyword
typename must be used whenever a type is referred to that is a subtype
of a type that itself depends on a template type parameter.
Writing typename in front of Outer<T>::Nested removes the
compilation error. Thus, the correct implementation of the function
nested becomes:
template <typename T>
typename Outer<T>::Nested Outer<T>::nested() const
{
return Nested();
}
Base is used as a base class of
Derived:
#include <iostream>
template <typename T>
class Base
{
public:
void member();
};
template <typename T>
void Base<T>::member()
{
std::cout << "This is Base<T>::member()\n";
}
template <typename T>
class Derived: public Base<T>
{
public:
Derived();
};
template <typename T>
Derived<T>::Derived()
{
member();
}
This example won't compile, and the compiler tells us something like:
error: there are no arguments to 'member' that depend on a template
parameter, so a declaration of 'member' must be available
This error causes some confusion as ordinary (non-template) base classes
readily make their public and protected members available to classes that are
derived from them. This is no different for class templates, but only if the
compiler can figure out what we mean. In the above example the compiler
can't as it doesn't know for what type T the member function
member must be initialized when called from Derived<T>::Derived.
To appreciate why this is true, consider the situation where we have defined a specialization:
template <>
Base<int>::member()
{
std::cout << "This is the int-specialization\n";
}
Since the compiler, when Derived<SomeType>::Derived is called, does
not know whether a specialization of member will be in effect, it can't
decide (when compiling Derived<T>::Derived) for what type to instantiate
member. It can't decide this when compiling Derived<T>::Derived as
member's call in Derived::Derived doesn't require a template type
parameter.
In cases like these, where no template type parameter is available to
determine which type to use, the compiler must be told that it should postpone
its decision about the template type parameter to use (and therefore about the
particular (here: member) function to call)
until instantiation time.
This may be implemented in two ways: either by using this or by
explicitly mentioning the base class, instantiated for the derived class's
template type(s). When this is used the compiler is informed that we're
referring to the type T for which the template was instantiated. Any
confusion about which member function to use (the derived class or base class
member) is resolved in favor of the derived class member. Alternatively, the
base or derived class can explicitly be mentioned (using Base<T> or
Derived<T>) as shown in the next example. Note that with the int
template type the int specialization is used.
#include <iostream>
template <typename T>
class Base
{
public:
virtual void member();
};
template <typename T>
void Base<T>::member()
{
std::cout << "This is Base<T>::member()\n";
}
template <>
void Base<int>::member()
{
std::cout << "This is the int-specialization\n";
}
template <typename T>
class Derived: public Base<T>
{
public:
Derived();
virtual void member();
};
template <typename T>
void Derived<T>::member()
{
std::cout << "This is Derived<T>::member()\n";
}
template <typename T>
Derived<T>::Derived()
{
this->member(); // Using `this' implies using the
// type for which T was instantiated
Derived<T>::member(); // Same: calls the Derived member
Base<T>::member(); // Same: calls the Base member
std::cout << "Derived<T>::Derived() completed\n";
}
int main()
{
Derived<double> d;
Derived<int> i;
}
/*
Generated output:
This is Derived<T>::member()
This is Derived<T>::member()
This is Base<T>::member()
Derived<T>::Derived() completed
This is Derived<T>::member()
This is Derived<T>::member()
This is the int-specialization
Derived<T>::Derived() completed
*/
The above example might also define virtual member templates (although
virtual member templates aren't often used). E.g., Base might declare a
virtual void member() and Derived might define its overriding function
member. In that case this->member() in Derived::Derived will, due
to member's virtual nature, of course call Derived::member. The
statement Base<T>::member(), however, will always call Base's
member function and can be used to
bypass dynamic polymorphism.
typename keyword may often be used for that purpose.
But typename cannot always come to the rescue. While parsing a source the
compiler receives a series of tokens, representing meaningful units of
text encountered in the program's source. A token could represent, e.g., an
identifier or a number. Other tokens represent operators, like =, + or
<. It is precisely the last token that may cause problems as it may have
very different meanings. The correct meaning cannot always be determined from
the context in which the compiler encounters <. In some situations the
compiler does know that < does not represent the less than
operator, as when a template parameter list follows the keyword template,
e.g.,
template <typename T, int N>
Clearly, in this case < does not represent a `less than' operator.
The special meaning of < when it is preceded by template forms the
basis for the syntactic constructs discussed in this section.
Assume the following class has been defined:
template <typename Type>
class Outer
{
public:
template <typename InType>
class Inner
{
public:
template <typename X>
void nested();
};
};
The class template Outer defines a nested class template
Inner. Inner in turn defines a template member function.
Next a class template Usage is defined, offering a member function
caller expecting an object of the above Inner type. An initial setup
for Usage looks like this:
template <typename T1, typename T2>
class Usage
{
public:
void fun(Outer<T1>::Inner<T2> &obj);
...
};
The compiler won't accept this as it interprets Outer<T1>::Inner as a
class type. But there is no class Outer<T1>::Inner. Here the compiler
generates an error like:
error: 'class Outer<T1>::Inner' is not a type
To inform the compiler that Inner
itself is a template, using the template type parameter <T2>, the
::template construction is required. It tells the compiler that the next
< should not be interpreted as a `less than' token, but rather as a
template type argument. So, the declaration is modified to:
void fun(Outer<T1>::template Inner<T2> &obj);
This still doesn't get us where we want to be: after all Inner<T2> is
a type, nested under a class template, depending on a template type
parameter. In fact, the original Outer<T1>::Inner<T2> &obj declaration
results in a series of error messages, one of them looking like this:
error: expected type-name before '&' token
As is often the case this error message nicely indicates what should be
done to get it right: add typename:
void fun(typename Outer<T1>::template Inner<T2> &obj);
Of course, fun itself is not only just declared, it must also be
implemented. Assume that its implementation should call Inner's member
nested, instantiated for yet another type X. The class template
Usage should therefore receive a third template type parameter, called
T3. Assume it has been defined. To implement fun, we write:
void fun(typename Outer<T1>::template Inner<T2> &obj)
{
obj.nested<T3>();
}
Once again we run into a problem. In the function's body the compiler once
again interprets < as `less than', seeing a logical expression having as
its right-hand side a primary expression instead of a function call specifying
a template type T3.
To tell the compiler that is should interpret <T3> as a type to
instantiate nested with the template keyword is used once more. This
time it is used in the context of the member selection operator. We write
.template to inform the compiler that what follows is not a `less than'
operator, but rather a type specification. The function's final implementation
becomes:
void fun(typename Outer<T1>::template Inner<T2> &obj)
{
obj.template nested<T3>();
}
Instead of defining value or reference parameters functions may also
define pointer parameters. If obj would have been defined as a pointer
parameter the implementation would have had to use the ->template
construction, rather than the .template construction. E.g.,
void fun(typename Outer<T1>::template Inner<T2> *ptr)
{
ptr->template nested<T3>();
}
enum
values. Enums are preferred over, e.g., int const values since enums never
require any linkage. They are pure symbolic values with no memory
representation whatsoever.
Consider the situation where a programmer must use a cast, say a
reinterpret_cast. A problem with a reinterpret_cast is that it is the
ultimate way to turn off all compiler checks. All bets are off, and we can
write extreme but absolutely pointless reinterpret_cast statements, like
int value = 12;
ostream &ostr = reinterpret_cast<ostream &>(value);
Wouldn't it be nice if the compiler would warn us against such oddities by generating an error message?
If that's what we'd like the compiler to do, there must be some way to
distinguish madness from weirdness. Let's assume we agree on the following
distinction: reinterpret casts are never acceptable if the target type
represents a larger type than the expression (source) type, since that would
immediately result in exceeding the amount of memory that's actually available
to the target type. For this reason it's clearly silly to
reinterpret_cast<doble *>(&intVar), but reinterpret_cast<char
*>(&intVar) could be defensible.
The intent is now to create a new kind of cast, let's call it
reinterpret_to_smaller_cast. It should only be allowed to perform a
reinterpret_to_smaller_cast if the target type occupies less memory than
the source type (note that this exactly the opposite reasoning as used by
Alexandrescu
(2001), section 2.1).
To start, we construct the following template:
template<typename Target, typename Source>
Target &reinterpret_to_smaller_cast(Source &source)
{
// determine whether Target is smaller than source
return reinterpret_cast<Target &>(source);
}
At the comment an enum-definition is inserted defining a symbol having a
suggestive name. A compile-time error results if the required condition is
not met and the error message displays the name of the symbol. A division by
zero is clearly not allowed, and noting that a false value represents a
zero value, the condition could be:
1 / (sizeof(Target) <= sizeof(Source));
The interesting part is that this condition doesn't result in any code at
all. The enum's value is a plain value that's computed by the compiler while
evaluating the expression:
template<typename Target, typename Source>
Target &reinterpret_to_smaller_cast(Source &source)
{
enum
{
the_Target_size_exceeds_the_Source_size =
1 / (sizeof(Target) <= sizeof(Source))
};
return reinterpret_cast<Target &>(source);
}
When reinterpret_to_smaller_cast is used to cast from int to
double an error is produced by the compiler, like this:
error: enumerator value for 'the_Target_size_exceeds_the_Source_size'
is not an integer constant
whereas no error is reported if, e.g.,
reinterpret_to_smaller_cast<int>(doubleVar) is requested with
doubleVar defined as a double.
In the above example a enum was used to compute (at compile-time) a
value that is illegal if an assumption is not met. The creative part is
finding an appropriate expression.
Enum values are well suited for these situations as they do not consume any memory and their evaluation does not produce any executable code. They can be used to accumulate values too: the resulting enum value then contains a final value, computed by the compiler rather than by executable code as the next sections illustrate. In general, programs shouldn't do run-time what they can do at compile-time and performing complex calculations resulting in constant values is a clear example of this principle.
int values. This is useful in situations where a scalar value (often a
bool value) is available to select a specialization but a type is required
to base the selection on. This situation will be encountered shortly (section
22.2.2).
Templatizing
integral values is based on the fact that a
class template together with its template arguments defines a type. E.g.,
vector<int> and vector<double> are different types.
Turning integral values into templates is easily done. Define a template (it
does not have to have any contents at all) and store the integral value in an
enum:
template <int x>
struct IntType
{
enum { value = x };
};
As IntType does not have any members the `class IntType' can be
defined as `struct IntType', saving us from having to type public:.
Defining the enum value `value' allows us to retrieve the value
used at the instantiation at no cost in storage. Enum values are neither
variables nor data members and thus have no address. They are mere values.
It's easy to use the struct IntType. An anonymous or named
object can be defined by specifying a value for its int non-type
parameter. Example:
int main()
{
IntType<1> it;
cout << "IntType<1> objects have value: " << it.value << "\n" <<
"IntType<2> objects are of a different type "
"and have values " << IntType<2>().value << '\n';
}
Actually, neither the named object nor the anonymous object is
required. As the enum is defined as a plain value, associated with the
struct IntType we merely have to specify the specific int for which
the struct IntType is defined to retrieve its `value', like this:
int main()
{
cout << "IntType<100>, no object, defines `value': " <<
IntType<100>::value << "\n";
}
if and
switch statements. If we want to be able to `program the compiler'
this feature must also be offered by templates.
Like templates storing values templates making choices do not require any code to be executed at run-time. The selection is purely made by the compiler, at compile-time. The essence of template meta programming is that we are not using or relying on any executable code. The result will often be executable code, but the code that is produced by the meta program is a function of decisions the compiler made by itself.
Template (member) functions are only instantiated when they are actually used. Consequenlty we can define specializations of functions that are mutually exclusive. Thus it is possible to define a specialization that can be compiled in one situation, but not in another and to define another specialization that can be compiled in the other situation, but not in the first situation. Using specializations code can be generated that is tailored to the demands of a particular situation.
A feature like this cannot be implemented in run-time executable code. For example, when designing a generic storage class the software engineer may intend to store value class type objects as well as objects of polymorphic class types in the final storage class. Thus the software engineer may conclude that the storage class should contain pointers to objects, rather than the objects themselves. The initially designed code may look like this:
template <typename Type>
void Storage::add(Type const &obj)
{
d_data.push_back(
d_ispolymorphic ?
obj.clone()
:
new Type(obj)
);
}
The intent is to use the clone member function of the Type class
if Type is a polymorphic class and the standard copy constructor if
Type is a value class.
Unfortunately, this scheme normally fails as value classes do not define
clone member functions and polymorphic base classes should delete
their copy constructors (cf. section 7.4). It doesn't matter to the
compiler that clone is never called for value classes and the copy
constructor is unavailable in polymorphic classes. It merely has some code to
compile, and can't do that because of missing members. It's as simple as that.
Template meta programming comes to the rescue. Knowing that class template
member functions are only instantiated when used, we intend to design
overloaded add member functions of which only one will be called (and thus
instantiated). Our selection will be based on an additional (in addition to
Type itself) template non-type parameter that indicates whether we'll use
Storage for polymorphic or non-polymorphic classes. Our class Storage
starts like this:
template <typename Type, bool isPolymorphic>
class Storage
Initially two overloaded versions of our add member are defined:
one used with Storage objects storing polymorphic objects (using true
as its template non-type argument) and one storing value
class objects (using false as its template non-type argument).
We run into a small problem: functions cannot be overloaded by their
argument values but only by their argument types. But a solution
exists. Realizing that types are defined by the combination of templates and
their template arguments we may convert the values true and false into
types using the knowledge obtained in section 22.2.1.1 about how to
convert integral values to types.
We'll provide one (private) add member with a IntType<true> parameter
(implementing the polymorphic class) and another (private) add member with
a IntType<false> parameter (implementing the non-polymorphic class).
In addition to these two private members a third (public) member add is
defined calling the appropriate private add member by providing an
IntType argument, constructed from Storage's template non-type
parameter.
Here are the implementations of the three add members:
// declared in Storage's private section:
template <typename Type, bool isPolymorphic>
void Storage<Type, isPolymorphic>::add(Type const &obj, IntType<true>)
{
d_data.push_back(obj.clone());
}
template <typename Type, bool isPolymorphic>
void Storage<Type, isPolymorphic>::add(Type const &obj, IntType<false>)
{
d_data.push_back(new Type(obj));
}
// declared in Storage's public section:
template <typename Type, bool isPolymorphic>
void Storage<Type, isPolymorphic>::add(Type const &obj)
{
add(obj, IntType<isPolymorphic>());
}
The appropriate add member is instantiated and called because a
primitive value can be converted to a type. Each of the possible template
non-type values is thus used to define an overloaded class template member
function.
Since class template members are only instantiated when used only one of
the overloaded private add members is instantiated. Since the other one is
never called (and thus never instantiated) compilation errors are prevented.
Some software engineers have reservations when thinking about the
Storage class that uses pointers to store copies of value class
objects. Their argument is that value class objects can very well be stored by
value, rather than by pointer. They'd rather store value class objects by
value and polymorphic class objects by pointer.
Such distinctions frequently occur in template meta programming and
the following struct IfElse
may be used to obtain one of two types, depending on a bool selector
value.
First define the generic form of the template:
template<bool selector, typename FirstType, typename SecondType>
struct IfElse
{
typedef FirstType type;
};
Then define a partial specialization. The specialization represents
a specific selector value (e.g., false) and leaves the remaining types
open to further specification:
template<typename FirstType, typename SecondType>
struct IfElse<false, FirstType, SecondType>
{
typedef SecondType type;
};
The former (generic) definition associates FirstType with the
IfElse::type type definition, the latter definition (partially specialized
for the logical value false) associates SecondType partial
specialization with the IfElse::type type definition.
The IfElse template allows us to define class templates whose data
organization is conditional to the template's parameters.
Using IfElse the Storage class may define
pointers to store copies of polymorphic class type objects and values
to store value class type objects:
template <typename Type, bool isPolymorphic>
class Storage
{
typedef typename IfElse<isPolymorphic, Type *, Type>::type
DataType;
std::vector<DataType> d_data;
private:
void add(Type const &obj, IntType<true>);
void add(Type const &obj, IntType<false>);
public:
void add(Type const &obj);
}
template <typename Type, bool isPolymorphic>
void Storage<Type, isPolymorphic>::add(Type const &obj, IntType<true>)
{
d_data.push_back(obj.clone());
}
template <typename Type, bool isPolymorphic>
void Storage<Type, isPolymorphic>::add(Type const &obj, IntType<false>)
{
d_data.push_back(obj);
}
template <typename Type, bool isPolymorphic>
void Storage<Type, isPolymorphic>::add(Type const &obj)
{
add(obj, IntType<isPolymorphic>());
}
The above example uses IfElse's type, defined by IfElse as
either FirstType or SecondType. IfElse's type defines the
actual data type to use for Storage's vector data type.
The remarkable result in this example is that the data organization of
the Storage class now depends on its template arguments. Since the
isPolymorphic == true situation uses different data types than the
t(isPolymorphic == false) situation, the overloaded private add members
can utilize this difference immediately. E.g., add(Type const &obj,
IntType<false>) uses direct copy construction to store a copy of obj
in d_vector.
It is also possible to make a selection from multiple types as IfElse
structs can be nested. Realize that using IfElse never has any effect on
the size or execution time of the final executable program. The final program
simply contains the appropriate type, conditional to the type that's
eventually selected.
The following example, defining MapType as a map having plain types or
pointers for either its key or value types, illustrates this approach:
template <typename Key, typename Value, int selector>
class Storage
{
typedef typename IfElse<
selector == 1, // if selector == 1:
map<Key, Value>, // use map<Key, Value>
typename IfElse<
selector == 2, // if selector == 2:
map<Key, Value *>, // use map<Key, Value *>
typename IfElse<
selector == 3, // if selector == 3:
map<Key *, Value>, // use map<Key *, Value>
// otherwise:
map<Key *, Value *> // use map<Key *, Value *>
>::type
>::type
>::type
MapType;
MapType d_map;
public:
void add(Key const &key, Value const &value);
private:
void add(Key const &key, Value const &value, IntType<1>);
...
};
template <typename Key, typename Value, int selector>
inline void Storage<selector, Key, Value>::add(Key const &key,
Value const &value)
{
add(key, value, IntType<selector>());
}
The principle used in the above examples is: if class templates may use
data types that depend on template non-type parameters, an IfElse struct
can be used to select the appropriate data types. Knowledge about the various
data types may also be used to define overloaded member functions. The
implementations of these overloaded members may then be optimized to the
various data types. In programs only one of these alternate functions (the one
that is optimized to the actually used data types) will then be instantiated.
The private add functions define the same parameters as the public
add wrapper function, but add a specific IntType type, allowing the
compiler to select the appropriate overloaded version based on the template's
non-type selector parameter.
for or while statement. However, iterations
can always be rewritten as recursions. Recursions are supported
by templates and so iterations can always be implemented as (tail) recursions.
To implement iterations by (tail) recursion do as follows:
enum values.
Most readers will be familiar with the recursive implementation of the
mathematical `factorial' operator, commonly represented by the exclamation
mark (!). Factorial n (so: n!) returns the successive products
n * (n - 1) * (n - 2) * ... * 1, representing the number of ways n
objects can be permuted. Interestingly, the factorial operator is itself
usually defined by a recursive definition:
n! = (n == 0) ?
1
:
n * (n - 1)!
To compute n! from a template, a template Factorial can be defined
using an int n template non-type parameter. A specialization is defined
for the case n == 0. The generic implementation uses recursion according
to the factorial definition. Furthermore, the Factorial template defines
an enum value `value' containing its factorial value. Here is the
generic definition:
template <int n>
struct Factorial
{
enum { value = n * Factorial<n - 1>::value };
};
Note how the expression assigning a value to `value' uses constant
values that can be determined by the compiler. The value n is provided, and
Factorial<n - 1> is computed using template meta
programming. Factorial<n-1> in turn results in value that can be
determined by the compiler (viz.
Factorial<n-1>::value). Factorial<n-1>::value represents the value
defined by the type Factorial<n - 1>. It is not the value returned
by an object of that type. There are no objects here but merely values
defined by types.
The recursion ends in a specialization. The compiler will select the specialization (provided for the terminating value 0) instead of the generic implementation whenever possible. Here is the specialization's implementation:
template <>
struct Factorial<0>
{
enum { value = 1 };
};
The Factorial template can be used to determine, compile time, the
number of permutations of a fixed number of objects. E.g.,
int main()
{
cout << "The number of permutations of 5 objects = " <<
Factorial<5>::value << "\n";
}
Once again, Factorial<5>::value is not evaluated at run-time, but
at compile-time. The run-time equivalent of the above cout statement is,
therefore:
int main()
{
cout << "The number of permutations of 5 objects = " <<
120 << "\n";
}
Storage. Data stored in Storage objects may either make
and store copies of the data or store the data as received. Storage
objects may also either use a vector or a linked list as its underlying
storage medium. How should the engineer tackle this request? Should four
different Storage classes be designed?
The engineer's first reaction could be to develop an all-in Storage class.
It could have having two data members, a list and a vector, and to provide the
constructor with maybe an enum value indicating whether the data itself or new
copies should be stored. The enum value can be used to initialize a series of
pointers to member functions performing the requested tasks (e.g., using a
vector to store the data or a list to store copies).
Complex, but doable. Then the engineer is asked to modify the class: in the
case of new copies a custom-made allocation scheme should be used rather than
the standard new operator. It should also be possible to use yet another
type of container, in addition to the vector and list that were already part
of the design. E.g., a deque could be preferred or maybe even a stack.
It's clear that the approach aiming at implementing all functionality and all
possible combinations in one class doesn't scale. The class Storage would
soon become a monolithic giant which is hard to understand, maintain, test,
and deploy.
One of the reasons why the big, all-encompassing class is hard to deploy and understand is that a well-designed class should enforce constraints: the design of the class should, by itself, disallow certain operations, violations of which should be detected by the compiler, rather than by a program that might terminate with a fatal error.
Consider the above request. If the class offers both an interface to access
the vector data storage and an interface to access the list data storage,
then it's likely that the class will offer an overloaded
operator[] member
to access elements in the vector. This member, however, will be syntactically
present, but semantically invalid when the list data storage is selected,
which doesn't support operator[].
Sooner or later, users of the monolithic all-encompassing class
Storage will fall into the trap of using operator[] even though
they've selected the list as the underlying data storage. The compiler won't
be able to detect the error, which will only appear once the program is
running, confusing users.
The question remains: how should the engineer proceed, when confronted with the above questions? It's time to introduce policies.
In the previous section the problem of creating a class that might use any of a series of allocation schemes was introduced. These allocation schemes all depend on the actual data type to be used, and so the `template' reflex should kick in. Allocation schemes should probably be defined as template classes, applying the appropriate allocation procedures to the data type at hand. E.g. (using in-class implementations to save some space), the following three allocation classes could be defined:
data is used `as is':
template <typename Data>
class PlainAlloc
{
template<typename IData>
friend std::ostream &operator<<(std::ostream &out,
PlainAlloc<IData> const &alloc);
Data d_data;
public:
PlainAlloc()
{}
PlainAlloc(Data data)
:
d_data(data)
{}
PlainAlloc(PlainAlloc<Data> const &other)
:
d_data(other.d_data)
{}
};
new operator to
allocate a new copy of the data:
template <typename Data>
class NewAlloc
{
template<typename IData>
friend std::ostream &operator<<(std::ostream &out,
NewAlloc<IData> const &alloc);
Data *d_data;
public:
NewAlloc()
:
d_data(0)
{}
NewAlloc(Data const &data)
:
d_data(new Data(data))
{}
NewAlloc(NewAlloc<Data> const &other)
:
d_data(new Data(*other.d_data))
{}
~NewAlloc()
{
delete d_data;
}
};
request(),
obtaining the required amount of memory, is left as an exercise to the
reader):
template<typename Data>
class PlacementAlloc
{
template<typename IData>
friend std::ostream &operator<<(std::ostream &out,
PlacementAlloc<IData> const &alloc);
Data *d_data;
static char s_commonPool[];
static char *s_free;
public:
PlacementAlloc()
:
d_data(0)
{}
PlacementAlloc(Data const &data)
:
d_data(new(request()) Data(data))
{}
PlacementAlloc(PlacementAlloc<Data> const &other)
:
d_data(new(request()) Data(*other.d_data))
{}
~PlacementAlloc()
{
d_data->~Data();
}
private:
static char *request();
};
Storage, introduced in the previous section. In addition
to this, additional allocation schemes could be implemented by the user as
well.
In order to be able to apply the proper allocation scheme to the class
Storage it should be designed as a class template itself. The class will
also need a template type parameter allowing users to specify the data type.
The data type to be used by a particular allocation scheme can of course be specified when the allocation scheme itself is specified. This would result in code like this:
template <typename Data, typename Scheme>
class Storage ...
Storage would then be used as follows:
Storage<string, NewAlloc<string> > storage;
This implementation is needlessly complex, as it requires the user to
specify the data type twice. Instead, the allocation scheme should be
specified using a new type of template parameter, not requiring the user to
specify the data type to be used by the allocation scheme. This new kind of
template parameter (in addition to the well-known template type parameter
and template non-type parameter) is called the
template template parameter.
Consider the class Storage, introduced at the beginning of this section.
Also consider the allocation classes discussed in the previous section. To
allow us to specify an allocation policy for the class Storage its
definition starts as follows:
template <typename Data, template <typename> class Policy>
class Storage ...
The second template parameter is the
template template parameter. It consists of the following elements:
template, starting the template
template parameter;
class must be
specified. In this case, typename
can not be used.
template <
template <
typename = std::string,
int = 12,
template < typename = int > class Inner = std::vector
>
class Policy
>
class Demo
{
...
};
Policy becomes a
base class of Storage.
The policy operates on the class Storage's data type. Therefore the
policy is informed about that data type as well. From this we reach the
following setup:
template <typename Data, template <typename> class Policy>
class Storage: public Policy<Data>
This scheme allows us to use the policy's members when implementing the
members of the class Storage.
The allocation classes shown before 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 providing additional
members. E.g., the class NewAlloc could be provided with the following
operators, allowing access to and modification of stored data:
operator Data &() // optionally add a `const' member too
{
return *d_data;
}
NewAlloc &operator=(Data const &data)
{
*d_data = data;
}
Other allocation classes can be given comparable members.
The next step consists of using the allocation schemes in some real
code. The next example shows how a storage can be defined using some data type
and an allocation scheme. First, define a class Storage:
template <typename Data, template <typename> class Policy>
class Storage: public std::vector<Policy<Data> >
{};
That's all we have to do. All required functionality is offered by the
vector base class, while the policy is `factored into the equation' via
the template template parameter. Here's an example showing its use:
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"));
Now the STL copy
function can be used in combination with the back_inserter iterator
to add some data to storage. Its elements can be both accessed and
modified directly using the index operator and then NewAlloc<std::string>
objects are inserted into cout (also using the copy function).
Interestingly, this is not the end of the story. After all, the intention
was to create a class allowing us to specify the storage type as
well. What if we don't want to use a vector, but instead would like to use
a list?
It's easy to change Storage's setup so that a completely different
storage type can be used on request, like a deque. To implement this, the
storage class is parameterized as well, using yet another template template
parameter:
template <typename Data, template <typename> class Policy,
template <typename> class Container =
std::vector>
class Storage: public Container< Policy<Data> >
{
};
The earlier example using a 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 list container, as the list lacks
operator[]. But that's immediately recognized by the compiler, producing
an error if an attempt is made to use operator[] on, e.g., a
list (A complete example showing the definition of the
allocation classes and the class Storage as well as its use is provided in
the Annotation's distribution in the file
yo/advancedtemplates/examples/storage.cc.).
This situation, although legal, should be avoided for various reasons:
vtable is
required as well as a data member pointing to the vtable;
By providing a well-defined interface a class derived from a policy class may
define member specializations using the different structures of policy classes
to their advantage. For example, a plain pointer-based policy class could
offer its functionality by resorting to C-style
pointer juggling,
whereas a vector-based policy class could use the vector's members
directly.
In this example a generic class template Size could be designed
expecting a container-like policy using features commonly found in
containers, defining the data (and hence the structore) of the container
specified in the policy. E.g.:
template <typename Data, template <typename> class Container>
struct Size: public Container<Data>
{
size_t size()
{ // relies on the container's `size()'
// note: can't use `this->size()'
return Container<Data>::size();
}
};
A specialization can now be defined for a much simpler storage class
using, e.g., plain pointers (the implementation capitalizes on first and
second, data members of std::pair. Cf. the example at the end of this
section):
template <typename Data>
struct Size<Data, Plain>: public Plain<Data>
{
size_t size()
{ // relies on pointer data members
return this->second - this->first;
}
};
Depending on the intentions of the template's author other members could
be implemented as well.
To ease the real use of the above templates, a generic wrapper class can
be constructed: it will use the Size template matching the actually used
storage type (e.g., a std::vector or some plain storage class) to define
its structure:
template <typename Data, template <typename> class Store>
class Wrapper: public Size<Data, Store>
{};
The above classes could now be used as follows (en passant showing an
extremely basic Plain class):
#include <iostream>
#include <vector>
template <typename Data>
struct Plain: public std::pair<Data *, Data *>
{};
int main()
{
Wrapper<int, std::vector> wiv;
std::cout << wiv.size() << "\n";
Wrapper<int, Plain> wis;
std::cout << wis.size() << "\n";
}
The wiv object now defines vector-data, the wis object merely
defines a std::pair object's data members.
std namespace
trait classes are found. E.g., most C++ programmers will have seen
the compiler mentioning `std::char_traits<char>' when performing an
illegal operation on std::string objects, as in std::string s(1).
Trait classes are used to make compile-time decisions about types. Traits
classes allow us to apply the proper code to the proper data type, be it a
pointer, a reference, or a plain value, all this maybe in combination with
const. The particular type of data to use can be inferred from the actual
type that is specified (or implied) when the template is used. This can be
fully automated, not requiring the template writer to make any decision.
Trait classes allow us to develop a template <typename Type1, typename
Type2, ...> without the need to specify many specializations covering all
combinations of, e.g., values, (const) pointers, or (const) references, which
would soon result in an unmaintainable exponential explosion of template
specializations (e.g., allowing these five different types for each template
parameter already results in 25 combinations when two template type parameters
are used: each must be covered by potentially different specializations).
Having available a trait class, the actual type can be inferred compile
time, allowing the compiler to deduct whether or not the actual type is a
pointer, a pointer to a member, a const pointer and make comparable deductions
in case the actual type is, e.g., an lvalue or rvalue reference type. This in
turn allows us to write templates that define types like
argument_type,
first_argument_type,
second_argument_type and
result_type, which are required by several generic algorithms (e.g.,
count_if()).
A trait class usually performs no behavior. I.e., it has no constructor
and no members that can be called. Instead, it defines a series of types and
enum values that have certain values depending on the actual type that is
passed to the trait class template. The compiler uses one of a set of
available specializations to select the one appropriate for an actual template
type parameter.
The point of departure when defining a trait template is a plain
vanilla struct, defining the characteristics of a plain value type like
an int. This sets the stage for specific specializations, modifying the
characteristics for any other type that could be specified for the template.
To make matters concrete, assume the intent is to create a trait class
BasicTraits telling us whether a type is a plain value type, a pointer
type, an lvalue reference type or an rvalue reference type (all of which may
or may not be const types).
Whatever the actually provided type, we want to be able to determine the `plain' type (i.e., the type without any modifiers, pointers or references), the `pointer type' and the `reference type', allowing us to define in all cases, e.g., an rvalue reference to its built-in type, even though we passed a const pointer to that type.
Our point of departure, as mentioned, is a plain struct defining the
required parameter. Maybe something like this:
template <typename T>
struct Basic
{
typedef T Type;
enum
{
isPointer = false,
isConst = false,
isRef = false,
isRRef = false
};
};
Although some conclusions can be drawn by combining various enum
values (e.g., a plain type is not a pointer or a reference or a rvalue
reference or a const), it is good practice to provide a full implementation of
trait classes, not requiring its users to construct these logical expressions
themselves. Therefore, the basic decisions in a trait class are usually made
by a
nested trait class,
leaving the task of creating appropriate logical expressions to a
surrounding trait class.
So, the struct Basic defines the generic form of our inner trait
class. Specializations handle specific details. E.g., a pointer type is
recognized by the following specialization:
template <typename T>
struct Basic<T *>
{
typedef T Type;
enum
{
isPointer = true,
isConst = false,
isRef = false,
isRRef = false
};
};
whereas a pointer to a const type is matched with the next specialization:
template <typename T>
struct Basic<T const *>
{
typedef T Type;
enum
{
isPointer = true,
isConst = true,
isRef = false,
isRRef = false
};
};
Several other specializations should be defined: e.g., recognizing
const value types or (rvalue) reference types. Eventually all these
specializations are implemented as nested structs of an outer class
BasicTraits, offering the public traits class interface. The outline of
the outer trait class is:
template <typename TypeParam>
class BasicTraits
{
// Define specializations of the template `Base' here
public:
BasicTraits(BasicTraits const &other) = delete;
typedef typename Basic<TypeParam>::Type ValueType;
typedef ValueType *PtrType;
typedef ValueType &RefType;
typedef ValueType &&RvalueRefType;
enum
{
isPointerType = Basic<TypeParam>::isPointer,
isReferenceType = Basic<TypeParam>::isRef,
isRvalueReferenceType = Basic<TypeParam>::isRRef,
isConst = Basic<TypeParam>::isConst,
isPlainType = not (isPointerType or isReferenceType or
isRvalueReferenceType or isConst)
};
};
The trait class's public interface explicitly deletes its copy
constructor. As it therefore defines no constructor at all and as it has no
static members it does not offer any run-time executable code.
All the trait class's facilities must therefore be used compile time.
A trait class template can be used to obtain the proper type, irrespective
of the template type argument provided. It can also be used to select
a proper specialization that depends on, e.g., the const-ness of a
template type. Example:
cout <<
"int: plain type? " << BasicTraits<int>::isPlainType << "\n"
"int: ptr? " << BasicTraits<int>::isPointerType << "\n"
"int: const? " << BasicTraits<int>::isConst << "\n"
"int *: ptr? " << BasicTraits<int *>::isPointerType << "\n"
"int const *: ptr? " << BasicTraits<int const *>::isPointerType <<
"\n"
"int const: const? " << BasicTraits<int const>::isConst << "\n"
"int: reference? " << BasicTraits<int>::isReferenceType << "\n"
"int &: reference? " << BasicTraits<int &>::isReferenceType << "\n"
"int const &: ref ? " << BasicTraits<int const &>::isReferenceType <<
"\n"
"int const &: const ? " << BasicTraits<int const &>::isConst << "\n"
"int &&: r-reference? " << BasicTraits<int &&>::isRvalueReferenceType <<
"\n"
"int &&: const? " << BasicTraits<int &&>::isConst << "\n"
"int const &&: r-ref ? "<< BasicTraits<int const &&>::
isRvalueReferenceType << "\n"
"int const &&: const ? "<< BasicTraits<int const &&>::isConst << "\n"
"\n";
BasicTraits<int *>::ValueType value = 12;
BasicTraits<int const *>::RvalueRefType rvalue = int(10);
BasicTraits<int const &&>::PtrType ptr = new int(14);
cout << value << ' ' << rvalue << ' ' << *ptr << '\n';
TypeTrait trait class was developed. Using
specialized versions of a nested struct Type modifiers, pointers,
references and values could be distinguished.
Knowing whether a type is a class type or not (e.g., the type represents a primitive type) could also be a useful bit of knowledge to a template developer. The class template developer might want to define a specialization when the template's type parameter represents a class type (maybe using some member function that should be available) and another specialization for non-class types.
This section addresses the question how a trait class can distinguish class types from non-class types.
In order to distinguish classes from non-class types a distinguishing feature that can be used at compile-time must be found. It may take some thinking to find such a distinguishing characteristic, but a good candidate eventually is found in the pointer to members syntactic construct. Pointers to members are only available for classes. Using the pointer to member construct as the distinguishing characteristic, a specialization can be developed that uses the pointer to member if available. Another specialization (or the generic template) does something else if the pointer to member construction is not available.
How can we distinguish a pointer to a member from `a generic situation', not being a pointer to a member? Fortunately, such a distinction is possible. A function template specialization can be defined having a parameter which is a pointer to a member function. The generic function template will then accept any other argument. The compiler will select the former (specialized) function when the provided type is a class type as class types may support a pointer to a member. The interesting verb here is `may': the class does not have to define a pointer to member.
Furthermore, the compiler will not actually call any function: we're talking compile-time here. All the compiler does is to select the appropriate function by evaluating a constant expression.
So, our intended function template will look like this:
template <typename ClassType>
static `some returntype' fun(void (ClassType::*)());
The function's return type (`(some returntype)') will be defined
shortly. Let's first have a closer look at the function's parameter. The
function's parameter defines a pointer to a member returning void. Such a
function does not have to exist for the concrete class-type that's
specified when the function is used. In fact, no implementation will be
provided. The function fun is only declared as a static member in the
trait class. It's not implemented and no trait class object is required to
call it. What, then, is its use?
To answer the question we now have a look at the generic function template that should be used when the template's argument is not a class type. The language offers a `worst case' parameter in its ellipsis parameter list. The ellipsis is a final resort the compiler may turn to if everything else fails. The generic function template specifies a plain ellipsis in its parameter list:
template <typename NonClassType>
static `some returntype' fun(...);
It would be an error to define the generic alternative as a function
expecting an int. The compiler, when confronted with alternatives, will
favor the simplest, most specified alternative over a more complex, generic
one. So, when providing fun with an argument it will select int
whenever possible and it won't select fun(void (ClassType::*)()). When
given the choice between fun(void (ClassType::*)()) and fun(...) it
will select the former unless it can't do that.
The question now becomes: what argument can be used for both a pointer to a member and for the ellipsis? Actually, there is such a `one size fits all' argument: 0. The value 0 can be used as argument value to initialize and pointers to members alike.
But 0 does not specify a particular class. Therefore, fun must specify
an explicit template argument and it will appear in our code as
fun<Type>(0), with Type being the template type parameter of the trait
class.
Now for the return type. The function's return type cannot be a simple
value (like true or false). Our eventual intent is to provide the
trait class with an enum telling us whether the trait class's template
argument represents a class type or not. That enum will be something like
this:
enum { isClass = some class/non-class distinguishing expression } ;
The distinguishing expression cannot be
enum { isClass = fun<Type>(0) } ;
as fun<Type>(0) is not a constant expression and enum values must
be defined by constant expressions so they can be determined at compile-time.
To determine isClass's value we must find an expression that allows
compile-time discriminates between fun<Type>(...) and fun<Type>(void
(Type::*)()).
In situations like these the
sizeof operator often is our tool of
choice as it is evaluated at compile-time. By defining different sized return
types for the two fun declarations we are able to distinguish
(compile-time) which of the two fun alternatives is selected by the
compiler.
The char type is by definition a type having size 1. By defining
another type containing two consecutive char values a larger type is
obtained. A char [2] is of course not a type, but a char[2] can be
defined as a data member of a struct, and a struct does define a
type. That struct will then have a size exceeding 1. E.g.,
struct Char2
{
char data[2];
};
Char2 can be defined as a nested type of our traits class. The
two fun function template declarations become:
template <typename ClassType>
static Char2 fun(void (ClassType::*)());
template <typename NonClassType>
static char fun(...);
Since typeof expressions can be evaluated at compile-time we can now
determine isClass's value:
enum { isClass = sizeof(fun<Type>(0)) == sizeof(Char2) };
This expression has several interesting implications:
fun function template is ever instantiated;
Type and selects
fun's function template specialization if Type is a class type and the
generic function template if not;
isClass.
<type_traits>
header file must be included.
All facilities offered by type_traits are defined in the std namespace
(omitted from the examples given below) allowing programmers to
is_lvalue_reference<typename Type>::value);
is_rvalue_reference<typename Type>::value);
is_reference<typename Type>::value);
is_signed<typename Type>::value);
is_unsigned<typename Type>::value);
is_pod<typename Type>::value);
has_trivial_default_constructor<typename Type>::value);
This concept of a trivial
member is also used in the next few type trait facilities.
has_trivial_copy_constructor<typename Type>::value);
has_trivial_destructor<typename Type>::value);
has_trivial_assign<typename Type>::value);
has_nothrow_default_constructor<typename Type>::value);
has_nothrow_destructor<typename Type>::value);
has_nothrow_copy_constructor<typename Type>::value);
has_nothrow_assign<typename Type>::value);
Base is a base class of another type
Derivedis_base_of<typename Base, typename Derived>::value);
From may be converted to a type To
(e.g., using a static_cast)is_convertible<typename From, typename To>::value);
Type if cond is trueenable_if<bool cond, typename Type>::type);
TrueType if cond is true, FalseType if
notconditional<bool cond, typename TrueType, typename
FalseType>::type);
remove_reference<typename Type>::type);
add_lvalue_reference<typename Type>::type);
add_rvalue_reference<typename Type>::type);
make_unsigned<typename Type>::type);
make_signed<typename Type>::type);
transform (cf. section 19.1.63) generic algorithm:
template <typename Return, typename Argument>
Return chop(Argument const &arg)
{
return Return(arg);
}
Furthermore assume that if Return is std::string then the above
implementation should not be used. Instead, with std::string a second
argument 1 should always be provided. This would allow us, if Argument
is a C++ string, to return a copy of arg from which its first
character has been chopped off.
Since chop is a function, it is not possible to define a partial
specialization like this:
template <typename Argument> // This won't compile!
std::string chop<std::string, Argument>(Argument const &arg)
{
return string(arg, 1);
}
Although a function template cannot be partially specialized it is
possible to use overloading, defining a second, dummy string parameter:
template <typename Argument>
std::string chop(Argument const &arg, std::string )
{
return string(arg, 1);
}
Now it is possible to distinguish the two cases, but at the expense of
a more complex function call. At the downside, in code this function may
requiring the use of the bind2nd binder (cf. section 18.1.4) to
provide the dummy second argument or it may require a (possibly expensive to
construct) dummy argument to allow the compiler to choose among the two
overloaded function templates.
Instead of providing a string dummy argument the functions could
use the IntType template (cf. section 22.2.1.1) to select the proper
overloaded version. E.g., IntType<0> could be defined as the type of the
second argument of the first overloaded chop function, and IntType<1>
could be used for the second overloaded function. From the point of view of
program efficiency this is an attractive option, as the provided IntType
objects are extremely lightweight. IntType objects contain no data at
all. But there's also an obvious disadvantage as there is no intuitively clear
association between the int value used and the intended type.
Instead of defining arbitrary IntType types it is more attractive to
use another lightweight solution, using an automatic type-to-type
association. The struct TypeType is a lightweight type wrapper, much like
IntType. Here is its definition:
template <typename T>
struct TypeType
{
typedef T Type;
};
TypeType is also a lightweight type as it doesn't have any data fields
either. TypeType allows us to use a natural type association for
chop's second argument. E.g, the overloaded functions can now be defined
as follows:
template <typename Return, typename Argument>
Return chop(Argument const &arg, TypeType<Argument> )
{
return Return(arg);
}
template <typename Argument>
std::string chop(Argument const &arg, TypeType<std::string> )
{
return std::string(arg, 1);
}
Using the above implementations any type can be specified for
Result. If it happens to be a std::string the appropriate overloaded
version is automatically selected. The following additional overload of the
function chop capitalizes on this:
template <typename Result>
Result chop(char const *txt) // char const * could also be a 2nd
{ // template type parameter
return chop(std::string(txt), TypeType<Result>());
}
Using the third chop function, the following statement produces the
text `ello world':
cout << chop<string>("hello world") << '\n';
Template functions do not support partial specializations. But they can be
overloaded. By providing overloads with dummy type-arguments that depend
on other parameters and calling these overloads from a overloaded function
that does not require the dummy type argument a situation similar to partial
specializations with class templates can often be realized.
struct is a useful tool. It can be used as a type acting
analogously to the
ASCII-Z (final 0-byte) in C-strings.
It can simply be defined as:
struct NullType
{};
T be used as a `stand in' for another type
U? Since C++ is a strongly typed language the answer is surprisingly
simple: Ts can be used instead of Us if a T is accepted as
argument in cases where Us are requested.
This reasoning is behind the following class which can be used to determine
whether a type T can be used where a type U is expected. The
interesting part is that no code is actually generated or executed. All
decisions are made by the compiler.
In the second part of this section we'll show shown how the code developed in
the first part can be used to detect whether a class B is a base class of
another clas D (the is_base_of template (cf. section 22.4.2)
also provides an answer to this question). The code developed here closely
follows the example provided by Alexandrescu
(2001,
p. 35).
First, a function test is designed accepting a type U.
The function test returns a value
of the as yet unknown type Convertible:
Convertible test(U const &);
The function test is never implemented. It is only declared. If a type
T can be used instead of a type U then T can also be passed as
argument to the above test function.
On the other hand, if the alternate type T cannot be used where a U is
expected, then the compiler won't be able to use the above test
function. Instead, it will use an alternative function that has a lower
selection priority but that can always be used with any T type.
C (and C++) offer a very general parameter list, a parameter list that will always be considered acceptable. This parameter list is the familiar ellipsis which represents the worst case the compiler may encounter. If everything else fails, then the function defining an ellipsis as its parameter list is selected.
Usually that's not a productive alternative, but in the current situation it is exactly what is needed. When confronted with two candidate functions, one of which defines an ellipsis parameter, the compiler will select the function defining the ellipsis parameter only if the alternative(s) can't be used.
Following the above reasoning an alternative function test(...) is
declared as well. This alternate function does not return a Convertible
value but a NotConvertible value:
NotConvertible test(...);
The return type test provided with a value of type T will be
Convertible if T can be converted to U. Otherwise
NotConvertible is returned.
This situation clearly shows similarities with the situation encountered in
section 22.4.1 where the value isClass had to be determined
compile time. Here two related problems must be solved:
T argument? This is more difficult than might
be expected at first sight as it might not be possible to define a T. If
type T does not define any constructor then no T object can be
defined.
Convertible be distinguished from NotConvertible?
T needs to be
defined. After all, the intent is to decide compile-time whether a type is
convertible and not to define a T value or object. Defining objects is not
a compile-time but a run-time matter.
By simply declaring a function returning a T we can
tell the compiler where it should assume a T:
T makeT();
This mysterious function has the magical power of enticing the compiler
into thinking that a T object will come out of it. This happens in the
following code:
test(makeT())
Now that the compiler sees test being called with a T argument it
will decide that its return value is Convertible if a conversion is in
fact possible. Otherwise it will decide that its return value is
NotConvertible (as it selected test(...) in that case).
The second problem, distinguishing Convertible from NotConvertible
is solved exactly the way isClass could be determined in section
22.4.1, viz. by making their sizes different. Having done so the
following expression determines whether T is convertible from U or
not:
isConvertible = sizeof(test(makeT())) == sizeof(Convertible);
By using char for Convertible and Char2 (cf. section
22.4.1) for NotConvertible the distinction can be made.
The above can be summarized in a class template LconvertibleToR,
having two template type parameters:
template <typename T, typename U>
class LconvertibleToR
{
struct Char2
{
char array[2];
};
static T makeT();
static char test(U const &);
static Char2 test(...);
public:
LconvertibleToR(LconvertibleToR const &other) = delete;
enum { yes = sizeof(test(makeT())) == sizeof(char) };
enum { sameType = 0 };
};
template <typename T>
class LconvertibleToR<T, T>
{
public:
LconvertibleToR(LconvertibleToR const &other) = delete;
enum { yes = 1 };
enum { sameType = 1 };
};
As the class template deletes its copy constructor no object can be
created. Only its enum values can be interrogated. The next example writes
1 0 1 0 when run from a main function:
cout <<
LconvertibleToR<ofstream, ostream>::yes << " " <<
LconvertibleToR<ostream, ofstream>::yes << " " <<
LconvertibleToR<int, double>::yes << " " <<
LconvertibleToR<int, string>::yes <<
"\n";
Conversion has been defined it's easy to determine whether a type
Base is a (public) base class of a type Derived.
Inheritance is determined by inspecting convertability of (const)
pointers. Derived const * can be converted to Base const * if
Base is a public and unambiguous base class of Derived;
Base is void.
LBaseRDerived. LBaseRDerived provides
an enum yes which is 1 if the left type is a base class of the right type
and both types are different:
template <typename Base, typename Derived>
struct LBaseRDerived
{
LBaseRDerived(LBaseRDerived const &) = delete;
enum {
yes =
LconvertibleToR<Derived const *, Base const *>::yes &&
not LconvertibleToR<Base const *, void const *>::sameType
};
};
If code should not consider a class to be its own base class, then the
trait class LBaseRtrulyDerived can be used to perform a strict test. This
trait class adds a test for type-equality:
template <typename Base, typename Derived>
struct LBaseRtrulyDerived
{
LBaseRtrulyDerived(LBaseRtrulyDerived const &) = delete;
enum {
yes =
LBaseRDerived<Base, Derived>::yes &&
not LconvertibleToR<Base const *, Derived const *>::sameType
};
};
Example: the next statement displays 1: 0, 2: 1, 3: 0, 4: 1, 5: 0
when executed from a main function:
cout << "\n" <<
"1: " << LBaseRDerived<ofstream, ostream>::yes << ", " <<
"2: " << LBaseRDerived<ostream, ofstream>::yes << ", " <<
"3: " << LBaseRDerived<void, ofstream>::yes << ", " <<
"4: " << LBaseRDerived<ostream, ostream>::yes << ", " <<
"5: " << LBaseRtrulyDerived<ostream, ostream>::yes <<
"\n";
This section itself was inspired by Andrei Alexandrescu's (2001) book Modern C++ design. It diverts from Alexandrescu's book in its use of variadic templates which were not yet available when he wrote his book. Even so, the algorithms used by Alexandrescu are still useful when using variadic templates.
The C++0x standard offers the tuple to store and retrieve
values of multiple types. Here the focus is merely on processing types.
A simple struct TypeList will be our working horse for the upcoming
subsections. Here is its definition:
template <typename ... Types>
struct TypeList
{
TypeList(TypeList const &) = delete;
enum { size = sizeof ... (Types) };
};
A typelist allows us to store any number of types. Here is an example
storing the three types char, short, int in a TypeList:
TypeList<char, short, int>
sizeof
operator (cf. section 21.5) it is easy to obtain the number of types
that were specified with a certain TypeList. For example, the following
statement displays the value 3:
std::cout << TypeList<int, char, bool>::size << '\n';
However, it's illustrative to see how the number of types specified with a
TypeList could be determined if sizeof hadn't been available.
To obtain the number of types that were specified with a TypeList
the following algorithm is used:
TypeList contains no types, its size equals zero;
TypeList contains types, its size equals 1 plus the number
of types that follow its first type.
TypeList. In
executable C++ recursion could also be used in comparable situations. For
example recursion can be used to determine the length of a plain C
(ascii-Z) string:
size_t c_length(char const *cp)
{
return *cp == 0 ? 0 : 1 + c_length(cp + 1);
}
While C++ functions usually use iteration rather than recursion,
iteration is not available to template meta programming algorithms. In
template meta programming repetition must be implemented using
recursion. Furthermore, while C++ run-time code may use conditions to
decide whether or not to start the next recursion template meta programming
cannot do so. Template meta programming algorithms must resort to (partial)
specializations. The specializations are used to select alternatives.
The number of types that are specified in a TypeList can be computed
using the following alternate implementation of TypeList, using a generic
struct declaration and two specialization for the empty and non-empty
TypeList (cf. the above description of the algorithm):
template <typename ... Types>
struct TypeList;
template <typename Head, typename ... Tail>
struct TypeList
{
enum { size = 1 + TypeList<Tail ...>::size };
};
template <>
struct TypeList<>
{
enum { size = 0 };
};
TypeList, an algorithm is used that either defines
`index'as -1 (if SearchType is not an element of the TypeList ) or it
defines `index' as the index of the first occurrence of SearchType in
the TypeList. The following algorithm is used:
TypeList is empty, `index' is -1;
TypeList's first element equals SearchType, `index' is 0;
TypeList's tail results in
`index' == -1;
TypeList's tail) index is
set to 1 + the index obtained when searching for SearchType in the
TypeList's tail.
ListSearch expecting a parameter pack:
template <typename ... Types>
struct ListSearch
{
ListSearch(ListSearch const &) = delete;
};
Specializations handle the alternatives mentioned with the algorithm:
TypeList is empty, `index' is -1:
template <typename SearchType>
struct ListSearch<SearchType, TypeList<>>
{
ListSearch(ListSearch const &) = delete;
enum { index = -1 };
};
TypeList's head equals SearchType, `index'
is 0. Note that SearchType is explicitly mentioned as the
TypeList's first element:
template <typename SearchType, typename ... Tail>
struct ListSearch<SearchType, TypeList<SearchType, Tail ...>>
{
ListSearch(ListSearch const &) = delete;
enum { index = 0 };
};
TypeList's tail. The index
value returned by this search is stored in a tmp enum value,
which is then used to determine index's value.
template <typename SearchType, typename Head, typename ... Tail>
struct ListSearch<SearchType, TypeList<Head, Tail ...> >
{
ListSearch(ListSearch const &) = delete;
enum {tmp = ListSearch<SearchType, TypeList<Tail ...>>::index};
enum {index = tmp == -1 ? -1 : 1 + tmp};
};
ListSearch can be used:
std::cout <<
ListSearch<char, TypeList<int, char, bool>>::index << "\n" <<
ListSearch<float, TypeList<int, char, bool>>::index << "\n";
TypeList is retrieving the type given its index. This inverse operation is
the topic of this section.
The algorithm is implemented using a struct TypeAt. TypeAt uses a
typedef to define the type matching a given index. But the index might be
out of bounds. In that case we have several options:
Null) that should not be used as a
type in the TypeList. Using that local type as a type in a TypeList is
considered an error as its use in a TypeList could easily cause confusion
(was the index invalid? did we actually stumble across Null?). A
static_cast may be used to prevent Null from being returned as the
type matching the search index;
TypeAt may define a enum value validIndex set to
true if the index was valid and set to false if not.
TypeAt works:
TypeAt,
expecting an index and a TypeList:
template <size_t index, typename Typelist>
struct TypeAt;
static_assert ends the compilation
template <size_t index>
struct TypeAt<index, TypeList<>>
{
static_assert(index < 0, "TypeAt index out of bounds");
typedef TypeAt Type;
};
Type as the first type in
the TypeList:
template <typename Head, typename ... Tail>
struct TypeAt<0, TypeList<Head, Tail ...>>
{
typedef Head Type;
};
Type is defined as Type defined by TypeAt<index
- 1> operating on the TypeList's tail:
template <size_t index, typename Head, typename ... Tail>
struct TypeAt<index, TypeList<Head, Tail ...>>
{
typedef typename TypeAt<index - 1, TypeList<Tail ...>>::Type Type;
};
typeAt can be used. Uncommenting the first variable
definition causes a TypeAt index out of bounds compilation error:
typedef TypeList<int, char, bool> list3;
// TypeAt<3, list3>::Type invalid;
TypeAt<0, list3>::Type intVariable = 13;
TypeAt<2, list3>::Type boolVariable = true;
cout << "The size of the first type is " <<
sizeof(TypeAt<0, list3>::Type) << ", "
"the size of the third type is " <<
sizeof(TypeAt<2, list3>::Type) << "\n";
if (typeid(TypeAt<1, list3>::Type) == typeid(char))
cout << "The typelist's 2nd type is char\n";
if (typeid(TypeAt<2, list3>::Type) != typeid(char))
cout << "The typelist's 3nd type is not char\n";
TypeList is easy and doesn't require
recursive template meta programs. Two variadic template structs Append and
Prefix and two specializations are all it takes.
Here are the declarations of the two variadic template structs:
template <typename ... Types>
struct Append;
template <typename ... Types>
struct Prefix;
To append or prefix a new type to a typelist specializations expect a
typelist and a type to add and simply define a new TypeList also including
the new type. The Append specialization shows that a template pack does
not have to be used as the first argument when defining another variadic
template type:
template <typename NewType, typename ... Types>
struct Append<TypeList<Types ...>, NewType>
{
typedef TypeList<Types ..., NewType> List;
};
template <typename NewType, typename ... Types>
struct Prefix<NewType, TypeList<Types ...>>
{
typedef TypeList<NewType, Types ...> List;
};
TypeList. Again, there are
several possibilities, each resulting in a different algorithm.
TypeList;
TypeList;
TypeList.
TypeList, keeping each type only once.
TypeList. Which ones eventually will be implemented depends of course on
the circumstances. As template meta programming is very powerful most if not
all algorithms can probably be implemented. As an illustration of how to erase
types from a TypeList the abovementioned algorithms are now developed in
the upcoming subsections.
EraseType from a TypeList
a recursive algorithm is used once again. The template meta program uses a
generic Erase struct and several specializations. The specializations
define a type List containing the resulting TypeList after the
erasure. Here is the algorithm:
Erase expecting the type to erase and a TypeList:
template <typename EraseType, typename TypeList>
struct Erase;
TypeList results:
template <typename EraseType>
struct Erase<EraseType, TypeList<>>
{
typedef TypeList<> List;
};
TypeList's head matches the type to erase, then List
will be a TypeList containing the original TypeList's tail
types:
template <typename EraseType, typename ... Tail>
struct Erase<EraseType, TypeList<EraseType, Tail ...>>
{
typedef TypeList<Tail ...> List;
};
TypeList's tail. This results in a TypeList to which the
orginal TypeList's head must be prefixed. The TypeList
returned by the prefix operation is then returned as Erase::Type:
template <typename EraseType, typename Head, typename ... Tail>
struct Erase<EraseType, TypeList<Head, Tail ...>>
{
typedef typename
Prefix<Head,
typename Erase<EraseType, TypeList<Tail ...>>::List
>::List List;
};
Erase can be used:
cout <<
Erase<int, TypeList<char, double, int>>::List::size << '\n' <<
Erase<char, TypeList<int>>::List::size << '\n' <<
Erase<int, TypeList<int>>::List::size << '\n' <<
Erase<int, TypeList<>>::List::size << "\n";
TypeList by its index we again use a
recursive template meta program. EraseIdx expects a size_t index value
and a TypeList from which its idxsup(th) type must be
erased. EraseIdx defines the type List containing the resulting
TypeList. Here is the algorithm:
EraseIdx expecting the index of the type to erase and a TypeList:
template <size_t idx, typename TypeList>
struct EraseIdx;
TypeList results:
template <size_t idx>
struct EraseIdx<idx, TypeList<>>
{
typedef TypeList<> List;
};
idx becomes 0. At that point
the TypeList's first type is ignored and Type is initialized
to a TypeList containing the types in the orginal TypeList's
tail:
template <typename EraseType, typename ... Tail>
struct EraseIdx<0, TypeList<EraseType, Tail ...>>
{
typedef TypeList<Tail ...> List;
};
EraseIdx is applied to the TypeList's
tail, providing it with a decremented value of idx. To the
resulting TypeList the orginal TypeList's head is
prefixed. The TypeList returned by the prefix operation is then
returned as EraseIdx::Type:
template <size_t idx, typename Head, typename ... Tail>
struct EraseIdx<idx, TypeList<Head, Tail ...>>
{
typedef typename Prefix<
Head,
typename EraseIdx<idx - 1, TypeList<Tail ...>>::List
>::List List;
};
EraseIdx can be used:
if
(
typeid(TypeAt<2,
EraseIdx<1,
TypeList<int, char, size_t, double, int>>::List
>::Type
)
== typeid(double)
)
cout << "the third type is now a double\n";
EraseType from a TypeList can easily be accomplished
by applying the erasure procedure not only to the head of the TypeList but
also to the TypeList's tail.
Here is the algorithm, described in a slightly different order than
Erase's algorithm:
TypeList is empty, there's nothing to erase, and an empty
TypeList results. This is exactly what we do with Erase, so we
can use inheritance to prevent us from having to duplicate elements of
a template meta program:
EraseAll expecting the type to erase and a TypeList that is
derived from Erase, thus already offering the empty TypeList
handling specialization:
template <typename EraseType, typename TypeList>
struct EraseAll: public Erase<EraseType, TypeList>
{};
TypeList's head matches EraseType EraseAll is also
applied to the TypeList's tail, thus removing all occurrences of
EraseType from TypeList:
template <typename EraseType, typename ... Tail>
struct EraseAll<EraseType, TypeList<EraseType, Tail ...>>
{
typedef typename EraseAll<EraseType, TypeList<Tail ...>>::List List;
};
TypeList's head does not match
EraseType) EraseAll is applied to the TypeList's tail.
The returned TypeList consists of the original TypeList's
initial type and the types of the TypeList returned by the
recursive EraseAll call:
template <typename EraseType, typename Head, typename ... Tail>
struct EraseAll<EraseType, TypeList<Head, Tail ...>>
{
typedef typename Prefix<
Head,
typename EraseAll<EraseType, TypeList<Tail ...>>::List
>::List List;
};
EraseAll can be used:
cout <<
"After erasing size_t from "
"TypeList<char, int, size_t, double, size_t>\n"
"it contains " <<
EraseAll<size_t,
TypeList<char, int, size_t, double, size_t>
>::List::size << " types\n";
TypeList all the TypeList's first
elements must be erased from the TypeList's tail, applying the procedure
recursively to the TypeList's tail. The algorithm, outlined below, merely
expects a TypeList:
EraseDup struct template is
declared. EraseDup structures define a type List representing the
TypeList that they generate. EraseDup calls expect a TypeList as
their template type parameters:
template <typename TypeList>
struct EraseDup;
TypeList is empty it can be returned empty and we're done:
template <>
struct EraseDup<TypeList<>>
{
typedef TypeList<> List;
};
EraseDup is first applied to the original TypeList's
tail. By definition this will result in a TypeList from which all
duplicates have been removed;
TypeList returned by the previous step might contain the
original TypeList's initial type. If so, it will be removed by applying
Erase on the returned TypeList, specifying the original TypeList's
initial type as the type to remove;
TypeList consists of the original TypeList's
initial type to which the types of the TypeList produced by the previous
step are appended.
template <typename Head, typename ... Tail>
struct EraseDup<TypeList<Head, Tail ...>>
{
typedef typename EraseDup<TypeList<Tail ...>>::List UniqueTail;
typedef typename Erase<Head, UniqueTail>::List NewTail;
typedef typename Prefix<Head, NewTail>::List List;
};
EraseDup can be used:
cout <<
"After erasing duplicates from "
"TypeList<double, char, int, size_t, int, double, size_t>\n"
"it contains " <<
EraseDup<
TypeList<double, char, int, size_t, int, double, size_t>
>::List::size << " types\n";
But there's more to typelist than a mere intellectual challenge. In the final sections of this chapter the following topics are covered: will be covered:
Again, much of the material covered by these sections was inspired by Alexandrescu's (2001) book, this time combined with the features offered by the C++0x standard.
Multi is now developed. The class template Multi creates
a new class from a template template parameter Policy defining the data
storage policy and a series of types from which Multi is
eventually derived. It does so by passing its template parameters to its base
class MultiBase that in turn creates a final class inheritance
tree. Since we don't know how many types will be used Multi is defined as
a variadic class template using a template pack ... Types.
In fact, the types that are specified with Multi aren't that
interesting. They primarily serve to `seed' the class Policy
with. Thererfore, rather than forwarding Multi's types to MultiBase
they are passed to Policy and the sequence of Policy<Type> types is
then forwarded to MultiBase. Multi's constructor expects
initialization values for its various Policy<Type>s which are perfectly
forwarded to MultiBase.
The class Multi (implementing its constructor in-class to save some
space) shows how a
template pack can be wrapped into a policy. Here is Multi's definition:
template <template <typename> class Policy, typename ... Types>
struct Multi: public MultiBase<0, Policy<Types> ...>
{
typedef TypeList<Types ...> PlainTypes;
typedef MultiBase<0, Policy<Types> ...> Base;
enum { size = PlainTypes::size };
Multi(Policy<Types> &&... types)
:
MultiBase<0, Policy<Types> ...>(
std::forward<Policy<Types>>(types) ...)
{}
};
Unfortunately, the design as described contains some flaws.
Policy template template parameter is defined as
template <typename> class Policy it can only accept policies expecting one
type argument. Contrary to this, std::vector> is a template expecting two
template arguments, the second one defining the allocation scheme used by
std::vector. This allocation scheme is hardly ever changed, and most
applications merely define objects of types like vector<int>,
vector<string> etc.. Template template parameters must, however, be specified
with the correct number and types of required template parameters so
vector can't be specified as a policy for Multi. This can be solved by
wrapping a more complex template in a simpler wrapper template, like so:
template <class Type>
struct Vector: public std::vector<Type>
{
Vector(std::initializer_list<Type> iniValues)
:
std::vector<Type>(iniValues)
{}
};
Now Vector will provide std::vector's second parameter using its
default template argument. Alternatively, a template using declaration
could be used.
TypeList contains two types like int and double
and the policy class is Vector, then the MultiBase class will
eventually inherit from vector<int> and vector<double>. But if the
TypeList contains identical types, like two int type specifications
MultiBase would inherit from two vector<int> classes. Classes
cannot be derived from identical base classes as that would make it impossible
to distinguish among their members Regarding this, Alexandrescu (2001)
writes (p.67):
There is one major source of annoyance...: you cannot use it when you have duplicate types in yourTypeList.
.... There is no easy way to solve the ambiguity, [as the eventually derived class/FBB] ends up inheriting [the same base class/FBB] twice.
IntType. The class template UWrap has two template parameters: one
non-type parameter idx and one type parameter. By ensuring that each
UWrap definition uses a unique idx value unique class types are
created. These unique class types are then used as base classes of the derived
class MultiBase:
template <size_t nr, typename Type>
struct UWrap: public Type
{
UWrap(Type const &type)
:
Type(type)
{}
};
Using UWrap it's easy to distinguish, e.g., two vector<int>
classes: UWrap<0, vector<int>> could refer to the first vector<int>,
UWrap<1, vector<int>> to the second vector.
Uniqueness of the various UWrap types is assured by the class
template MultiBase as discussed in the next section.
It must also be possible to initialize a Multi class object. Its
constructor therefore expects the initialization values for all its Policy
values. So if a Multi is defined for Vector, int, string then its
constructor can receive the matching initialization values. E.g.,
Multi<Vector, int, string> mvis({1, 2, 3}, {"one", "two", "three"});
MultiBase is Multi's base class. It
defines a class that, eventually, is derived from the list of Policy
types that, in turn, were created by Multi using any additional types that
were passed to it.
MultiBase itself has no concept of a Policy. To MultiBase the
world consists of a simple template pack whose types will be used to define a
class from. In addition to the PolicyTypes template pack, MultiBase
also defines a size_t nr non-type parameter that is used to create unique
UWrap types. Here is MultiBase's generic class declaration:
template <size_t nr, typename ... PolicyTypes>
struct MultiBase;
Two specializations handle all possible MultiBase invocations. One
specialization is a recursive template. This template handles the first type
of MultiBase's template parameter pack and recursively uses itself to
handle the remaining types. The second specialization is invoked once the
template parameter pack is exhausted and does nothing. Here is the definition
of the latter specialization:
template <size_t nr>
struct MultiBase<nr>
{};
The recursively defined specialization is the interesting one. It performs the following tasks:
UWrap type. The uniqueness is
guaranteerd by using MultiBase's nr parameter when defining
UWrap. In addition to nr the UWrap class receives the first type
of the template parameter pack made available to MultiBase;
MultiBase type is defined using as its first template argument an
incremented nr value (thus ensuring the uniqueness of the UWrap types
defined by recursive MultiWrap types). Its second template argument is the
tail of the template parameter pack made available to MultiBase
MultiBase class
hierarchy is provided in figure 21.

MultiBase's constructor simple receives the initialization values that
were passed to (originally) the Multi object. To accomplish this
perfect forwarding is used. MultiBase's constructor passes its first
parameter value to its UWrap base class, also using perfect forwarding.
MultiBase's recursive definition is:
template <size_t nr, typename PolicyT1, typename ... PolicyTypes>
struct MultiBase<nr, PolicyT1, PolicyTypes ...> :
public UWrap<nr, PolicyT1>,
public MultiBase<nr + 1, PolicyTypes ...>
{
typedef PolicyT1 Type;
typedef MultiBase<nr + 1, typename PolicyTypes ...> Base;
MultiBase(PolicyT1 && policyt1, PolicyTypes &&... policytypes)
:
UWrap<nr, PolicyT1>(std::forward<PolicyT1>(policyt1)),
MultiBase<nr + 1, PolicyTypes ...>(
std::forward<PolicyTypes>(policytypes) ...)
{}
};
Multi class template defines PlainTypes as the TypeList
holding all the types of its parameter pack. Each MultiType derived from a
UWrap type also defines a type Type representing the policy type that
was used to define the UWrap type and a type Base representing the
type of its nested MultiBase class.
These three type definitions allow us to access the types from which the
Multi object was created as well as the values of those types.
The class template typeAt, is a pure template meta program class template
(it has no run-time executable code). It expects a size_t idx template
argument specifying the index of the policy type in a Multi type object as
well as a Multi class type. It defines the type Type as the Type
defined by Multi's MultiBase<idx, ... base class. Example:
typeAt<0, Multi<Vector, int, double>>::Type // Type is vector<double>
The class template typeAt defines (and uses) a nested class template
PolType doing all the work. PolType's generic definition specifies two
template parameters: an index used to specify the index of the requested type
and a typename which initialized by a MultiBase type
argument. PolType's recursive definition recursively reduces its index
non-type parameter, passsing the next base class in MultiBase's
inheritance tree to the recursive call. As PolType eventually defines the
type Type to be the requested policy type the recursive definition defines
its Type as the type defined by the recursive call. The final
(non-recursive) specialization defines the initial policy type of the
MultiBase type as Type. Here is typeAt's definition:
template <size_t index, typename Multi>
class typeAt
{
template <size_t idx, typename MultiBase>
struct PolType;
template <size_t idx,
size_t nr, typename PolicyT1, typename ... PolicyTypes>
struct PolType<idx, MultiBase<nr, PolicyT1, PolicyTypes ...>>
{
typedef typename PolType<
idx - 1, MultiBase<nr + 1, PolicyTypes ...>>::Type Type;
};
template <size_t nr, typename PolicyT1, typename ... PolicyTypes>
struct PolType<0, MultiBase<nr, PolicyT1, PolicyTypes ...>>
{
typedef PolicyT1 Type;
};
public:
typeAt(typeAt const &) = delete;
typedef typename PolType<index, typename Multi::Base>::Type Type;
};
The types specified by Multi's parameter pack can also be retrieved using
a second helper class template: plainTypeAt. Example:
plainTypeAt<0, Multi<Vector, int, double>>::Type // Type is double
The class template plainTypeAt uses a comparable (but simpler)
implementation than typeAt. It is also a pure template meta program class
template defining a nested class template At. At is implemented like
typeAt but it visits the types of the original template pack that was
passed to Multi, and made available by Multi as its PlainTypes
type. Here is plainTypeAt's definition:
template <size_t index, typename Multi>
class plainTypeAt
{
template <size_t idx, typename List>
struct At;
template <size_t idx, typename Head, typename ... Tail>
struct At<idx, TypeList<Head, Tail...>>
{
typedef typename At<idx - 1, TypeList<Tail ...>>::Type Type;
};
template <typename Head, typename ... Tail>
struct At<0, TypeList<Head, Tail...>>
{
typedef Head Type;
};
public:
plainTypeAt(plainTypeAt const &) = delete;
typedef typename At<index, typename Multi::PlainTypes>::Type Type;
};
Arguably the neatest support template is get. This is a function template
defining size_t idx as its first template parameter and typename Multi
as its second template parameter. Get defines one function parameter: a
reference to a Multi, so it can deduct Multi's type by itself. Knowing
that it's a Multi, we reason that it is also a UWrap<nr, PolicyType>
and therefore also a PolicyType, as the latter class is defined as a base
class of UWrap.
Since class type objects can initialize references to their base classes the
PolicyType & can be initialized by an appropriate UWrap reference,
which in turn can be initialized by a Multi object. Since we can
determine PolicyType using TypeAt (note that evaluating typename
typeAt<idx, Multi>::Type is a purely compile-time matter), the get
function can very well be implemented inline by a single return
statement:
template <size_t idx, typename Multi>
inline typename typeAt<idx, Multi>::Type &get(Multi &multi)
{
return static_cast<
UWrap<idx, typename typeAt<idx, Multi>::Type> &>(multi);
}
The intermediate UWrap cast is required to disambiguate between
identical policy types (like two vector<int> types). As UWrap is
uniquely determined by its nr template argument and this is the number
argument that is passed to get ambiguities can easily be prevented.
Multi and its support templates have been developed, how can a
Multi be used?
A word of warning is in place. To reduce the size of the developed classes
they were designed in a minimalist way. For example, the get function
template cannot be used with Multi const objects and there is no default,
or move constructor available for Multi types. Multi was designed to
illustrate some of the possibilities of template meta programming and
hopefully Multi's implementation served that purpose well. But can it be
used? If so, how?
This section provides some annotated examples. They may be concatenated to
define a series of statements that could be placed in a main function's
body, which would result in a working program.
Policy could be defined:
template <typename Type>
struct Policy
{
Type d_type;
Policy(Type &&type)
:
d_type(std::forward<Type>(type))
{}
};
Policy defines a data member and it can be used to define Multi
objects:
Multi<Policy, string> ms(Policy<string>("hello"));
Multi<Policy, string, string> ms2s(Policy<string>("hello"),
Policy<string>("world"));
typedef Multi<Policy, string, int> MPSI;
MPSI mpsi(string("hello"), 4);
Multi class or object
either use the ::size enum value (using the Multi class) or the
.size member (using the Multi object):
cout << "There are " << MPSI::size << " types in MPSI\n"
"There are " << mpsi.size << " types in mpsi\n";
plainTypeAt:
plainTypeAt<0, MPSI>::Type sx = "String type";
plainTypeAt<1, MPSI>::Type ix = 12;
cout << static_cast<Policy<string> &>(mpsi).d_type << '\n' <<
static_cast<Policy<int> &>(mpsi).d_type << '\n';
Policy<Type> types. In that case get still works fine:
typedef Multi<Policy, int, int> MPII;
MPII mpii(4, 18);
cout << get<0>(mpii).d_type << ' ' << get<1>(mpii).d_type << '\n';
std::vector in a Vector:
typedef Multi<Vector, int, double> MVID;
MVID mi({1, 2, 3}, {1.2, 3.4, 5.6, 7.8});
Multi type:
typeAt<0, Multi<Vector, int>>::Type vi = {1, 2, 3};
Vector is a std::vector, the reference
returned by get support index operators that can be used as left hand side
or right hand side operands:
cout << get<0>(mi)[2] << '\n';
get<1>(mi)[3] = get<0>(mi)[0];
cout << get<1>(mi)[3] << '\n';