Traits and Policy Classes
Templates enable us to parameterize classes and functions for
various types. It could be tempting to introduce as many template parameters as
possible to enable the customization of every aspect of a type or algorithm. In
this way, our "templatized" components could be instantiated to meet the exact
needs of client code.
However, from a practical point of view it is rarely
desirable to introduce dozens of template parameters for maximal
parameterization. Having to specify all the corresponding arguments in the
client code is overly tedious.
Fortunately, it turns out that most of the extra parameters we
would introduce have reasonable default values. In some cases the extra
parameters are entirely determined by a few main
parameters, and we'll see that such extra parameters can be omitted altogether.
Other parameters can be given default values that depend on the main parameters
and will meet the needs of most situations, but the default values must
occasionally be overridden (for special applications). Yet other parameters are
unrelated to the main parameters: In a sense they are themselves main
parameters, except for the fact that there exist default values that almost
always fit the bill.
Policy classes and traits (or traits
templates) are C++ programming devices that greatly facilitate the
management of the sort of extra parameters that come up in the design of
industrial-strength templates. In this chapter we show a number of situations in
which they prove useful and demonstrate various techniques that will enable you
to write robust and powerful devices of your own.
An Example: Accumulating a Sequence
Computing the sum of a sequence of values is a fairly common
computational task. However, this seemingly simple problem provides us with an
excellent example to introduce various levels at which policy classes and traits
can help.
15.1.1 Fixed Traits
Let's first assume that the values of the sum we want to
compute are stored in an array, and we are given a pointer to the first element
to be accumulated and a pointer one past the last element to be accumulated.
Because this book is about templates, we wish to write a template that will work
for many types. The following may seem straightforward by now [1]:
[1] Most examples in this section use ordinary pointers for the sake of simplicity. Clearly, an industrial-strength interface may prefer to use iterator parameters following the conventions of the C++ standard library (see [JosuttisStdLib]). We revisit this aspect of our example later.
// traits/accum1.hpp #ifndef ACCUM_HPP #define ACCUM_HPP template <typename T> inline T accum (T const* beg, T const* end) { T total = T(); // assume T() actually creates a zero value while (beg != end) { total += *beg; ++beg; } return total; } #endif // ACCUM_HPP
The only slightly subtle decision here is how to create a zero value of the correct type to start our summation.
We use the expression T() here, which normally should work for built-in
numeric types like int and float (see Section 5.5 on page
56).
To motivate our first traits template, consider the following
code that makes use of our accum():
// traits/accum1.cpp #include "accum1.hpp" #include <iostream> int main() { // create array of 5 integer values int num[]={1,2,3,4,5}; // print average value std::cout << "the average value of the integer values is " << accum(&num[0], &num[5]) / 5 << '\n'; // create array of character values char name[] = "templates"; int length = sizeof(name)-1; // (try to) print average character value std::cout << "the average value of the characters in \"" << name << "\" is " << accum(&name[0], &name[length]) / length << '\n'; }
In the first half of the program we use accum() to sum
five integer values:
int num[]={1,2,3,4,5};
…
accum(&num[0], &num[5])
The average integer value is then obtained by simply dividing
the resulting sum by the number of values in the array.
The second half of the program attempts to do the same for all
letters in the word template (provided the characters from a
to z form a contiguous sequence in the actual character set, which is
true for ASCII but not for EBCDIC [2]). The result should presumably lie between
the value of a and the value of z. On most platforms today,
these values are determined by the ASCII codes: a is encoded as 97 and
z is encoded as 122. Hence, we may expect a result between 97 and 122.
However, on our platform the output of the program is as follows:
[2] EBCDIC is an abbreviation of Extended Binary-Coded Decimal Interchange Code, which is an IBM character set that is widely used on large IBM computers.
the average value of the integer values is 3 the average value of the characters in "templates" is -5
The problem here is that our template was instantiated for the
type char, which turns out to be too by introducing an additional
template parameter AccT that describes the type used for the variable
total (and hence the return type). However, this would put an extra
burden on all users of our template: They would have to specify an extra type in
every invocation of our template. In our example we may, therefore, need to
write the following:
accum<int>(&name[0],&name[length])
This is not an excessive constraint, but it can be avoided.
An alternative approach to the extra parameter is to create an
association between each type T for which accum() is called
and the corresponding type that should be used to hold the accumulated value.
This association could be considered characteristic of the type T, and
therefore the type in which the sum is computed is sometimes called a trait of T. As is turns out, our association
can be encoded as specializations of a template:
// traits/accumtraits2.hpp
template<typename T>
class AccumulationTraits;
template<>
class AccumulationTraits<char> {
public:
typedef int AccT;
};
template<>
class AccumulationTraits<short> {
public:
typedef int AccT;
};
template<>
class AccumulationTraits<int> {
public:
typedef long AccT;
};
template<>
class AccumulationTraits<unsigned int> {
public:
typedef unsigned long AccT;
};
template<>
class AccumulationTraits<float> {
public:
typedef double AccT;
};
The template AccumulationTraits is called a traits template because it holds a trait of its
parameter type. (In general, there could be more than one trait and more than
one parameter.) We chose not to provide a generic definition of this template
because there isn't a great way to select a good accumulation type when we don't
know what the type is. However, an argument could be made that T itself
is often a good candidate for such a type (although clearly not in our earlier
example).
With this in mind, we can rewrite our accum() template
as follows:
// traits/accum2.hpp #ifndef ACCUM_HPP #define ACCUM_HPP #include "accumtraits2.hpp" template <typename T> inline typename AccumulationTraits<T>::AccT accum (T const* beg, T const* end) { // return type is traits of the element type typedef typename AccumulationTraits<T>::AccT AccT; AccT total = AccT(); // assume T() actually creates a zero value while (beg != end) { total += *beg; ++beg; } return total; } #endif // ACCUM_HPP
The output of our sample program then becomes what we
expect:
the average value of the integer values is 3 the average value of the characters in "templates" is 108
Overall, the changes aren't very dramatic considering that we
have added a very useful mechanism to customize our algorithm. Furthermore, if
new types arise for use with accum(), an appropriate AccT can
be associated with it simply by declaring an additional explicit specialization
of the AccumulationTraits template. Note that this can be done for any
type: fundamental types, types that are declared in other libraries, and so
forth.
15.1.2 Value Traits
So far, we have seen that traits represent additional type
information related to a given "main" type. In this section we show that this
extra information need not be limited to types. Constants and other classes of
values can be associated with a type as well.
Our original accum() template uses the default
constructor of the return value to initialize the result variable with what is
hoped to be a zero-like value:
AccT total = AccT(); // assume T() actually creates a zero value … return total;
Clearly, there is no guarantee that this produces a good value
to start the accumulation loop. Type T may not even have a default
constructor.
Again, traits can come to the rescue. For our example, we can
add a new value trait to our
AccumulationTraits:
// traits/accumtraits3.hpp template<typename T> class AccumulationTraits; template<> class AccumulationTraits<char> { public: typedef int AccT; static AccT const zero = 0; }; template<> class AccumulationTraits<short> { public: typedef int AccT; static AccT const zero = 0; }; template<> class AccumulationTraits<int> { public: typedef long AccT; static AccT const zero = 0; }; …
In this case, our new trait is a constant that can be evaluated
at compile time. Thus, accum() becomes:
// traits/accum3.hpp #ifndef ACCUM_HPP #define ACCUM_HPP #include "accumtraits3.hpp" template <typename T> inline typename AccumulationTraits<T>::AccT accum (T const* beg, T const* end) { // return type is traits of the element type typedef typename AccumulationTraits<T>::AccT AccT; AccT total = AccumulationTraits<T>::zero; while (beg != end) { total += *beg; ++beg; } return total; } #endif // ACCUM_HPP
In this code, the initialization of the accumulation variable
remains straightforward:
AccT total = AccumulationTraits<T>::zero;
A drawback of this formulation is that C++ allows us to
initialize only a static constant data member inside its class if it has an
integral or enumeration type. This excludes our own classes, of course, and
floating-point types as well. The following specialization is, therefore, an
error:
… template<> class AccumulationTraits<float> { public: typedef double AccT; static double const zero = 0.0; // ERROR: not an integral type };
The straightforward alternative is not to define the value
trait in its class:
…
template<>
class AccumulationTraits<float> {
public:
typedef double AccT;
static double const zero;
};
The initializer then goes in a source file and looks something
like the following:
…
double const AccumulationTraits<float>::zero = 0.0;
Although this works, it has the disadvantage of being more
opaque to compilers. While processing client files, compilers are typically
unaware of definitions in other files. In this case, for example, a compiler
would not be able to take advantage of the fact that the value zero is
really 0.0.
Consequently, we prefer to implement value traits, which are
not guaranteed to have integral values as inline member functions. [3] For
example, we could rewrite AccumulationTraits as follows:
[3] Most modern C++ compilers can "see through" calls of simple inline functions.
// traits/accumtraits4.hpp template<typename T> class AccumulationTraits; template<> class AccumulationTraits<char> { public: typedef int AccT; static AccT zero() { return 0; } }; template<> class AccumulationTraits<short> { public: typedef int AccT; static AccT zero() { return 0; } }; template<> class AccumulationTraits<int> { public: typedef long AccT; static AccT zero() { return 0; } }; template<> class AccumulationTraits<unsigned int> { public: typedef unsigned long AccT; static AccT zero() { return 0; } }; template<> class AccumulationTraits<float> { public: typedef double AccT; static AccT zero() { return 0; } }; …
For the application code, the only difference is the use of
function call syntax (instead of the slightly more concise access to a static
data member):
AccT total = AccumulationTraits<T>::zero();
Clearly, traits can be more than just extra types. In our example, they can be a mechanism to
provide all the necessary information that accum() needs about the
element type for which it is called. This is the key of the traits concept:
Traits provide an avenue to configure concrete
elements (mostly types) for generic computations.
15.1.3 Parameterized Traits
The use of traits in accum() in the previous sections
is called fixed, because once the decoupled trait
is defined, it cannot be overridden in the algorithm. There may be cases when
such overriding is desirable. For example, we may happen to know that a set of
float values can safely be summed into a variable of the same type, and
doing so may buy us some efficiency.
In principle, the solution consists of adding a template
parameter but with a default value determined by our traits template. In this
way, many users can omit the extra template argument, but those with more
exceptional needs can override the preset accumulation type. The only bee in our
bonnet for this particular case is that function templates cannot have default
template arguments. [4]
[4] This is almost certainly going to change in a revision of the C++ standard, and compiler vendors are likely to provide the feature even before this revised standard is published (see Section 13.3 on page 207).
For now, let's circumvent the problem by formulating our
algorithm as a class. This also illustrates the fact that traits can be used in
class templates at least as easily as in function templates. The drawback in our
application is that class templates cannot have their template arguments
deduced. They must be provided explicitly. Hence, we need the form
Accum<char>::accum(&name[0], &name[length])
to use our revised accumulation template:
// traits/accum5.hpp #ifndef ACCUM_HPP #define ACCUM_HPP #include "accumtraits4.hpp" template <typename T, typename AT = AccumulationTraits<T> > class Accum { public: static typename AT::AccT accum (T const* beg, T const* end) { typename AT::AccT total = AT::zero(); while (beg != end) { total += *beg; ++beg; } return total; } }; #endif // ACCUM_HPP
Presumably, most users of this template would never have to
provide the second template argument explicitly because it can be configured to
an appropriate default for every type used as a first argument.
As is often the case, we can introduce convenience functions to
simplify the interface:
template <typename T> inline typename AccumulationTraits<T>::AccT accum (T const* beg, T const* end) { return Accum<T>::accum(beg, end); } template <typename Traits, typename T> inline typename Traits::AccT accum (T const* beg, T const* end) { return Accum<T, Traits>::accum(beg, end); }
15.1.4 Policies and Policy Classes
So far we have equated accumulation with summation. Clearly we can imagine other kinds of
accumulations. For example, we could multiply the sequence of given values. Or,
if the values were strings, we could concatenate them. Even finding the maximum
value in a sequence could be formulated as an accumulation problem. In all these
alternatives, the only accum() operation that needs to change is
total += *start. This operation can be called a policy of our accumulation process. A policy class,
then, is a class that provides an interface to apply one or more policies in an
algorithm. [5]
[5] We could generalize this to a policy parameter, which could be a class (as discussed) or a pointer to a function.
Here is an example of how we could introduce such an interface
in our Accum class template:
// traits/accum6.hpp #ifndef ACCUM_HPP #define ACCUM_HPP #include "accumtraits4.hpp" #include "sumpolicy1.hpp" template <typename T, typename Policy = SumPolicy, typename Traits = AccumulationTraits<T> > class Accum { public: typedef typename Traits::AccT AccT; static AccT accum (T const* beg, T const* end) { AccT total = Traits::zero(); while (beg != end) { Policy::accumulate(total, *beg); ++beg; } return total; } }; #endif // ACCUM_HPP
With this a SumPolicy could be written as follows:
// traits/sumpolicy1.hpp #ifndef SUMPOLICY_HPP #define SUMPOLICY_HPP class SumPolicy { public: template<typename T1, typename T2> static void accumulate (T1& total, T2 const & value) { total += value; } }; #endif // SUMPOLICY_HPP
In this example we chose to make our policy an ordinary class
(that is, not a template) with a static member function template (which is
implicitly inline). We discuss an alternative option later.
By specifying a different policy to accumulate values we can
compute different things. Consider, for example, the following program, which
intends to determine the product of some values:
// traits/accum7.cpp #include "accum6.hpp" #include <iostream> class MultPolicy { public: template<typename T1, typename T2> static void accumulate (T1& total, T2 const& value) { total *= value; } }; int main() { // create array of 5 integer values int num[]={1,2,3,4,5}; // print product of all values std::cout << "the product of the integer values is " << Accum<int,MultPolicy>::accum(&num[0], &num[5]) << '\n'; }
However, the output of this program isn't what we would
like:
the product of the integer values is 0
The problem here is caused by our choice of initial value:
Although 0 works well for summation, it does not work for
multiplication (a zero initial value forces a zero result for accumulated
multiplications). This illustrates that different traits and policies may
interact, underscoring the importance of careful template design.
In this case we may recognize that the initialization of an
accumulation loop is a part of the accumulation policy. This policy may or may
not make use of the trait zero(). Other alternatives are not to be
forgotten: Not everything must be solved with traits and policies. For example,
the accumulate() function of the C++ standard library takes the initial
value as a third (function call) argument.
15.1.5 Traits and Policies: What's the Difference?
A reasonable case can be made in support of the fact that
policies are just a special case of traits. Conversely, it could be claimed that
traits just encode a policy.
The New Shorter Oxford English
Dictionary (see [NewShorterOED]) has this to say:
-
trait n. ... a distinctive feature characterizing a thing
-
policy n. ... any course of action adopted as advantegous or expedient
Based on this, we tend to limit the use of the term policy classes to classes that encode an action of some
sort that is largely orthogonal with respect to any other template argument with
which it is combined. This is in agreement with Andrei Alexandrescu's statement
in his book Modern C++ Design (see page 8 of [AlexandrescuDesign]) [6]:
[6] Alexandrescu has been the main voice in the world of policy classes, and he has developed a rich set of techniques based on them.
Policies have much in common with traits but differ in that they put less emphasis on type and more on behavior.
Nathan Myers, who introduced the traits technique, proposed the
following more open-ended definition (see [MyersTraits]):
Traits class: A class used in place of template parameters. As a class, it aggregates useful types and constants; as a template, it provides an avenue for that "extra level of indirection" that solves all software problems.
In general, we therefore tend to use the following (slightly
fuzzy) definitions:
-
Traits represent natural additional properties of a template parameter.
-
Policies represent configurable behavior for generic functions and types (often with some commonly used defaults).
To elaborate further on the possible distinctions between the
two concepts, we list the following observations about traits:
-
Traits can be useful as fixed traits (that is, without being passed through template parameters).
-
Traits parameters usually have very natural default values (which are rarely overridden, or simply cannot be overridden).
-
Traits parameters tend to depend tightly on one or more main parameters.
-
Traits mostly combine types and constants rather than member functions.
-
Traits tend to be collected in traits templates.
For policy classes, we make the following observations:
-
Policy classes don't contribute much if they aren't passed as template parameters.
-
Policy parameters need not have default values and are often specified explicitly (although many generic components are configured with commonly used default policies).
-
Policy parameters are mostly orthogonal to other parameters of a template.
-
Policy classes mostly combine member functions.
-
Policies can be collected in plain classes or in class templates.
However, there is certainly an indistinct line between both
terms. For example, the character traits of the C++ standard library also define
functional behavior such as comparing, moving, and finding characters. And by
replacing these traits you can define string classes that behave in a
case-insensitive manner (see Section 11.2.14 in [JosuttisStdLib]) while keeping the same
character type. Thus, although they are called traits, they have some properties associated with
policies.
15.1.6 Member Templates versus Template Template Parameters
To implement an accumulation policy we chose to express
SumPolicy and MultPolicy as ordinary classes with a member
template. An alternative consists of designing the policy class interface using
class templates, which are then used as template template arguments. For
example, we could rewrite SumPolicy as a template:
// traits/sumpolicy2.hpp #ifndef SUMPOLICY_HPP #define SUMPOLICY_HPP template <typename T1, typename T2> class SumPolicy { public: static void accumulate (T1& total, T2 const & value) { total += value; } }; #endif // SUMPOLICY_HPP
The interface of Accum can then be adapted to use a
template template parameter:
// traits/accum8.hpp #ifndef ACCUM_HPP #define ACCUM_HPP #include "accumtraits4.hpp" #include "sumpolicy2.hpp" template <typename T, template<typename,typename> class Policy = SumPolicy, typename Traits = AccumulationTraits<T> > class Accum { public: typedef typename Traits::AccT AccT; static AccT accum (T const* beg, T const* end) { AccT total = Traits::zero(); while (beg != end) { Policy<AccT,T>::accumulate(total, *beg); ++beg; } return total; } }; #endif // ACCUM_HPP
The same transformation can be applied to the traits parameter.
(Other variations on this theme are possible: For example, instead of explicitly
passing the AccT type to the policy type, it may be advantageous to
pass the accumulation trait and have the policy determine the type of its result
from a traits parameter.)
The major advantage of accessing policy classes through
template template parameters is that it makes it easier to have a policy class
carry with it some state information (that is, static data members) with a type
that depends on the template parameters. (In our first approach the static data
members would have to be embedded in a member class template.)
However, a downside of the template template parameter approach
is that policy classes must now be written as templates, with the exact set of
template parameters defined by our interface. This, unfortunately, disallows any
additional template parameters in our policies. For example, we may want to add
a Boolean nontype template parameter to SumPolicy that selects whether
summation should happen with the += operator or whether + only
should be used. In the program using a member template we can simply rewrite
SumPolicy as a template:
// traits/sumpolicy3.hpp #ifndef SUMPOLICY_HPP #define SUMPOLICY_HPP template<bool use_compound_op = true> class SumPolicy { public: template<typename T1, typename T2> static void accumulate (T1& total, T2 const & value) { total += value; } }; template<> class SumPolicy<false> { public: template<typename T1, typename T2> static void accumulate (T1& total, T2 const & value) { total = total + value; } }; #endif // SUMPOLICY_HPP
With implementation of Accum using template template
parameters such an adaptation is no longer possible.
15.1.7 Combining Multiple Policies and/or Traits
As our development has shown, traits and policies don't
entirely do away with having multiple template parameters. However, they do
reduce their number to something manageable. An interesting question, then, is
how to order such multiple parameters.
A simple strategy is to order the parameters according to the
increasing likelihood of their default value to be selected. Typically, this
would mean that the traits parameters follow the policy parameters because the
latter are more often overridden in client code. (The observant reader may have
noticed this strategy in our development.)
If we are willing to add a significant amount of complexity to
our code, an alternative exists that essentially allows us to specify the
nondefault arguments in any order. Refer to Section 16.1 on page 285
for details. Chapter 13
also discusses potential future template features that could simplify the
resolution of this aspect of template design.
15.1.8 Accumulation with General Iterators
Before we end this introduction to traits and policies, it is
instructive to look at one version of accum() that adds the capability
to handle generalized iterators (rather than just pointers), as expected from an
industrial-strength generic component. Interestingly, this still allows us to
call accum() with pointers because the C++ standard library provides
so-called iterator traits. (Traits are
everywhere!) Thus, we could have defined our initial version of accum()
as follows (ignoring our later refinements):
// traits/accum0.hpp #ifndef ACCUM_HPP #define ACCUM_HPP #include <iterator> template <typename Iter> inline typename std::iterator_traits<Iter>::value_type accum (Iter start, Iter end) { typedef typename std::iterator_traits<Iter>::value_type VT; VT total = VT(); // assume T() actually creates a zero value while (start != end) { total += *start; ++start; } return total; } #endif // ACCUM_HPP
The iterator_traits structure encapsulates all the
relevant properties of iterator. Because a partial specialization for pointers
exists, these traits are conveniently used with any ordinary pointer types. Here
is how a standard library implementation may implement this support:
namespace std { template <typename T> struct iterator_traits<T*> { typedef T value_type; typedef ptrdiff_t difference_type; typedef random_access_iterator_tag iterator_category; typedef T* pointer; typedef T& reference; }; }
However, there is no type for the accumulation of values to
which an iterator refers; hence we still need to design our own
AccumulationTraits.
Type Functions
The initial traits example demonstrates that you can define
behavior that depends on types. This is different from what you usually
implement in programs. In C and C++, functions more exactly can be called value functions: They take some values as parameters
and return another value as a result. Now, what we have with templates are type functions: a function that takes some type
arguments and produces a type or constant as a result.
A very useful built-in type function is sizeof, which
returns a constant describing the size (in bytes) of the given type argument.
Class templates can also serve as type functions. The parameters of the type
function are the template parameters, and the result is extracted as a member
type or member constant. For example, the sizeof operator could be
given the following interface:
// traits/sizeof.cpp
#include <stddef.h>
#include <iostream>
template <typename T>
class TypeSize {
public:
static size_t const value = sizeof(T);
};
int main()
{
std::cout << "TypeSize<int>::value = "
<< TypeSize<int>::value << std::endl;
}
In what follows we develop a few more general-purpose type
functions that can be used as traits classes in this way.
15.2.1 Determining Element Types
For another example, assume that we have a number of container
templates such as vector<T>, list<T>, and
stack<T>. We want a type function that, given such a container
type, produces the element type. This can be achieved using partial
specialization:
// traits/elementtype.cpp #include <vector> #include <list> #include <stack> #include <iostream> #include <typeinfo> template <typename T> class ElementT; // primary template template <typename T> class ElementT<std::vector<T> > { // partial specialization public: typedef T Type; }; template <typename T> class ElementT<std::list<T> > { // partial specialization public: typedef T Type; }; template <typename T> class ElementT<std::stack<T> > { // partial specialization public: typedef T Type; }; template <typename T> void print_element_type (T const & c) { std::cout << "Container of " << typeid(typename ElementT<T>::Type).name() << " elements.\n"; } int main() { std::stack<bool> s; print_element_type(s); }
The use of partial specialization allows us to implement this
without requiring the container types to know about the type function. In many
cases, however, the type function is designed along with the applicable types
and the implementation can be simplified. For example, if the container types
define a member type value_type (as the standard containers do), we can
write the following:
template <typename C> class ElementT { public: typedef typename C::value_type Type; };
This can be the default implementation, and it does not exclude
specializations for container types that do not have an appropriate member type
value_type defined. Nonetheless, it is usually advisable to provide
type definitions for template type parameters so that they can be accessed more
easily in generic code. The following sketches the idea:
template <typename T1, typename T2, ... > class X { public: typedef T1 … ; typedef T2 … ; … };
How is a type function useful? It allows us to parameterize a
template in terms of a container type, without also requiring parameters for the
element type and other characteristics. For example, instead of
template <typename T, typename C> T sum_of_elements (C const& c);
which requires syntax like
sum_of_elements<int>(list) to specify the element type
explicitly, we can declare
template<typename C> typename ElementT<C>::Type sum_of_elements (C const& c);
where the element type is determined from the type
function.
Note that the traits can be implemented as an extension to the
existing types. Thus, you can define these type functions even for fundamental
types and types of closed libraries.
In this case, the type ElementT is called a traits
class because it is used to access a trait of the given container type C (in
general, more than one trait can be collected in such a class). Thus, traits
classes are not limited to describing characteristics of container parameters
but of any kind of "main parameters."
15.2.2 Determining Class Types
With the following type function we can determine whether a
type is a class type:
// traits/isclasst.hpp
template<typename T>
class IsClassT {
private:
typedef char One;
typedef struct { char a[2]; } Two;
template<typename C> static One test(int C::*);
template<typename C> static Two test(…);
public:
enum { Yes = sizeof(IsClassT<T>::test<T>(0)) == 1 };
enum { No = !Yes };
};
This template uses the SFINAE
(substitution-failure-is-not-an-error) principle of Section 8.3.1 on page 106.
The key to exploit SFINAE is to find a type construct that is invalid for
function types but not for other types, or vice versa. For class types we can
rely on the observation that the pointer-to-member type construct int
C::* is valid only if C is a class type.
The following program uses this type function to test whether
certain types and objects are class types:
// traits/isclasst.cpp #include <iostream> #include "isclasst.hpp" class MyClass { }; struct MyStruct { }; union MyUnion { }; void myfunc() { } enumE{e1}e; // check by passing type as template argument template <typename T> void check() { if (IsClassT<T>::Yes) { std::cout << " IsClassT " << std::endl; } else { std::cout << " !IsClassT " << std::endl; } } // check by passing type as function call argument template <typename T> void checkT (T) { check<T>(); } int main() { std::cout << "int: "; check<int>(); std::cout << "MyClass: "; check<MyClass>(); std::cout << "MyStruct:"; MyStruct s; checkT(s); std::cout << "MyUnion: "; check<MyUnion>(); std::cout << "enum: "; checkT(e); std::cout << "myfunc():"; checkT(myfunc); }
The program has the following output:
int: !IsClassT MyClass: IsClassT MyStruct: IsClassT MyUnion: IsClassT enum: !IsClassT myfunc(): !IsClassT
15.2.3 References and Qualifiers
Consider the following function template definition:
// traits/apply1.hpp
template <typename T>
void apply (T& arg, void (*func)(T))
{
func(arg);
}
Consider also the following code that attempts to use it:
// traits/apply1.cpp
#include <iostream>
#include "apply1.hpp"
void incr (int& a)
{
++a;
}
void print (int a)
{
std::cout << a << std::endl;
}
int main()
{
intx=7;
apply (x, print);
apply (x, incr);
}
The call
apply (x, print)
is fine. With T substituted by int, the
parameter types of apply() are int& and
void(*)(int), which corresponds to the types of the arguments. The
call
apply (x, incr)
is less straightforward. Matching the second parameter requires
T to be substituted with int&, and this implies that the
first parameter type is int& &, which ordinarily is not a legal
C++ type. Indeed, the original C++ standard ruled this an invalid substitution,
but because of examples like this, a later technical
corrigendum (a set of small corrections of the standard; see [Standard02]) made
T& with T substituted by int& equivalent to
int&. [7]
[7] Note that we still cannot write int& &. This is similar to the fact that T const allows T to be substituted with int const, but an explicit int const const is not valid.
For C++ compilers that do not implement the newer reference
substitution rule, we can create a type function that applies the "reference
operator" if and only if the given type is not already a reference. We can also
provide the opposite operation: Strip the reference operator (if and only if the
type is indeed a reference). And while we are at it, we can also add or strip
const qualifiers. [8] All this is achieved using partial
specialization of the following generic definition:
[8] The handling of volatile and const volatile qualifiers is omitted for brevity, but they can be handled similarly.
// traits/typeop1.hpp template <typename T> class TypeOp { // primary template public: typedef T ArgT; typedef T BareT; typedef T const ConstT; typedef T & RefT; typedef T & RefBareT; typedef T const & RefConstT; };
First, a partial specialization to catch const
types:
// traits/typeop2.hpp template <typename T> class TypeOp <T const> { // partial specialization for const types public: typedef T const ArgT; typedef T BareT; typedef T const ConstT; typedef T const & RefT; typedef T & RefBareT; typedef T const & RefConstT; };
The partial specialization to catch reference types also
catches reference-to-const types. Hence, it applies the TypeOp
device recursively to obtain the bare type when necessary. In contrast, C++
allows us to apply the const qualifier to a template parameter that is
substituted with a type that is already const. Hence, we need not worry
about stripping the const qualifier when we are going to reapply it
anyway:
// traits/typeop3.hpp template <typename T> class TypeOp <T&> { // partial specialization for references public: typedef T & ArgT; typedef typename TypeOp<T>::BareT BareT; typedef T const ConstT; typedef T & RefT; typedef typename TypeOp<T>::BareT & RefBareT; typedef T const & RefConstT; };
References to void types are not allowed. It is
sometimes useful to treat such types as plain void however. The
following specialization takes care of this:
// traits/typeop4.hpp template<> class TypeOp <void> { // full specialization for void public: typedef void ArgT; typedef void BareT; typedef void const ConstT; typedef void RefT; typedef void RefBareT; typedef void RefConstT; };
With this in place, we can rewrite the apply template
as follows:
template <typename T> void apply (typename TypeOp<T>::RefT arg, void (*func)(T)) { func(arg); }
and our example program will work as intended.
Remember that T can no longer be deduced from the
first argument because it now appears in a name qualifier. So T is
deduced from the second argument only, and T is used to create the type
of the first parameter.
15.2.4 Promotion Traits
So far we have studied and developed type functions of a single
type: Given one type, other related types or constants were defined. In general,
however, we can develop type functions that depend on multiple arguments. One
example that is very useful when writing operator templates are so-called promotion traits. To motivate the idea, let's write a
function template that allows us to add two Array containers:
template<typename T> Array<T> operator+ (Array<T> const&, Array<T> const&);
This would be nice, but because the language allows us to add a
char value to an int value, we really would prefer to allow
such mixed-type operations with arrays too. We are then faced with determining
what the return type of the resulting template should be:
template<typename T1, typename T2>
Array<???> operator+ (Array<T1> const&, Array<T2> const&);
A promotion traits template allows us to fill in the question
marks in the previous declaration as follows:
template<typename T1, typename T2> Array<typename Promotion<T1, T2>::ResultT> operator+ (Array<T1> const&, Array<T2> const&);
or, alternatively, as follows:
template<typename T1, typename T2> typename Promotion<Array<T1>, Array<T2> >::ResultT operator+ (Array<T1> const&, Array<T2> const&);
The idea is to provide a large number of specializations of the
template Promotion to create a type function that matches our needs.
Another application of promotion traits was motivated by the introduction of the
max() template, when we want to specify that the maximum of two values
of different type should have the "the more powerful type" (see Section 2.3 on page
13).
There is no really reliable generic definition for this
template, so it may be best to leave the primary class template undefined:
template<typename T1, typename T2> class Promotion;
Another option would be to assume that if one of the types is
larger than the other, we should promote to that larger type. This can by done
by a special template IfThenElse that takes a Boolean nontype template
parameter to select one of two type parmeters:
// traits/ifthenelse.hpp #ifndef IFTHENELSE_HPP #define IFTHENELSE_HPP // primary template: yield second or third argument depending on first argument template<bool C, typename Ta, typename Tb> class IfThenElse; // partial specialization: true yields second argument template<typename Ta, typename Tb> class IfThenElse<true, Ta, Tb> { public: typedef Ta ResultT; }; // partial specialization: false yields third argument template<typename Ta, typename Tb> class IfThenElse<false, Ta, Tb> { public: typedef Tb ResultT; }; #endif // IFTHENELSE_HPP
With this in place, we can create a three-way selection between
T1, T2, and void, depending on the sizes of the types
that need promotion:
// traits/promote1.hpp // primary template for type promotion template<typename T1, typename T2> class Promotion { public: typedef typename IfThenElse<(sizeof(T1)>sizeof(T2)), T1, typename IfThenElse<(sizeof(T1)<sizeof(T2)), T2, void >::ResultT >::ResultT ResultT; };
The size-based heuristic used in the primary template works
sometimes, but it requires checking. If it selects the wrong type, an
appropriate specialization must be written to override the selection. On the
other hand, if the two types are identical, we can safely make it to be the
promoted type. A partial specialization takes care of this:
// traits/promote2.hpp // partial specialization for two identical types template<typename T> class Promotion<T,T> { public: typedef T ResultT; };
Many specializations are needed to record the promotion of
fundamental types. A macro can reduce the amount of source code somewhat:
// traits/promote3.hpp
#define MK_PROMOTION(T1,T2,Tr) \
template<> class Promotion<T1, T2> { \
public: \
typedef Tr ResultT; \
}; \
\
template<> class Promotion<T2, T1> { \
public: \
typedef Tr ResultT; \
};
The promotions are then added as follows:
// traits/promote4.hpp MK_PROMOTION(bool, char, int) MK_PROMOTION(bool, unsigned char, int) MK_PROMOTION(bool, signed char, int) …
This approach is relatively straightforward, but requires the
several dozen possible combinations to be enumerated. Various alternative
techniques exist. For example, the IsFundaT and IsEnumT
templates could be adapted to define the promotion type for integral and
floating-point types. Promotion would then need to be specialized only for the
resulting fundamental types (and user-defined types, as shown in a moment).
Once Promotion is defined for fundamental types (and
enumeration types if desired), other promotion rules can often be expressed
through partial specialization. For our Array example:
// traits/promotearray.hpp
template<typename T1, typename T2>
class Promotion<Array<T1>, Array<T2> > {
public:
typedef Array<typename Promotion<T1,T2>::ResultT> ResultT;
};
template<typename T>
class Promotion<Array<T>, Array<T> > {
public:
typedef Array<typename Promotion<T,T>::ResultT> ResultT;
};
This last partial specialization deserves some special
attention. At first it may seem that the earlier partial specialization for
identical types (Promotion<T,T>) already takes care of this case.
Unfortunately, the partial specialization Promotion<Array<T1>,
Array<T2> > is neither more nor less specialized than the partial
specialization Promotion<T,T> (see also Section 12.4 on page 200).
[9] To
avoid template selection ambiguity, the last partial specialization was added.
It is more specialized than either of the previous two partial
specializations.
[9] To see this, try to find a substitution of T that makes the latter become the former, or substitutions for T1 and T2 that make the former become the latter.
More specializations and partial specializations of the
Promotion template can be added as more types are added for which a
concept promotion makes sense.
Policy Traits
So far, our examples of traits templates have been used to
determine properties of template parameters: what sort of type they represent,
to which type they should promote in mixed-type operations, and so forth. Such
traits are called property traits.
In contrast, some traits define how some types should be
treated. We call them policy traits. This is
reminiscent of the previously discussed concept of policy classes (and we
already pointed out that the distinction between traits and policies is not
entirely clear), but policy traits tend to be more unique properties associated
with a template parameter (whereas policy classes are usually independent of
other template parameters).
Although property traits can often be implemented as type
functions, policy traits usually encapsulate the policy in member functions. As
a first illustration, let's look at a type function that defines a policy for
passing read-only parameters.
15.3.1 Read-only Parameter Types
In C and C++, function call arguments are passed "by value" by
default. This means that the values of the arguments computed by the caller are
copied to locations controlled by the callee. Most programmers know that this
can be costly for large structures and that for such structures it is
appropriate to pass the arguments "by reference-to-const" (or "by
pointer-to-const" in C). For smaller structures, the picture is not
always clear, and the best mechanism from a performance point of view depends on
the exact architecture for which the code is being written. This is not so
critical in most cases, but sometimes even the small structures must be handled
with care.
With templates, of course, things get a little more delicate:
We don't know a priori how large the type substituted for the template parameter
will be. Furthermore, the decision doesn't depend just on size: A small
structure may come with an expensive copy constructor that would still justify
passing read-only parameters "by reference-to-const."
As hinted at earlier, this problem is conveniently handled
using a policy traits template that is a type function: The function maps an
intended argument type T onto the optimal parameter type T or T
const&. As a first approximation, the primary template can use "by
value" passing for types no larger than two pointers and "by
reference-to-const" for everything else:
template<typename T> class RParam { public: typedef typename IfThenElse<sizeof(T)<=2*sizeof(void*), T, T const&>::ResultT Type; };
On the other hand, container types for which sizeof
returns a small value may involve expensive copy constructors. So we may need
many specializations and partial specializations, such as the following:
template<typename T> class RParam<Array<T> > { public: typedef Array<T> const& Type; };
Because such types are common in C++, it may be safer to mark
nonclass types "by value" in the primary template and then selectively add the
class types when performance considerations dictate it (the primary template
uses IsClassT<> from page 266 to identify class types):
// traits/rparam.hpp #ifndef RPARAM_HPP #define RPARAM_HPP #include "ifthenelse.hpp" #include "isclasst.hpp" template<typename T> class RParam { public: typedef typename IfThenElse<IsClassT<T>::No, T, T const&>::ResultT Type; }; #endif // RPARAM_HPP
Either way, the policy can now be centralized in the traits
template definition, and clients can exploit it to good effect. For example,
let's suppose we have two classes, with one class specifying that calling by
value is better for read-only arguments:
// traits/rparamcls.hpp #include <iostream> #include "rparam.hpp" class MyClass1 { public: MyClass1 () { } MyClass1 (MyClass1 const&) { std::cout << "MyClass1 copy constructor called\n"; } }; class MyClass2 { public: MyClass2 () { } MyClass2 (MyClass2 const&) { std::cout << "MyClass2 copy constructor called\n"; } }; // pass MyClass2 objects with RParam<> by value template<> class RParam<MyClass2> { public: typedef MyClass2 Type; };
Now, you can declare functions that use RParam<>
for read-only arguments and call these functions:
// traits/rparam1.cpp #include "rparam.hpp" #include "rparamcls.hpp" // function that allows parameter passing by value or by reference template <typename T1, typename T2> void foo (typename RParam<T1>::Type p1, typename RParam<T2>::Type p2) { … } int main() { MyClass1 mc1; MyClass2 mc2; foo<MyClass1,MyClass2>(mc1,mc2); }
There are unfortunately some significant downsides to using
RParam. First, the function declaration is significantly more mess.
Second, and perhaps more objectionable, is the fact that a function like
foo() cannot be called with argument deduction because the template
parameter appears only in the qualifiers of the function parameters. Call sites
must therefore specify explicit template arguments.
An unwieldy workaround for this option is the use of an inline
wrapper function template, but it assumes the inline function will be elided by
the compiler. For example:
// traits/rparam2.cpp #include "rparam.hpp" #include "rparamcls.hpp" // function that allows parameter passing by value or by reference template <typename T1, typename T2> void foo_core (typename RParam<T1>::Type p1, typename RParam<T2>::Type p2) { … } // wrapper to avoid explicit template parameter passing template <typename T1, typename T2> inline void foo (T1 const & p1, T2 const & p2) { foo_core<T1,T2>(p1,p2); } int main() { MyClass1 mc1; MyClass2 mc2; foo(mc1,mc2); // same as foo_core<MyClass1,MyClass2>(mc1,mc2) }
15.3.2 Copying, Swapping, and Moving
To continue the theme of performance, we can introduce a policy
traits template to select the best operation to copy, swap, or move elements of
a certain type.
Presumably, copying is covered by the copy constructor and the
copy-assignment operator. This is definitely true for a single element, but it
is not impossible that copying a large number of items of a given type can be
done significantly more efficiently than by repeatedly invoking the constructor
or assignment operations of that type.
Similarly, certain types can be swapped or moved much more
efficiently than a generic sequence of the classic form:
T tmp(a); a = b; b = tmp;
Container types typically fall in this category. In fact, it
occasionally happens that copying is not allowed, whereas swapping or moving is
fine. In the chapter on utilities, we develop a so-called smart pointer with this property (see Chapter 20).
Hence, it can be useful to centralize decisions in this area in
a convenient traits template. For the generic definition, we will distinguish
class types from nonclass types because we need not worry about user-defined
copy constructors and copy assignments for the latter. This time we use
inheritance to select between two traits implementations:
// traits/csmtraits.hpp
template <typename T>
class CSMtraits : public BitOrClassCSM<T, IsClassT<T>::No > {
};
The implementation is thus completely delegated to
specializations of BitOrClassCSM<> ("CSM" stands for
"copy, swap, move"). The second template parameter indicates whether bitwise
copying can be used safely to implement the various operations. The generic
definition conservatively assumes that class types can not be bitwised copied
safely, but if a certain class type is known to be a plain old data type (or POD), the CSMtraits class is easily
specialized for better performance:
template<> class CSMtraits<MyPODType> : public BitOrClassCSM<MyPODType, true> { };
The BitOrClassCSM template consists, by default, of
two partial specializations. The primary template and the safe partial
specialization that doesn't copy bitwise is as follows:
// traits/csm1.hpp #include <new> #include <cassert> #include <stddef.h> #include "rparam.hpp" // primary template template<typename T, bool Bitwise> class BitOrClassCSM; // partial specialization for safe copying of objects template<typename T> class BitOrClassCSM<T, false> { public: static void copy (typename RParam<T>::ResultT src, T* dst) { // copy one item onto another one *dst = src; } static void copy_n (T const* src, T* dst, size_t n) { // copy n items onto n other ones for (size_tk=0;k<n; ++k) { dst[k] = src[k]; } } static void copy_init (typename RParam<T>::ResultT src, void* dst) { // copy an item onto uninitialized storage ::new(dst) T(src); } static void copy_init_n (T const* src, void* dst, size_t n) { // copy n items onto uninitialized storage for (size_tk=0;k<n; ++k) { ::new((void*)((char*)dst+k)) T(src[k]); } } static void swap (T* a, T* b) { // swap two items T tmp(a); *a = *b; *b = tmp; } static void swap_n (T* a, T* b, size_t n) { // swap n items for (size_tk=0;k<n; ++k) { T tmp(a[k]); a[k] = b[k]; b[k] = tmp; } } static void move (T* src, T* dst) { // move one item onto another assert(src != dst); *dst = *src; src->~T(); } static void move_n (T* src, T* dst, size_t n) { // move n items onto n other ones assert(src != dst); for (size_tk=0;k<n; ++k) { dst[k] = src[k]; src[k].~T(); } } static void move_init (T* src, void* dst) { // move an item onto uninitialized storage assert(src != dst); ::new(dst) T(*src); src->~T(); } static void move_init_n (T const* src, void* dst, size_t n) { // move n items onto uninitialized storage assert(src != dst); for (size_tk=0;k<n; ++k) { ::new((void*)((char*)dst+k)) T(src[k]); src[k].~T(); } } };
The term move here means that a
value is transferred from one place to another, and hence the original value no
longer exists (or, more precisely, the original location may have been
destroyed). The copy operation, on the other
hand, guarantees that both the source and destination locations have valid and
identical values. This should not be confused with the distinction between
memcpy() and memmove(), which is made in the standard C
library: In that case, move implies that the
source and destination areas may overlap, whereas for copy they do not. In our implementation of the CSM
traits, we always assume that the sources and destinations do not overlap. In an
industrial-strength library, a shift operation
should probably be added to express the policy for shifting objects within a
contiguous area of memory (the operation enabled by memmove()). We omit
it for the sake of simplicity.
The member functions of our policy traits template are all
static. This is almost always the case, because the member functions are meant
to apply to objects of the parameter type rather than objects of the traits
class type.
The other partial specialization implements the traits for
bitwise types that can be copied:
// traits/csm2.hpp #include <cstring> #include <cassert> #include <stddef.h> #include "csm1.hpp" // partial specialization for fast bitwise copying of objects template <typename T> class BitOrClassCSM<T,true> : public BitOrClassCSM<T,false> { public: static void copy_n (T const* src, T* dst, size_t n) { // copy n items onto n other ones std::memcpy((void*)dst, (void*)src, n); } static void copy_init_n (T const* src, void* dst, size_t n) { // copy n items onto uninitialized storage std::memcpy(dst, (void*)src, n); } static void move_n (T* src, T* dst, size_t n) { // move n items onto n other ones assert(src != dst); std::memcpy((void*)dst, (void*)src, n); } static void move_init_n (T const* src, void* dst, size_t n) { // move n items onto uninitialized storage assert(src != dst); std::memcpy(dst, (void*)src, n); } };
We used another level of inheritance to simplify the
implementation of the traits for bitwise types that can be copied. This is
certainly not the only possible implementation. In fact, for particular
platforms it may be desirable to introduce some inline assembly (for example, to
take advantage of hardware swap operations).
Afternotes
Nathan Myers was the first to formalize the idea of traits
parameters. He originally presented them to the C++ standardization committee as
a vehicle to define how character types should be treated in standard library
components (for example, input and output streams). At that time he called them
baggage templates and noted that they contained
traits. However, some C++ committee members did not like the term baggage, and the name traits was promoted instead. The latter term has been
widely used since then.
Client code usually does not deal with traits at all: The
default traits classes satisfy the most common needs, and because they are
default template arguments, they need not appear in the client source at all.
This argues in favor of long descriptive names for the default traits templates.
When client code does adapt the behavior of a template by providing a custom
traits argument, it is good practice to typedef the resulting specializations to
a name that is appropriate for the custom behavior. In this case the traits
class can be given a long descriptive name without sacrificing too much source
estate.
Our discussion has presented traits templates as being class
templates exclusively. Strictly speaking, this does not need to be the case. If
only a single policy trait needs to be provided, it could be passed as an
ordinary function template. For example:
template <typename T, void (*Policy)(T const&, T const&)> class X;
However, the original goal of traits was to reduce the baggage
of secondary template arguments, which is not achieved if only a single trait is
encapsulated in a template parameter. This justifies Myers's preference for the
term baggage as a collection of traits. We
revisit the problem of providing an ordering criterion in Chapter 22.
The standard library defines a class template
std::char_traits, which is used as a policy traits parameter. To adapt
algorithms easily to the kind of STL iterators for which they are used, a very
simple std::iterator_traits property traits template is provided (and
used in standard library interfaces). The template std::numeric_limits
can also be useful as a property traits template, but it is not visibly used in
the standard library proper. The class templates std::unary_function
and std::binary_function fall in the same category and are very simple
type functions: They only typedef their arguments to member names that make
sense for functors (also known as function
objects, see Chapter
22). Lastly, memory allocation for the standard container types is handled
using a policy traits class. The template std::allocator is provided as
the standard item for this purpose.
Policy classes have apparently been developed by many
programmers and a few authors. Andrei Alexandrescu made the term policy classes popular, and his book Modern C++ Design covers them in more detail than our
brief section (see [AlexandrescuDesign]).