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.1.0~pre2) 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 which can then be tailored to specific types when required.
However, templates allow us to do more than that. If not for compiler implementation limitations, templates could be used to program, 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 compile-time:
Of course, asking the compiler to compute, e.g., prime numbers, is one thing. 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, which is that we can ask the compiler to compute virtually anything using C++'s template language.
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.
Following that discussion a third type of template parameter, the template template parameter is introduced, 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 resulted 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
is
discussed. It is used to distinguish types defined by class templates from
members defined by class templates;
typename
to situations where
types nested in templates are returned from member functions of class
templates;
::template
and
variants, used to inform the compiler that a name used inside a template is
itself a class template.
typename
has been used until now to indicate a
template type parameter. However, it is also used to disambiguate
code inside templates. Consider the following code:
template <typename Type> Type function(Type t) { Type::Ambiguous *ptr; return t + *ptr; }When this code is shown to the compiler, it will complain with an -at first sight puzzling- error message like:
demo.cc:4: error: 'ptr' was not declared in this scopeThe puzzling nature of this error message is that the intention of the programmer was actually to declare a pointer to a type
Ambiguous
defined
within the class template Type
. However, the compiler, when confronted
with Type::Ambiguous
has to make a decision about the nature of this
construct. Clearly it cannot inspect Type
to find out its true nature,
since Type
is a template type, and hence its actual definition isn't
available yet. The compiler now is confronted with two possibilities: either
Type::Ambiguous
is a static member of the as yet mysterious template
Type
, or it is a subtype defined by Type
. As the standard
specifies that the compiler must assume the former, the statement
Type::Ambiguous *ptr;is eventually interpreted as a multiplication of the static member
Type::Ambiguous
and the (now undeclared) entity ptr
. The reason for
the error message should now be clear: in this context ptr
is unknown.
To disambiguate code in which an identifier refers to a
type that is itself a subtype of a template type parameter the keyword
typename
must be used. Accordingly, the above code is altered into:
template <typename Type> Type function(Type t) { typename Type::Ambiguous *ptr; return t + *ptr; }Classes fairly often define subtypes. When such classes are thought of when designing templates, these subtypes may appear inside the template's definitions as subtypes of template type parameters, requiring the use of the
template
keyword. E.g., assume a class template Handler
defines a
typename Container
as its type parameter, as well as a data member storing
the container's begin()
iterator. Furthermore, the class template
Handler
may offer a constructor accepting any container supporting a
begin()
member. The skeleton of the class Handler
could then be:
template <typename Container> class Handler { Container::const_iterator d_it; public: Handler(Container const &container) : d_it(container.begin()) {} };What were the considerations we had in mind when designing this class?
Container
represents any container supporting
iterators.
begin()
. The
initialization d_it(container.begin())
clearly depends on the
template's type parameter, so it's only checked for basic syntactic
correctness.
const_iterator
, defined in the class Container
.
typename
is required. If
this is omitted, and a Handler
is instantiated because of the following
main()
function one again a peculiar compilation error is generated:
#include "handler.h" #include <vector> using namespace std; int main() { vector<int> vi; Handler<vector<int> > ph(vi); } /* Reported error: handler.h:4: error: syntax error before `;' token */Clearly the line
Container::const_iterator d_it;in the
Handler
class causes a problem: it is interpreted by the
compiler as a static member instead of a subtype. Again, the problem is
solved using typename
:
template <typename Container> class Handler { typename Container::const_iterator d_it; ... };An interesting illustration that the compiler indeed assumes
X::a
to
be a member a
of the class X
is provided by the error message we get
when we try to compile main()
using the following implementation of
Handler
's constructor:
Handler(Container const &container) : d_it(container.begin()) { size_t x = Container::ios_end; } /* Reported error: error: `ios_end' is not a member of type `std::vector<int, std::allocator<int> >' */
As a final illustration consider what happens if the function template
introduced at the beginning of this section doesn't return a Type
value,
but a Type::Ambiguous
value. Again, a subtype of a template type is
referred to, and typename
is required:
template <typename Type> typename Type::Ambiguous function(Type t) { return t.ambiguous(); }Using
typename
in the specification of a return type is further
discussed in section 22.1.2.
In some cases typename
can be avoided by resorting to a
typedef
. E.g., Iterator
, defined using typedef
, can be used to
indicate the specific type:
template <typename Container> class Handler { typedef typename Container::const_iterator Iterator; Iterator d_it; ... };
nested()
returns an object
of the nested class. Note that a (deprecated) in-class member implementation
is used. 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: within the class
Outer
there is
no ambiguity with respect to the meaning of nested()
's return
type.
However, since it is advised to implement inline and template members
below their class interface (see section 7.6.1), we now remove the
implementation from the interface itself, and put it below the
interface. Suddenly the compiler refuses to compile our member nested()
:
template <typename T> class Outer { public: class Nested {}; Nested nested() const; }; template <typename T> Outer<T>::Nested Outer<T>::nested() const { return Nested(); }The above implementation of
nested()
produces an error message like
error: expected constructor, destructor, or type conversion before 'Outer'.
In cases like these the return type (i.e., Outer<T>::Nested
)
refers to a subtype of Outer<T>
rather than to a member of
Outer<T>
.
As a general rule the following holds true: the keyword typename
must
be used whenever a type is referred to that is a subtype of a type that is
itself depending on a template type parameter.
Writing typename
in front of Outer<T>::Nested
removes the
compilation error and therefore the correct implementation of the function
nested()
becomes:
template <typename T> typename Outer<T>::Nested Outer<T>::nested() const { return Nested(); }
#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
At first glance, this error may cause some confusion, since with
non-class templates public and protected base class members are immediately
available. This also holds true for class templates, but only if the compiler
can figure out what we mean. In the above situation, the compiler can't, since
it doesn't know for what type T
the member function member
must be
initialized.
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 processing the class
Derived
, can't be sure
that no specialization will be in effect once an instantiation of Derived
is called for, it can't decide yet for what type to instantiate member
,
since 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
for member()
until instantiation time. This can 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). In the following main()
function both
forms are used. Note that with the int
template type the int
specialization is used.
#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 <> void Base<int>::member() { std::cout << "This is the int-specialization\n"; } template <typename T> class Derived: public Base<T> { public: Derived(); }; template <typename T> Derived<T>::Derived() { this->member(); // Using `this' implies <T> at // instantiation time. Base<T>::member(); // Same. } int main() { Derived<double> d; Derived<int> i; } /* Generated output: This is Base<T>::member() This is Base<T>::member() This is the int-specialization This is the int-specialization */
typename
keyword can often used to that purpose.
However, 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 represents, 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 is used
in multiple ways, which cannot always be determined from the context in which
the compiler encounters <
. Sometimes, however, the compiler will know
that <
does not represent the less than operator, as in the situation
where 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 <
if 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(); }; };Here a class template
Outer
defines a nested class template Inner
,
which 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
could be written as follows:
template <typename T1, typename T2> class Usage { public: void fun(Outer<T1>::Inner<T2> &obj); ... };The compiler, however, won't accept this. It interprets
Outer<T1>::Inner
as a class type, which of course doesn't exist. In this
situation the compiler generates an error like:
error: 'class Outer<T1>::Inner' is not a typeTo inform the compiler that in this case
Inner
itself is a template,
using the template type parameter <T2>
, the
::template
construction is
required. This 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);But this still doesn't get us where we want to be: after all
Inner<T2>
is a type, nested under a class template, depenting on a template type
parameter. Actually, the compiler produces a series of error messages here,
one of them being like:
error: expected type-name before '&' tokenwhich nicely indicates what should be done to get it right: add
typename
:
void fun(typename Outer<T1>::template Inner<T2> &obj);
Next, fun()
itself is not only just declared, it is implemented as
well. The implementation should call Inner
's member nested()
function,
instantiated for yet another type X
. The class template Usage
should
now receive a third template type parameter, which can be called T3
: let's
assume it has been defined. To implement fun()
, we start out with:
void fun(typename Outer<T1>::template Inner<T2> &obj) { obj.nested<T3>(); }However, once again we run into a problem. The compiler once again interprets
<
as `less than', and expects a logical expression, having as
its right-hand side a primary expression instead of a formal template type.
To tell the compiler in situations like these that <T3>
should be
interpreted 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: by writing
.template
the compiler is informed 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 value or reference parameters functions may define pointer
parameters. If obj
would have been defined as a pointer
parameter the implementation would 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
have any linkage: they are pure symbolic values with no memory
representation.
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 abusing the amount of memory that's
actually available to the target type. In this way we can't allow reinterpret
cast from int
to double
since a double
is a larger type than an
int
.
The intent is now to create a new kind of cast, let's call it
reinterpret_to_smaller_cast
, which can only be performed if the target
type is a smaller type than the source type (note that this exactly the
opposite readoning as used by Alexandrescu
(2001),
section 2.1).
The following template is constructed:
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 with a suggestive name, resulting in a compile-time error if the condition is not met. 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: it's a mere value that's computed by the compiler while compiling the expression. To transform this into a useful error message the expression is assigned to a desciptive
enum
value, resulting in, e.g.,
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
ostream
an error is produced by the compiler, like:
error: enumerator value for 'the_Target_size_exceeds_the_Source_size' not integer constantwhereas no error is reported if, e.g.,
reinterpret_to_smaller_cast<int>(cout)
is requested.
In the above example a enum
was used to compute 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 will then contain a final value, computed by the compiler rather than by code as the next sections illustrate. In general, programs shouldn't do run-time what they can do compile-time and computing complex calculations resulting in constant values is a clear example of this principle.
int
values. This is primarily useful in situations where a scalar value
(often a bool
value) is available to select an appropriate member
specialization, a situation that 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 represent a
type. E.g., vector<int>
and vector<double>
are different types.
Templatizing integral values is simply implemented: just define a template, it
does not have to have any contents at all, but it customarily has the integral
values stored as an enum
value:
template <int x> struct IntType { enum { value = x }; };Since
IntType
does not have any members, but just the enum
value,
the `class
' can be defined as a `struct
', saving us from typing
public:
. Defining the enum
value `value
' allows us to retrieve the
value used at the instantiation at no cost in memory: enum values are not
variables or data members, and thus have no address. They are mere values.
Using the struct IntType
is easy: just define an anonymous or named
object by specifying a value for its int
non-type parameter:
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 << endl; }
Since template (member) functions are only instantiated when they are actually used, we can even define specializations of functions which are mutually exclusive. I.e., it is possible to define a specialization which may be compilable in one situation, but not in another, and a second specialization which is compilable in the other situation, but not in the first situation. This way code can be tailored to the demands of a concrete situation.
A feature like this cannot be implemented in code. For example, when designing a generic storage class the software engineer may have the intention to store value class type objects as well as objects of polymorphic class types in the storage class. From this point of departure the engineer may conclude that the storage class should contain pointers to objects, rather than objects themselves, and the following code may be conceived of:
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 will normally fail to compile as value classes
do not define clone()
member functions and polymorphic base classes should
define
their copy constructor in the class's private section. It doesn't matter
to the compiler that clone()
is never called for value classes and the
copy constructor is never called for value type classes: it 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 template
functions are only instantiated when used, we design specializations of
our add()
function, and provide our class Storage
with an additional
(in addition to Type
itself) template
non-type parameter indicating whether we'll use Storage
for polymorphic or
non-polymorphic classes:
template <typename Type, bool isPolymorphic> class Storage ...and we simply define overloaded versions of our
add()
member: one
implementing the polymorphic class variant expecting true
as its argument,
and one implementing the value class variant accepting false
as its
argument.
Again we run into a problem: overloading members cannot be based on
argument values, only on types. Fortunately there is a way out: in section
22.2.1.1 it was discussed how to convert integral values to types, and
knowledge of how to do this now comes in handy. The strategy is to define two
overloaded versions: one defining an IntType<true>
parameter, implementing
the polymorphic class and one defining an IntType<false>
parameter,
implementing the polymorphic class. In addition to these overloaded versions
of the member function add()
the member add()
itself calls the
appropriate overloaded member by providing an IntType
argument,
constructed from Storage
's template non-type parameter. Here are the
implementations:
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>()); }
In the above example making a selection was made possible by converting a
primitive value to a type and then (since each concrete primitive value may be
used to construct a different type) using these types to define overloaded
versions of template member functions one of which is then called from a
(public) member using IntType
to construct the appropriate selector type.
Since template members are only instantiated when used, only one of the
overloaded private add()
members is instantiated. Since the other one is
never called compilation errors are prevented.
Some software engineers may have second thoughts when thinking about the
Storage
class using pointers to store copies of value classes. Their
argument could be that value class objects can be stored by value,
rather than by pointer. In those cases we'd like to define the actual type
used for storing the values as either value types or pointer types. Situations
like these 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 general form of the template:
template<bool selector, typename FirstType, typename SecondType> struct IfElse { typedef FirstType TypeToUse; };Then define a specialization for the case where the selector value is
false
. Note that the specialized struct
has three arguments, matching
the template parameters of the above general form. The template<...>
specification used with specializations merely define what remaining template
parameters are present in the specialization:
template<typename FirstType, typename SecondType> struct IfElse<false, FirstType, SecondType> { typedef SecondType TypeToUse; };The
IfElse
struct uses in its second definition a partial
specialization to select the FalseType
if the selector
is
false
. In all other cases (i.e., selector == true
) the less specific
generic case is instantiated by the compiler, defining FirstType
as the
TypeToUse
.
The IfElse
struct allows us to
templatize structural types: our
Storage
class may use pointers to store copies of polymorphic class
type objects, but values to store value class type objects.
template <typename Type, bool isPolymorphic> class Storage { typedef typename IfElse<isPolymorphic, Type *, Type>::TypeToUse 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 TypeToUse
, which is a type defined
by IfElse
as either FirstType
or SecondType
to define the actual
data type to be used for Storage
's std::vector
data type. To prevent
long data type definitions Storage
defines its own type DataType
.
The remarkable result in this example is that the structure of the
Storage
class's data is now depending on its template parameters. Since
the isPolymorphic == true
situation uses different data types than
t(isPolymorphic == false) situation, the overloaded private add()
members
can utilize this difference immediately. E.g., add(Type const &obj,
IntType<false>)
now uses direct copy construction to stpre a copy of obj
in d_vector
.
It is also possible to select a type from more than two alternatives. In
that case, IfElse
structs can be nested. Remember that these structs never
have any effect on the run-time program, which simply is confronted with the
appropriate type, conditional to the type that's associated with the selector
value. The following example, defining MapType
as a map having plain types
or pointers for either its key or its value type, 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 *> >::TypeToUse >::TypeToUse >::TypeToUse 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 different data types are to be used in class templates, depending on template non-type parameters, an
IfElse
struct can be used to define the appropriate type, and overloaded
member functions may utilize knowledge about the appropriate types to optimize
their implementations.
Note that the overloaded functions have identical parameter lists as the
matching public wrapper function, but add to this parameterlist a specific
IntType
type, allowing the compiler to select the appropriate overloaded
version, based on the template's non-type selector parameter.
The principle to follow here is:
enum
values.
Most readers will be familiar with the recursive implementation of the
mathematical `factorial' operator, indicated 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 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 a int n
template non-type parameter, and defining a specialization
for the case n == 0
. The generic implementation uses recursion according
to the factorial definition. Furthermore, the Factorial
template defines an
enum
value `value
' to contain the 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,
compiler determinable values: n is provided, and Factorial<n - 1>()
is
computed by template meta programming, also resulting in a compiler
determinable value. Also note the interpretation of Factorial<n - 1>::value
:
it is the value
defined by the type Factorial<n - 1>
; it's not,
e.g., the value returned by an object of that type. There are no objects
here, simply values defined by types.
To end the resrsion a specialization is required, which will be preferred by the compiler over the generic implementation when its template arguments are present. The specialization can be provided for the value 0:
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 run-time, but
compile-time. The above statement is therefore run-time equivalent to:
int main() { cout << "The number of permutations of 5 objects = " << 120 << "\n"; }
Storage
which is able to store data, which may either make
and store copies of the data or store the data as received, and which may
either use a vector or a linked list as its underlying storage medium. How
should the engineer tackle this request?
The engineer's first reaction could be to develop Storage
as a class
having two data members, one being a list, another being a vector, and to
provide the constructor with maybe an enum value indicating whether the data
itself or new copies should be stored using that enum value to set a series
of pointers to member functions to activate the appropriate subset of its
private member functions, providing public wrapper functions to hide the use
of the pointers to members.
Complex, but doable, until the engineer is confronted with a modification of
the original question: now the request states that it should also be possible
to use -in the case of new copies- a custom-made allocation scheme, rather
than the standard new
operator, and 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 queue
could be preferred or maybe even
a stack
.
It's clear that the approach suggesting to have all functionality provided by
the 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, crashing or terminating 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, leaving users rather than the engineer flabbergasted.
The question remains: how should the engineer proceed, when confronted with the above questions? It's time to introduce policies.
In the previous section a problem of how to create a class which might use 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: the 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 also be designed as a class template. The class will
also need a template type parameter allowing users to specify the data type.
It would be possible to specify the data type with the specification of the allocation scheme, resulting in code like:
template <typename Data, typename Scheme> class Storage ...and then use
Storage
, e.g., as follows:
Storage<string, NewAlloc<string> > storage;However, this implementation is unnecessarily complex, as it requires the user to specify the data type twice. Instead, the allocation scheme should be specified using a third type of template parameter, not requiring the user to specify the data type with the allocation scheme to use. This third type of template parameter (in addition to the well-known template type parameter and template non-type parameter) is the template template parameter.
Consider the class Storage
, introduced at the beginning of this section,
and consider the allocation classes discussed in the previous section. To
specify an allocation policy the class Storage
starts its
definition as follows:
template <typename Data, template <typename> class Policy> class Storage ...The second template parameter is the template template parameter. It contains the following elements:
template
starts the definition of a template
template parameter;
class
is
provided. In this case, typename
cannot 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
. Moreover, the policy should operate on the data
type to be used with the class Storage
. Therefore the policy is handed
that data type as well. From this we obtain 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
.
Now the allocation classes do not really offer many useful members: apart
from the extraction operator, no immediate access to the data is offered. This
can easily be repaired by providing some additional members. E.g., the class
NewAlloc
could be extended 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 provided with comparable members.
The next step is to use the allocation schemes in some real code. The
following example shows how a storage can be constructed for a data type to be
specified and an allocation scheme to be specified. First, define a class
Storage
:
template <typename Data, template <typename> class Policy> class Storage: public std::vector<Policy<Data> > { };That's all there is. 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 of 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] << endl; storage[1] = "hello"; copy(storage.begin(), storage.end(), ostream_iterator<NewAlloc<std::string> >(cout, "\n"));Following the construction of a
Storage
object, 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
, again using the STL copy()
algorithm.
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, say a list
or a deque
. To
implement this, the storage class is parameterized as well, again using a
template template parameter, that could be given a
default value too, as
shown in the following redefinition of Storage
:
template <typename Data, template <typename> class Policy, template <typename> class Container = std::vector> class Storage: public Container< Policy<Data> > { };The earlier example in which a
Storage
object was used can be used
again, without any modifications, 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: a pointer to the vtable;
To avoid these drawbacks, it is good practice to prevent using references or pointers to policy classes to refer or point to derived class objects. This is accomplished by providing policy classes with nonvirtual protected destructors. Since the destructor is non-virtual there is no implementation penalty in reduced efficiency or memory overhead, and since it is protected users cannot refer to classes derived from the policy class using a pointer or reference to the policy class.
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 situation 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(); } };Next, a specialization can be defined to accomodate the specifics of 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 use the above templates for real, a generic wrapper class can now be
constructed: depending on the actual storage type that is used (e.g., a
std::vector
or some plain storage class) it will use the matching Size
template 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 the software engineer to apply the proper code to the proper
data type, be it a pointer, a reference, or a plain value, all maybe in
combination with const
. Moreover, the specification of the particular type
of data to be used does not have to be made by the template writer, but can be
inferred from the actual type that is specified (or implied) when the template
is used.
Trait classes allow the software engineer 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 actual 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., a 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 generic point of departure when defining a trait template is a plain
vanilla struct
, defining the characteristics of a plain value type, e.g.,
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, or a reference type (all of which may or may not be const
types).
Moreover, whatever the actual type that's provided, 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., a 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. E.g., something like:
template <typename T> struct Basic { typedef T Type; enum { isValue = true, isPointer = false, isConst = false }; };
However, often decisions about types can be made using constant logical
expressions. Note that the above definition does not contain a
`isReference
' enumeration value. Such a value is not required as it is
implied by the expression not isPointer and not isValue
.
Although some conclusions can be drawn by combining various enum
values, 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 { isValue = false, isPointer = true, isConst = 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 { isValue = false, isPointer = true, isConst = true }; };
Several other specializations should be defined: e.g.,
recognizing const
value types or reference types. Eventually all these
specializations wind up being nested struct
s 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: typedef typename Basic<TypeParam>::Type ValueType; typedef ValueType *PtrType; typedef ValueType &RefType; enum { isValueType = Basic<TypeParam>::isValue, isPointerType = Basic<TypeParam>::isPointer, isReferenceType = not Basic<TypeParam>::isPointer and not Basic<TypeParam>::isValue, isConst = Basic<TypeParam>::isConst }; };
A trait class template can be used to obtain the proper type, irrespective
of the template type argument provided, or it can be used to select
the proper specialization, depending on, e.g., the const
-ness of a
template type. The following statements serve as an illustration:
cout << BasicTraits<int>::isPointerType << " " << BasicTraits<int *>::isPointerType << " " << BasicTraits<int>::isConst << " " << BasicTraits<int>::isReferenceType << " " << BasicTraits<int &>::isReferenceType << " " << BasicTraits<int const>::isConst << " " << BasicTraits<int const *>::isPointerType << " " << BasicTraits<int const *>::isConst << " " << endl; BasicTraits<int *>::ValueType value = 12; int *otherValue = &value; cout << *otherValue << endl;
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. E.g, the class template developer might define a specialization for a member knowing the template's type parameter is 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 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, which is available only for classes. Using the pointer to member construct as the distinguishing characteristic, we now look for a construction which uses the pointer to member if available, and does something else if the pointer to member construction is not available.
Note again the rule of thumb that works so well for template meta programming: define a generic situation, and then specialize for the situations you're interested in. It's not a trivial task to apply this rule of thumb here: 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 can be provided with a parameter which is a pointer to a member function (defining the `specialization' case), and another function template can be defined so that it accepts any argument. The compiler will then select the latter function in all situations but those in which the provided type is actually a class type, and thus a type which may support a pointer to a member.
Note that the compiler will not call the functions: we're talking
compile-time here, and all the compiler does is to select the appropriate
function, in order to be able to evaluate a constant expression (defining the
value of, e.g, the enum value
isClass
).
So, our function template will be something like:
template <typename ClassType> static (returntype) fun(void (ClassType::*)());Note that in this function `
(returntype)
' is not yet specified. It
will be shortly.
The question about what the return type should be will be answered
shortly. Arbitrarily the function's parameter defines a pointer to a member
returning void
. Note that there's no need for such a function to exist
for the concrete class-type that's specified with the traits class, since all
the compiler will do is select this function if a class-type was provided
to the trait class in which fun()
will be nested. In line with this:
fun()
is only declared, not defined. Furthermore note that fun()
is
declared as a static member of the trait class, so that there's no need
for an actual object when fun()
is called.
So far for the class-types. What about the non-class types? For those
types a (generic) alternative must be found, one the compiler will select when
the actual type is not a class type. Again, the language offers a `worst case'
solution in the
ellipsis parameter list. The ellipsis is a final resort the compiler
may turn to if everything else fails. It's not only used to define (the in
C++ deprecated) functions having a variable number of arguments, but
it's also used to define the catch-all exception catch
clause. Therefore,
the `generic case' can be defined as follows:
template <typename NonClassType> static (returntype) fun(...);Note that 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
when possible, given the nature of the used argument.
The question now becomes: what argument can be used for both a pointer to
a member and the generic situation? Actually, there is such a `one size fits
all' argument: 0. The value 0 can be used as argument value to initialize not
only primitive types, but also to initialize pointers and pointers to members.
Therefore, fun
will be called as fun<Type>(0)
, with Type
being the
template type parameter of the trait class. Here, Type
must be specified
since the compiler will not be able to determine fun
's template type
parameter when fun(...)
is selected.
Now for the return type: the return type cannot be a simple value (like
true
or false
). When using a simple value the isClass
enum value
cannot be defined, since
enum { isClass = fun<Type>(0) } ;needs to be evaluated to obtain
fun
's return value, which is clearly
not possible as enum values must be determined compile-time.
To allow a compile-time definition of isClass
's value the solution
must be sought in an expression that discriminates between fun<Type>(...)
and fun<Type>(void (Type::*)())
. In situations like these
sizeof
is
our tool of choice. The sizeof
operator is evaluated compile-time, and so
by defining return types that differ in their sizes it is possible to
discriminate compile-time among the two fun()
alternatives.
The char
type is by definition a type having size 1. By defining
another type containing two consecutive char
values a bigger
type is obtained. Now char [2]
is not a type, but char[2]
can be
defined as a data member of a struct
which will thus have a size exceeding
1. E.g.,
struct Char2 { char data[2]; };
Char2
can be defined as a nested type within our traits class, and the
two fun
declarations become:
template <typename ClassType> static Char2 fun(void (ClassType::*)()); template <typename NonClassType> static char fun(...);This, in turn enables us to specify an expression that can be evaluated compile time, allowing the compiler to determine
isClass
's value:
enum { isClass = sizeof(fun<Type>(0)) == sizeof(Char2) };
Note, however, that no fun()
function template ever makes it to
the instantiation stage, but the compiler nonetheless is able to infer
what fun
's return type will be, given a concrete template type
argument. This inference is then used by the compiler to determine the truth
of an expression, in turn enabling the compiler to compute the required
compile-time constant value isClass
, allowing us to determine whether a
certain type is or is not a class type. Marvelous!
type_traits
allowing template meta programs to identify and modifiy
characteristics of types.
All facilities offered in 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
Derived
is_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
specified implementation should not be used. Rather, with std::string
a
second argument 1
should always be provided (e.g., if Argument
is a
C++ string, a std::string
is returned holding a copy of the function's
argument, except for the argument's first character, which is chopped off).
Since chop()
is a function, it is not possible to use a
partial specialization. So it is not possible to specialize for
std::string
as attempted in the following erroneous implementation:
template <typename Argument> std::string chop<std::string, Argument>(Argument const &arg) { return string(arg, 1); }it is possible to use overloading, though. Instead of using partial specializations overloaded function templates could be designed:
template <typename Return, typename Argument> Return chop(Argument const &arg, Argument ) { return Return(arg); } template <typename Argument> std::string chop(Argument const &arg, std::string ) { return string(arg, 1); }This way it is possible to distinguish the two cases, but at the expense of a more complex function call (e.g., maybe requiring the use of the
bind2nd()
binder (cf. section 18.1.4)
to bind the second argument to a fixed value) as well as the need to
provide a (possibly expensive to construct) dummy argument to allow the
compiler to choose among the two overloaded function templates.
Alternatively, overloaded versions 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: they contain no data at all. But there's also an obvious
disadvantage: there is no intuitively clear association between on the
one hand the int
value used and on the other hand the intended type.
In situations like these it is more attractive to use another lightweight
solution. Instead of using an arbitrary int-to-type association, an
intuitively clear and automatic type-to-type association is used. The
struct TypeType
is a lightweight type wrapper, much like IntType
is a
lightweight wrapper around an int
. Here is its definition:
template <typename T> struct TypeType { typedef T Type; };This too is 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 correct overloaded
version is automatically selected. E.g.,
template <typename Result> Result chopper(char const *txt) { return chop(std::string(txt), TypeType<Result>()); }Using
chopper()
, the following statement will produce the text
`ello world
':
cout << chopper<string>("hello world") << endl;
struct
is a useful little tool. It can be used as a type acting
analogously to the
ASCII-Z
(final 0-byte) in C-strings or as a
0-pointer to indicate the end of a linked list. Its definition is simply:
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: T
s can be used instead of U
s if a T
is accepted as
argument in cases where U
s 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, however, is that no code is actually generated or executed:
all decisions can be made by the compiler.
In the second part of this section it will be 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 code developed here closely follows the example
provided by Alexandrescu
(2001, p. 35).
First, a function is conceived of that will accept a type U
to which
an alternative type T
will be compared. This function returns a value of
the as yet unsepcified type Convertible
:
Convertible test(U const &);
The function test()
is not implemented; it is merely declared. The idea is
that if a type T
can be used instead of a type U
it can be passed as
argument to the above test()
function.
If T
cannot be used where a U
is expected, then the above function
will not be used by the compiler. Of course, getting a compiler error is not
the kind of `answer' we're looking for and so the next question is what
alternative we can offer to the compiler in cases where a T
cannot be used
as argument to the above function.
C (and C++) offer a very general parameter list, a parameter list that
will always be considered acceptable. This parameter list consists of the
ellipsis
which actually is the worst situation the compiler may encounter: if
everything else fails, then the function defining an ellipsis as its parameter
list is selected. Normally that's not a productive and
type-safe
alternative, but in the current situation it is exactly what is
needed. When confronted with two alternative function calls, one of which
defines the ellipsis parameter list, the compiler will only select the
function defining the ellipsis if the alternative(s) can't be used. So we
declare an alternative function test()
, having the ellipsis as its
parameter list, and returning another type, e.g., NotConvertible
:
NotConvertible test(...);
Now, if code passes a value of type T
to the test
function, the return
type will be Convertible
if T
can be converted to U
, and
NotConvertible
if conversion is not possible.
Two problems still need to be solved: how do we obtain a T
argument? The
problems here are firstly, that it might not be possible to define a T
, as
a type T
might hide all its constructors and secondly, how can the two
return values be distinguished?
Although it might be impossoble to construct a T
object, it is fortunately
not necessary to construct a T
. After all, the intent is to decide
compile time
whether a type is convertible to another type and not actually to
construct such a T
value. So another function is declared:
T makeT();This mysterious function has the magical power of enticing the compiler into thinking that a
T
object will come out of it. So, what will happen
when the compiler is shown the following code:
test(makeT())
If T
can be converted to U
the first function Convertible test(U
const &)
will be selected by the compiler; otherwise the function NotConvertible
test(...)
will be selected.
If it is now possible to distinguish Convertible
from
NotConvertible
compile-time then it is possible to determine whether T
is convertible to U
.
Since Convertible
and NotConvertible
are values, their sizes are
known. If these sizes differ, then the sizeof
operator can be used to
distinguish the two types; hence it is possible to determine which test()
function was selected and hence it is known whether T
can be converted to
U
or not. E.g., if the following expression evaluates as true
T
is
convertible to U
:
sizeof(test(makeT())) == sizeof(Convertible);
The size of a char
is well known. By definition it is 1. Using a
typedef
Convertible
can be defined as a synonym of char
, thus
having size 1. Now NotConvertible
must be defined so that it has a
different type. E.g.,
struct NotConvertible { char array[2]; };Note there that a simple
typedef char NotConvertible[2]
does not work:
functions cannot return arrays, but they can return arrays embedded in a
structs.
The above can be wrapped up in a template class, having two template type parameters:
template <typename T, typename U> class Conversion { typedef char Convertible; struct NotConvertible { char array[2]; }; static T makeT(); static Convertible test(U const &); static NotConvertible test(...); public: enum { exists = sizeof(test(makeT())) == sizeof(Convertible) }; enum { sameType = 0 }; }; template <typename T> class Conversion<T, T> { public: enum { exists = 1 }; enum { sameType = 1 }; };
The above class never results in any
run-time execution of
code. When used, it merely defines the values 1 or 0 for its exist
enum
value, depending whether the conversion exists or not. The following example
writes 1 0 1 0
when run from a main()
function:
cout << Conversion<ofstream, ostream>::exists << " " << Conversion<ostream, ofstream>::exists << " " << Conversion<int, double>::exists << " " << Conversion<int, string>::exists << " " << endl;
Conversion
has be defined it's easy to determine whether a type
Base
is a (public) base class of a type Derived
. To determine
inheritance convertability of (const) pointers is examined. Derived const
*
can be converted to Base const *
if:
Base
is a public and unambiguous base class of Derived
;
Base
is void.
Conversion<Derived const *, Base const *>::exists
:
#define BASE_1st_DERIVED_2nd(Base, Derived) \ (Conversion<Derived const *, Base const *>::exists && \ not Conversion<Base const *, void const *>::sameType)
If code should not consider a class to be its own base class, then the following stricted test is possible, which adds a test for type-equality:
#define BASE_1st_DERIVED_2nd_STRICT(Base, Derived) \ (BASE_1st_DERIVED_2nd(Base, Derived) && \ not Conversion<Base const *, Derived const *>::sameType)
The following example writes 1: 0, 2: 1, 3: 0, 4: 1, 5: 0
when run from a
main()
function:
cout << "\n" << "1: " << BASE_1st_DERIVED_2nd(ofstream, ostream) << ", " << "2: " << BASE_1st_DERIVED_2nd(ostream, ofstream) << ", " << "3: " << BASE_1st_DERIVED_2nd(void, ofstream) << ", " << "4: " << BASE_1st_DERIVED_2nd(ostream, ostream) << ", " << "5: " << BASE_1st_DERIVED_2nd_STRICT(ostream, ostream) << " " << endl;
This section itself was inspired by Andrei Alexandrescu's (2001) book Modern C++ design, and much of this section's structure borrows from Andrei's coverage of typelists.
A typelist is a very simple struct
: like a std::pair
it consists
of two elements, although in this case the typelist does not contain data
members, but type definitions. It is defined as follows:
template <typename First, typename Second> struct TypeList { typedef First Head; typedef Second Tail; };
The typelist allows us to store any number of types using a recursive
definition. E.g., the three types char, short, int
may be stored as
follows:
TypeList<char, Typelist<short, int> >Although this is a possible representation, usually
NullType
(cf. section 22.5.2) is used as the final type, acting comparably to a
0-pointer. Using NullType
the above three types are represented as
follows:
TypeList<char, TypeList<short, TypeList<int, NullType> > >This way to represent lists of types may be accepted by the compiler, but usually not as easily by programmers, who frequently have a hard time putting in the right number of parentheses. Alexandrescu suggest to ease the burden by defining a series of macros, even though macros are generally deprecated in C++. The
TYPELIST
macros suggested by Alexandrescu allow us to
define typelists for varying numbers of types, and they are easily expanded if
accommodating larger numbers of types is necessary. Here are the definitions
of the first five TYPELIST
macros:
#define TYPELIST_1(T1) TypeList<T1, NullType > #define TYPELIST_2(T1, T2) TypeList<T1, TYPELIST_1(T2) > #define TYPELIST_3(T1, T2, T3) TypeList<T1, TYPELIST_2(T2, T3) > #define TYPELIST_4(T1, T2, T3, T4) TypeList<T1, TYPELIST_3(T2, T3, T4) > #define TYPELIST_5(T1, T2, T3, T4, T5) TypeList<T1, TYPELIST_4(T2, T3, T4, T5) >
Note the recursive nature of these macro definitions: recursion is how template meta programs perform iteration and in the upcoming sections implementations heavily depend on recursion. With all solutions to the problems covered by the coming sections a verbal discussion is provided explaining the philosophies that underlie the recursive implementations.
Of course, even here macros are ugly. The
macro
processor will be confused if a type is somewhat complex, like
Wrap<HEAD, idx>
. Fortunately situations like these can be prevented using
a simple typedef
. E.g., typenef Wrap<HEAD, idx> HEADWRAP
and then
using HEADWRAP
instead of the full type definition.
size_t c_length(char const *cp) { return *cp == 0 ? 0 : 1 + c_length(cp + 1); }In the context of template meta programming the alternatives that are used to execute or terminate recursion are never written in one implementation, but instead specializations are used: each specialization implements an alternative.
The length of a typelist will be determined by a struct ListSize
ordinarily expecting a typelist as its template type parameter. It's merely
declared, since it turns out that only its specializations are required:
template <typename TypeList> struct ListSize;
Following the above algorithm specializations are now constructed:
ListSize
's type is empty (i.e., a
NullType
), its size
is 0:
template <> struct ListSize<NullType> { enum { size = 0 }; };
TypeList
:
template <typename Head, typename Tail> struct ListSize<TypeList<Head, Tail> > { enum { size = 1 + ListSize<Tail>::size }; };
std::cout << ListSize<TYPELIST_3(int, char, bool)>::size << "\n";
NullType
, define `index' as -1;
The implementation again sets out with the declaration of a struct:
ListSearch
expects two template type parameters: searchtype's type and a
typelist:
template <typename SearchType, typename TypeList> struct ListSearch;
Next, specializations are defined implementing the above alternatives:
template <typename SearchType> struct ListSearch<SearchType, NullType> { enum { index = -1 }; };
SearchType
twice:
template <typename SearchType, typename Tail> struct ListSearch<SearchType, TypeList<SearchType, Tail> > { enum { index = 0 }; };
tmp
to store the index value obtained
from searching the typelist's tail for searchtype:
template <typename SearchType, typename Head, typename Tail> class ListSearch<SearchType, TypeList<Head, Tail> > { enum { tmp = ListSearch<SearchType, Tail>::index } ; public: enum { index = tmp == -1 ? -1 : 1 + tmp }; };
Assuming all required headers have been included, the following example
shows how ListSearch
can be used:
int main() { std::cout << ListSearch<char, TYPELIST_2(int, char)>::index << "\n"; }
Rather than defining an enum
value, the current algorithm should define a
type equal to the type at a given index position. If the type does not exist,
the typedef can be made a synonym of NullType
since NullType
cannot
appear in a typelist.
The following algorithm is used (the implementation of the parts is provided immediately following the descriptions of the algorithm's steps):
TypeAt
, expecting an index and a typelist:
template <int index, typename Typelist> struct TypeAt;
NullType
define the return type as
NullType
as well:
template <int index> struct TypeAt<index, NullType> { typedef NullType Type; };
template <typename Head, typename Tail> struct TypeAt<0, TypeList<Head, Tail> > { typedef Head Type; };
typename
following
typedef
: it is required as the defining type's result
type is a nested
type:
template <int index, typename Head, typename Tail> struct TypeAt<index, TypeList<Head, Tail> > { typedef typename TypeAt<index - 1, Tail>::Type Type; };
ListSearch
can be used:
int main() { typedef TYPELIST_3(int, char, bool) list3; enum { test = 2 }; std::cout << (ListSearch<TypeAt<test, list3>::result, list3>::index == -1 ? "Illegal Index\n" : "Index represents a valid type\n"); }
To append a new type to a typelist, the following algorithm can be used:
template <typename TypeList, typename NewType> struct Append;
NullType
:
NullType
, and the original type is
NullType
, the result itself is the NullType
:
template <> struct Append<NullType, NullType> { typedef NullType TList; };
Note that the simple alternative:
template <typename TypeList> struct Append<TypeList, NullType> { typedef TypeList Result; };is not a good idea, as it will match all types that are offered as the template's first template type parameter. E.g.,
Append<int, NullType>
would be accepted, but would certainly not result in a TypeList
.
NullType
to an existing
TypeList
, leave the TypeList
as-is:
template <typename Head, typename Tail> struct Append<TypeList<Head, Tail>, NullType> { typedef TypeList<Head, Tail> TList; };
NullType
:
NullType
, the final typelist
consists of the typelist containing the new type:
template <typename NewType> struct Append<NullType, NewType> { typedef TYPELIST_1(NewType) TList; };
template <typename Head, typename Tail, typename NewType> struct Append<TypeList<Head, Tail>, NewType> { typedef TypeList<Head, typename Append<Tail, NewType>::TList> TList; };
Once again: note that typename
is required in front of
Append
(cf. section 22.1.1)
template <typename TypeList, typename EraseType> struct Erase;
NullType
, there's nothing to erase, and
NullType
is the result:
template <typename EraseType> struct Erase<NullType, EraseType> { typedef NullType Result; };
template <typename EraseType, typename Tail> struct Erase<TypeList<EraseType, Tail>, EraseType> { typedef Tail Result; };
template <typename Head, typename Tail, typename EraseType> struct Erase<TypeList<Head, Tail>, EraseType> { typedef TypeList<Head, typename Erase<Tail, EraseType>::Result> Result; }; // //ERASEALL template <typename TypeList, typename EraseType> struct EraseAll: public Erase<TypeList, EraseType> {};
But there's more: what if the intention is to erase all elements from the
typelist? In that case also apply Erase
to the tail when the type to erase
matches the typelist's head. E.g., a struct EraseAll
can be defined
similarly to Erase
, except for the case where the typelist's head matches
the type to be removed: in that case EraseAll
must also be applied to the
typelist's tail, as there may be additional types to be removed in the tail as
well.
Since EraseAll
closely resembles Erase
, let's see how we can use
class derivation in combination with specializations to our benefit.
Erase
but one can be
used unaltered by EraseAll
. So, EraseAll
inherits from Erase
,
using the generic struct's template parameters:
template <typename TypeList, typename EraseType> struct EraseAll: public Erase<TypeList, EraseType> {};
Thus, EraseAll
is a mere copy of Erase
, and it could be used as a
synonym of Erase
(of course, erasing only the first element).
EraseAll
for
the case where the type to be removed equals the typelist's head:
template <typename EraseType, typename Tail> struct EraseAll<TypeList<EraseType, Tail>, EraseType> { typedef typename EraseAll<Tail, EraseType>::Result Result; };
Note the EraseAll
action on the template's tail.
EraseAll
definitions are all it takes to create a
template that will do the job of erasing all occurrences of a type from a
typelist, borrowing most of its code from the already existing Erase
template. The effect of EraseAll
vs. Erase
can be seen when defining
either Erase
or EraseAll
in the following example:
#include <iostream> #include "erase.h" #include "listsize.h" // change Erase to EraseAll to erase all `int' types below #define ERASE Erase int main() { std::cout << ListSize< ERASE<TYPELIST_3(int, double, int), int>::Result >::size << "\n"; }
EraseDuplicates
template:
EraseDuplicates
struct is declared. It expects
as its single template type a TypeList
:
template <typename TypeList> struct EraseDuplicates;
NullType
, we're done. The result
will remain to be a NullType
:
template <> struct EraseDuplicates<NullType> { typedef NullType Result; };
EraseDuplicates
specialization is defined for a true
Typelist
:
template <typename Head, typename Tail> struct EraseDuplicates<TypeList<Head, Tail> > ...This specialization implements the following reasoning:
EraseDuplicates
erases duplicates, then no duplicates will
be encountered in the typelist's tail if EraseDuplicates
is recursively
processes the template's tail. When this recursive call is completed, a
typelist is obtained (called, e.g., UniqueTail
) not containing any
type duplicates in the typelist's tail:
typedef typename EraseDuplicates<Tail>::Result UniqueTail;
UniqueTail
, producing the
typelist NewTail
:
typedef typename Erase<UniqueTail, Head>::Result NewTail;
NewTail
:
typedef TypeList<Head, NewTail> Result;
EraseDuplicates
, not exposing
UniequeTail
and NewTail
for cosmetic reasons:
template <typename TypeList> struct EraseDuplicates; template <> struct EraseDuplicates<NullType> { typedef NullType Result; }; template <typename Head, typename Tail> class EraseDuplicates<TypeList<Head, Tail> > { typedef typename EraseDuplicates<Tail>::Result UniqueTail; typedef typename Erase<UniqueTail, Head>::Result NewTail; public: typedef TypeList<Head, NewTail> Result; };
Fortunately, there's more to typelist than a mere intellectual challenge. In this section of the chapter on Advanced Template Applications the following topics will be covered:
Tuples
, defining struct
s from typelists, having
index-accessible data members for each of the types specified in a
typelist.
Again, much (and more) of the materials covered below is found in Alexandrescu's (2001) book. Hopefully the current section is an tasty appetizer for the main courses offered by Andrei Alexandrescu.
GenScat
will be developed. The
purpose of GenScat
is to create a new class using on the one hand a
basic building block of the class that's finally constructed and on the other
hand a series of types that will be fed to the building block.
The building block itself is provided as a template template parameter, and the final class will inherit from all building blocks instantiated for each of the types specified in a provided typelist. However, there is a flaw in this plan.
If the typelist contains two types, say int
and double
and the
building block class is std::vector
, then the final GenScat
class will
inherit from vector<int>
and vector<double>
. There's nothing wrong
with that. But what if the typelist contains two int
type specifications?
In that case the GenScat
class will inherit from two vector<int>
classes, and, e.g., vector<int>::size()
will cause an ambiguity which is
hard to solve. Alexandrescu (2001)
in this regard writes (p.67):
There is one major source of annoyance...: you cannot use it when you have duplicate types in your typelist.It is true that the same base class is inherited multiple times when the typelist contains duplicate types, but there is a way around this problem. If instead of inheriting from the plain base classes these base classes would themselves be wrapped in unique classes, then these unique classes can be used to access the base classes by implication: since they are mere wrappers they inherit the functionality of the `true' base classes.
.... There is no easy way to solve the ambiguity, [as the eventually derived class/FBB] ends up inheriting [the same base class/FBB] twice.
Thus, the problem is shifted from type duplication to finding unique
wrappers. Of course, that problem has been solved in principle in section
22.2.1.1, where wrappers around plain int
values were
introduced. A comparable wrapper can be designed in the context of class
derivation. E.g.,
template <typename Base, int idx> struct Wrap: public Base { Wrap(Base const &base) : Base(base) {} Wrap() {} };
Using Wrap
two vector<int>
classes can be distinguished easily:
Wrap<1, vector<int> >
could be used to refer to one of the vectors,
Wrap<2, vector<int> >
could refer to the other vector. By ensuring that
the index values never collide all wrapper types will be unique.
Uniqueness of the Wrap
values is implemented by the GenScat
class: it
is itself a wrapper around the class GenScatter
, that will do all the
work. GenScat
merely seeds GenScatter
with an initial value:
template <typename Type, template <typename> class TemplateClass> class GenScat: public GenScatter<Type, TemplateClass, 0> {};
GenScatter
. In its intended form (which actually turns out to be a
specialization) GenScatter
takes a typelist, a template class and a index
value that is used to ensure uniqueness of the Wrap
types.
With these ingredients, GenScatter
creates a (fairly complex) class, that
itself is normally derived from several GenScatter
base classes. Each
GenScatter
class is eventually derived from a base class which is a
Wrap
class around the template class instantiated for each of the types in
the provided typelist.
This complex arrangement itself causes yet another problem: it would be nice
if the top-level derived class could be initialized by base class
initializers. However, with normal class derivation indirect base classes
cannot be initialized using base class initializers. This is a severe problem:
if indirect base classes cannot be initialized by a GenScat
class or by a
class derived from GenScat
, the types in the provided typelist cannot be
refence types, as references must be initialized at
construction time.
However, an indirect base class can be initialized by a top level derived class if it is a virtual base class (cf. section 14.4.2). The consequence of a virtual base class is that any duplicates of a virtual base class, following different paths in a class hierarchy will be merged into one class.
In the GenScat
hierarchy the Wrap
template class wrappers are the
final (usable) base classes of the hierarchy. By defining these Wrap
base
classes as virtual base classes they can be initialized by GenScat
(or by a class that itself is derived from GenScat
), while merging of
the Wrap
base classes is prevented due to the fact that all Wrap
base
classes are unique.
It's time for some code. The GenScatter
class performs the
following tasks:
Wrap
template
classes at its final nodes;
Wrap
base class types in the
order in which they were constructed, to allow clients to obtain the Wrap
template class wrapper matching a specific type in the provided typelist.
GenScatter
class
hierarchy and its subclasses is provided in figure 20.
The core definition of GenScatter
expects a typelist, a template class
and an index:
template < typename Head, typename Tail, template <typename> class TemplateClass, int idx > class GenScatter<TypeList<Head, Tail>, TemplateClass, idx> : virtual public Wrap<TemplateClass<Head>, idx>, public GenScatter<Tail, TemplateClass, idx + 1> { typedef typename GenScatter<Tail, TemplateClass, idx + 1>::WrapList BaseWrapList; public: typedef TypeList<Wrap<TemplateClass<Head>, idx>, BaseWrapList> WrapList; };
TemplateClass
type, which itself is wrapped in a Wrap
template class wrapper using the unique index value that was passed to
GenScatter
.
Wrap
template class wrapper is then defined as a virtual
base class.
Wrap
class will
receive the next index value.
WrapList
type is defined
as the typelist consisting of the current Wrap
wrapper as its head, and
the base GenScatter
class's WrapList
as its tail.
GenScatter
's main template definition expects a simple type as its
first template parameter. Since this is a plain type, the class can
immediately define a virtual Wrap
template class wrapper as its base
class, and it can immediately define the WrapList
type as a typelist
containing the class's base class:
template <typename Type, template <typename> class TemplateClass, int idx> class GenScatter : virtual public Wrap<TemplateClass<Type>, idx> { typedef Wrap<TemplateClass<Type>, idx> Base; public: typedef TYPELIST_1(Base) WrapList; };
Finally, a specialization to handle the ending NullType
is required:
it merely defines an empty WrapList
:
template <template <typename> class TemplateClass, int idx> class GenScatter<NullType, TemplateClass, idx> { public: typedef NullType WrapList; };
Both the Wrap
template class wrapper and the GenScatter
class can
normally be defined in the anonymous namespace, as they are only used at
file-scope, by themselves, by GenScatter
and by the occasional additional
support functions and classes.
GenScatter
class returns a typelist containing all Wrap
base
classes matching the types in the order in which they appeared in
GenScat
's typelist, it is attractive to be able to obtain these Wrap
base class types by their index numbers. Being able to reach these types by
their indices allows, e.g., base class intializations as well as quick access
to their respective members.
The class template BaseClass
was designed with these thoughts in mind. It
uses AtIndex
(cf. section 22.6.3) to obtain a particular Wrap
base class and returns the latter type as its Type
type definition:
template <int idx, typename Derived> struct BaseClass { typedef typename TypeAt<idx, typename Derived::WrapList>::Type Type; };
Once a GenScatter
object is available, the following function template can
be used to obtain a cast-less reference to any of its Wrap
policy classes,
given its index. Since the index can be matched one-to-one with the
GenScatter
's typelist, clients should have no problem finding the
appropriate index values for a particular problem at hand:
template <int idx, typename Derived> struct BaseClass { typedef typename TypeAt<idx, typename Derived::WrapList>::Type Type; };
GenScat
can be used by itself, to define a simple
struct
containg various data members. The foundation of such a
conglomerate struct
could be the following struct Field
:
template <typename Type> struct Field { Type field; Field(Type v = Type()) : field(v) {} };
Such an instant struct
could be
useful in various situations; due to the nature of the struct Field
, all
data types would by default be initialized to their natural defaults. E.g.,
GenScat
can be used directly as follows:
GenScat<TYPELIST_2(int, int), Field> gs; base<1>(gs).d_value = 12; cout << base<0>(gs).d_value << " " << base<1>(gs).d_value << endl;The above code, when it is run from
main()
will write the values 0 and
12, showing that default initialization and assignment to the individual
fields is simply implemented.
Useful as this may be, sometimes more refined initializations may be
necessary. E.g, an application needs a struct
having two int
data
fields and a reference to a std::string
. Since the struct
contains a
reference field, an initialization is required at construction time. In this
case a struct
can be derived from GenScat
, while providing a
constructor for the derived class performing the necessary
intiializations. For situations like these, the BaseClass
support struct
(section 22.7.3) comes in quite handy. Here is the struct
MyStruct
, derived from the appropriate GenScat
template, including its
field-initializations:
struct MyStruct: public GenScat<TYPELIST_3(int, std::string &, int), Field> { MyStruct(int i, std::string &text, int i2) : BaseClass<0, MyStruct>::Type(i), BaseClass<1, MyStruct>::Type(text), BaseClass<2, MyStruct>::Type(i2) {} };
Note how each of the types in the provided typelist has its order-number
mapped to the index used with the BaseClass
invocations. Also, since
MyStruct
is also an object of its base class (GenScat
), it can be
specified as the Derived
argument of BaseClass
. Furthermore, from the
types specified in the typelist the types of acceptable arguments of the
Type
s to be initialized can be derived. E.g., the string text
is
passed as argument to Type
when initializing the second field.
The following example shows how MyStruct
can be used:
string text("hello"); MyStruct myStruct(12345, text, 12); cout << base<0>(myStruct).field << " " << base<1>(myStruct).field << " " << base<2>(myStruct).field << endl; base<0>(myStruct).field = 123; base<1>(myStruct).field = "new text"; cout << base<0>(myStruct).field << "\n" << "`text' now contains: " << text << endl;When these lines of code are placed in a
main()
function, and the
program is run the following output is produced showing proper initialization,
reassignment and reassignment of the refered to string text
via the
appropriate MyStruct
field:
12345 hello 12 123 `text' now contains: new text
As a final example consider the struct Vectors
:
struct Vectors: public GenScat<TYPELIST_3(int, std::string, int), std::vector> { Vectors() : BaseClass<0, Vectors>::Type(std::vector<int>(1)), BaseClass<1, Vectors>::Type(std::vector<std::string>(2)), BaseClass<2, Vectors>::Type(std::vector<int>(3)) {} };
The struct Vectors
uses std::vector
as its template template
parameter, and Vectors
objects will thus offer three std::vector
s: the
first containing int
s, the second string
s, and the third again
int
s. Due to the nature of the Wrap
template class wrapper, the three
std::vector
base classes of Vectors
must be initialized by
std::vector
objects, and the constructor simply provides three vectors of
varying sizes. Alternatively, the constructor could be furnished with three
vector
references or three size_t
values to allow a more flexible
initialization.
A Vectors
object could be used as follows, showing that the base
support function (cf. section 22.7.3) provides easy access to the
vector
base class of choice:
Vectors vects; cout << base<0>(vects).size() << " " << base<1>(vects).size() << " " << base<2>(vects).size() << endl;Running this code fragment produces the output `
1 2 3
', as expected.