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, and each additional template parameter complicates the contract between the component and its client.
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 that there exist default values that almost always fit the bill.
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.
Most of the traits presented here are available in the C++ standard library in some form. However, for clarity’s sake, we often present simplified implementations that omit some details present in industrial-strength implementations (like those of the standard library). For this reason, we also use our own naming scheme, which, however, maps easily to the standard traits.
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.
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
traits/accum1.hpp
#ifndef ACCUM_HPP
#define ACCUM_HPP
template<typename T>
T accum (T const* beg, T const* end)
{
T total{}; // assume this 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 value initialization (with the {
…}
notation) here as introduced in Section 5.2 on page 68. It means that the local object total
is initialized either by its default constructor or by zero (which means nullptr
for pointers and false
for Boolean values).
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, 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, 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(num0, 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 templates
(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:
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 small a range for the accumulation of even relatively small values. Clearly, we could resolve this 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,name+5)
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>
struct AccumulationTraits;
template<>
struct AccumulationTraits<char> {
using AccT = int;
};
template<>
struct AccumulationTraits<short> {
using AccT = int;
};
template<>
struct AccumulationTraits<int> {
using AccT = long;
};
template<>
struct AccumulationTraits<unsigned int> {
using AccT = unsigned long;
};
template<>
struct AccumulationTraits<float> {
using AccT = double;
};
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:3
traits/accum2.hpp
#ifndef ACCUM_HPP
#define ACCUM_HPP
#include "accumtraits2.hpp"
template<typename T>
auto accum (T const* beg, T const* end)
{
// return type is traits of the element type
using AccT = typename AccumulationTraits<T>::AccT;
AccT total{}; // assume this 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.
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{};
// assume this actually creates a zero value
…
return total;
Clearly, there is no guarantee that this produces a good value to start the accumulation loop. Type AccT
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>
struct AccumulationTraits;
template<>
struct AccumulationTraits<char> {
using AccT = int;
static AccT const zero = 0;
};
template<>
struct AccumulationTraits<short> {
using AccT = int;
static AccT const zero = 0;
};
template<>
struct AccumulationTraits<int> {
using AccT = long;
static AccT const zero = 0;
};
…
In this case, our new trait provides an zero
element as a constant that can be evaluated at compile time. Thus, accum()
becomes the following:
traits/accum3.hpp
#ifndef ACCUM_HPP
#define ACCUM_HPP
#include "accumtraits3.hpp"
template<typename T>
auto accum (T const* beg, T const* end)
{
// return type is traits of the element type
using AccT = typename AccumulationTraits<T>::AccT;
AccT total = AccumulationTraits<T>::zero; // init total by trait value
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 a static constant data member inside its class only if it has an integral or enumeration type.
constexpr
static data members are slightly more general, allowing floating-point types as well as other literal types:
template<>
struct AccumulationTraits<float> {
using Acct = float;
static constexpr float zero = 0.0f;
};
However, neither const
nor constexpr
permit nonliteral types to be initialized this way. For example, a user-defined arbitrary-precision BigInt
type might not be a literal type, because typically it has to allocate components on the heap, which usually precludes it from being a literal type, or just because the required constructor is not constexpr
. The following specialization is then an error:
class BigInt {
BigInt(long long);
…
};
…
template<>
struct AccumulationTraits<BigInt> {
using AccT = BigInt;
static constexpr BigInt zero = BigInt{0}; // ERROR: not a literal type
};
The straightforward alternative is not to define the value trait in its class:
template<>
struct AccumulationTraits<BigInt> {
using AccT = BigInt;
static BigInt const zero; // declaration only
};
The initializer then goes in a source file and looks something like the following:
BigInt const AccumulationTraits<BigInt>::zero = BigInt{0};
Although this works, it has the disadvantage of being more verbose (code must be added in two places), and it is potentially less efficient because compilers are typically unaware of definitions in other files.
In C++17, this can be addressed using inline variables:
template<>
struct AccumulationTraits<BigInt> {
using AccT = BigInt;
inline static BigInt const zero = BigInt{0}; // OK since C++17
};
An alternative that works prior to C++17 is to use inline member functions for value traits that won’t always yield integral values. Again, such a function can be declared constexpr
if it returns a literal type.4
For example, we could rewrite AccumulationTraits
as follows:
traits/accumtraits4.hpp
template<typename T>
struct AccumulationTraits;
template<>
struct AccumulationTraits<char> {
using AccT = int;
static constexpr AccT zero() {
return 0;
}
};
template<>
struct AccumulationTraits<short> {
using AccT = int;
static constexpr AccT zero() {
return 0;
}
};
template<>
struct AccumulationTraits<int> {
using AccT = long;
static constexpr AccT zero() {
return 0;
}
};
template<>
struct AccumulationTraits<unsigned int> {
using AccT = unsigned long;
static constexpr AccT zero() {
return 0;
}
};
template<>
struct AccumulationTraits<float> {
using AccT = double;
static constexpr AccT zero() {
return 0;
}
};
…
and then extend these traits for our own types:
traits/accumtraits4bigint.hpp
template<>
struct AccumulationTraits<BigInt> {
using AccT = BigInt;
static BigInt zero() {
return BigInt{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(); // init total by trait function
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 to the notion of traits: Traits provide an avenue to configure concrete elements (mostly types) for generic computations.
The use of traits in accum()
in the previous sections is called fixed, because once the decoupled trait is defined, it cannot be replaced 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.
We can address this problem by adding a template parameter AT
for the trait itself having a default value determined by our traits template:
traits/accum5.hpp
#ifndef ACCUM_HPP
#define ACCUM_HPP
#include "accumtraits4.hpp"
template<typename T, typename AT = AccumulationTraits<T>>
auto accum (T const* beg, T const* end)
{
typename AT::AccT total = AT::zero();
while (beg != end) {
total += *beg;
++beg;
}
return total;
}
#endif //ACCUM_HPP
In this way, many users can omit the extra template argument, but those with more exceptional needs can specify an alternative to the preset accumulation type. 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 deduced for the first argument.
So far we have equated accumulation with summation. However, 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 += *beg
. This operation can be called a policy of our accumulation process.
Here is an example of how we could introduce such a policy in our accum()
function 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>>
auto accum (T const* beg, T const* end)
{
using AccT = typename Traits::AccT;
AccT total = Traits::zero();
while (beg != end) {
Policy::accumulate(total, *beg);
++beg;
}
return total;
}
#endif //ACCUM_HPP
In this version of accum() SumPolicy
is a policy class, that is, a class that implements one or more policies for an algorithm through an agreed-upon interface.5 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
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/accum6.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>(num, 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 std::accumulate()
function of the C++ standard library takes the initial value as a third (function call) argument.
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 advantageous 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
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 (i.e., 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, we can define string classes that behave in a case-insensitive manner (see Section 13.2.15 in [JosuttisStdLib]) while keeping the same character type. Thus, although they are called traits, they have some properties associated with policies.
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 (see Section 5.7 on page 83 and Section 12.2.3 on page 187). 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/accum7.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>>
auto accum (T const* beg, T const* end)
{
using AccT = typename Traits::AccT;
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 (i.e., 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 can make the expression of the traits themselves more verbose and less natural than a simple nontemplate class.
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 21.4 on page 512 for details.
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 iterator traits. (Traits are everywhere!) Thus, we could have defined our initial version of accum()
as follows (ignoring our later refinements):7
traits/accum0.hpp
#ifndef ACCUM_HPP
#define ACCUM_HPP
#include <iterator>
template<typename Iter>
auto accum (Iter start, Iter end)
{
using VT = typename std::iterator_traits<Iter>::value_type;
VT total{}; // assume this actually creates a zero value
while (start != end) {
total += *start;
++start;
}
return total;
}
#endif //ACCUM_HPP
The std::iterator_traits
structure encapsulates all the relevant properties of iterators. 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*> {
using difference_type = ptrdiff_t;
using value_type = T;
using pointer = T*;
using reference = T&;
using iterator_category = random_access_iterator_tag ;
};
}
However, there is no type for the accumulation of values to which an iterator refers; hence we still need to design our own AccumulationTraits
.
The initial traits example demonstrates that we can define behavior that depends on types. Traditionally, in C and C++, we define functions that could more specifically be called value functions: They take some values as arguments and return another value as a result. With templates, we can additionally define type functions: functions that takes some type as arguments and produce a type or a 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 <cstddef>
#include <iostream>
template<typename T>
struct TypeSize {
static std::size_t const value = sizeof(T);
};
int main()
{
std::cout << "TypeSize<int>::value = "
<< TypeSize<int>::value << ’\n’;
}
This may not seem very useful, since we have the built-in sizeof
operator available, but note that TypeSize<T>
is a type, and it can therefore be passed as a class template argument itself. Alternatively, TypeSize
is a template and can be passed as a template template argument.
In what follows, we develop a few more general-purpose type functions that can be used as traits classes in this way.
Assume that we have a number of container templates, such as std::vector<>
and std::list<>
, as well as built-in arrays. We want a type function that, given such a container type, produces the element type. This can be achieved using partial specialization:
traits/elementtype.hpp
#include <vector>
#include <list>
template<typename T>
struct ElementT; // primary template
template<typename T>
struct ElementT<std::vector<T>> { //partial specialization for std::vector
using Type = T;
};
template<typename T>
struct ElementT<std::list<T>> { //partial specialization for std::list
using Type = T;
};
…
template<typename T, std::size_t N>
struct ElementT<T[N]> { //partial specialization for arrays of known bounds
using Type = T;
};
template<typename T>
struct ElementT<T[]> { //partial specialization for arrays of unknown bounds
using Type = T;
};
…
Note that we should provide partial specializations for all possible array types (see Section 5.4 on page 71 for details).
We can use the type function as follows:
traits/elementtype.cpp
#include "elementtype.hpp"
#include <vector>
#include <iostream>
#include <typeinfo>
template<typename T>
void printElementType (T const& c)
{
std::cout << "Container of "
<< typeid(typename ElementT<T>::Type).name()
<< " elements.\n";
}
int main()
{
std::vector<bool> s;
printElementType(s);
int arr[42];
printElementType(arr);
}
The use of partial specialization allows us to implement the type function without requiring the container types to know about it. 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>
struct ElementT {
using Type = typename C::value_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 member type definitions for class template type parameters so that they can be accessed more easily in generic code (like the standard container templates do). The following sketches the idea:
template<typename T1, typename T2, …>
class X {
public:
using … = T1;
using … = 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 sumOfElements (C const& c);
which requires syntax like sumOfElements<int>(list)
to specify the element type explicitly, we can declare
template<typename C>
typename ElementT<C>::Type sumOfElements (C const& c);
where the element type is determined from the type function.
Observe how the traits are implemented as an extension to existing types; that is, we 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.”
As a convenience, we can create an alias template for type functions. For example, we could introduce
template<typename T>
using ElementType = typename ElementT<T>::Type;
which allows us to further simplify the declaration of sumOfElements
above to
template<typename C>
ElementType<C> sumOfElements (C const& c);
In addition to providing access to particular aspects of a main parameter type, traits can also perform transformations on types, such as adding or removing references or const
and volatile
qualifiers.
For example, we can implement a RemoveReferenceT
trait that turns reference types into their underlying object or function types, and leaves nonreference types alone:
traits/removereference.hpp
template<typename T>
struct RemoveReferenceT {
using Type = T;
};
template<typename T>
struct RemoveReferenceT<T&> {
using Type = T;
};
template<typename T>
struct RemoveReferenceT<T&&> {
using Type = T;
};
Again, a convenience alias template makes the usage simpler:
template<typename T>
using RemoveReference = typename RemoveReference<T>::Type;
Removing the reference from a type is typically useful when the type was derived using a construct that sometimes produces reference types, such as the special deduction rule for function parameters of type T&&
discussed in Section 15.6 on page 277.
The C++ standard library provides a corresponding type trait std::remove_reference<>
, which is described in Section D.4 on page 729.
Similarly, we can take an existing type and create an lvalue or rvalue reference from it (along with the usual convenience alias templates):
traits/addreference.hpp
template<typename T>
struct AddLValueReferenceT {
using Type = T&;
};
template<typename T>
using AddLValueReference = typename AddLValueReferenceT<T>::Type;
template<typename T>
struct AddRValueReferenceT {
using Type = T&&;
};
template<typename T>
using AddRValueReference = typename AddRValueReferenceT<T>::Type;
The rules of reference collapsing (Section 15.6 on page 277) apply here. For example, calling AddLValueReference<int&&>
produces type int&
(there is therefore no need to implement them manually via partial specialization).
If we leave AddLValueReferenceT
and AddRValueReferenceT
as they are and do not introduce specializations of them, then the convenience aliases can actually be simplified to
template<typename T>
using AddLValueReferenceT = T&;
template<typename T>
using AddRValueReferenceT = T&&;
which can be instantiated without instantiating a class template (and is therefore a lighter-weight process). However, this is risky, as we may well want to specialize these template for special cases. For example, as written above, we cannot use void
as a template argument for these templates. A few explicit specializations can take care of that:
template<>
struct AddLValueReferenceT<void> {
using Type = void;
};
template<>
struct AddLValueReferenceT<void const> {
using Type = void const;
};
template<>
struct AddLValueReferenceT<void volatile> {
using Type = void volatile;
};
template<>
struct AddLValueReferenceT<void const volatile> {
using Type = void const volatile;
};
and similarly for AddRValueReferenceT
.
With that in place, the convenience alias template must be formulated in terms of the class templates to ensure that the specializations are picked up also (since alias templates cannot be specialized).
The C++ standard library provides corresponding type traits std::add_lvalue_reference<>
and std::add_rvalue_reference<>
, which are described in Section D.4 on page 729. The standard templates include the specializations for void
types.
Transformation traits can break down or introduce any kind of compound type, not just references. For example, we can remove a const
qualifier if present:
traits/removeconst.hpp
template<typename T>
struct RemoveConstT {
using Type = T;
};
template<typename T>
struct RemoveConstT<T const> {
using Type = T;
};
template<typename T>
using RemoveConst = typename RemoveConstT<T>::Type;
Moreover, transformation traits can be composed, such as creating a RemoveCVT
trait that removes both const
and volatile
:
traits/removecv.hpp
#include "removeconst.hpp"
#include "removevolatile.hpp"
template<typename T>
struct RemoveCVT : RemoveConstT<typename RemoveVolatileT<T>::Type> {
};
template<typename T>
using RemoveCV = typename RemoveCVT<T>::Type;
There are two things to note with the definition of RemoveCVT
. First, it is making use of both RemoveConstT
and the related RemoveVolatileT
, first removing the volatile
(if present) and then passing the resulting type to RemoveConstT
.8 Second, it is using metafunction forwarding to inherit the Type
member from RemoveConstT
rather than declaring its own Type
member that is identical to the one in the RemoveConstT
specialization. Here, metafunction forwarding is used simply to reduce the amount of typing in the definition of RemoveCVT
. However, metafunction forwarding is also useful when the metafunction is not defined for all inputs, a technique that will be discussed further in Section 19.4 on page 416.
The convenience alias template RemoveCV
could be simplified to
template<typename T>
using RemoveCV = RemoveConst<RemoveVolatile<T>>;
Again, this works only if RemoveCVT
is not specialized. Unlike in the case of AddLValueReference
and AddRValueReference
, we cannot think of any reasons for such specializations.
The C++ standard library also provides corresponding type traits std::remove_volatile<>
, std::remove_const<>
, and std::remove_cv<>
, which are described in Section D.4 on page 728.
To round out our discussion of transformation traits, we develop a trait that mimics type conversions when passing arguments to parameters by value. Derived from C, this means that the arguments decay (turning array types into pointers and function types into pointer-to-function types; see Section 7.4 on page 115 and Section 11.1.1 on page 159) and delete any top-level const
, volatile
, or reference qualifiers (because top-level type qualifiers on parameter types are ignored when resolving a function call).
The effect of this pass-by-value can be seen in the following program, which prints the actual parameter type produced after the compiler decays the specified type:
traits/passbyvalue.cpp
#include <iostream>
#include <typeinfo>
#include <type_traits>
template<typename T>
void f(T)
{
}
template<typename A>
void printParameterType(void (*)(A))
{
std::cout << "Parameter type: " << typeid(A).name() << ’\n’;
std::cout << "- is int: " <<std::is_same<A,int>::value << ’\n’;
std::cout << "- is const: " <<std::is_const<A>::value << ’\n’;
std::cout << "- is pointer: " <<std::is_pointer<A>::value << ’\n’;
}
int main()
{
printParameterType(&f<int>);
printParameterType(&f<int const>);
printParameterType(&f<int[7]>);
printParameterType(&f<int(int)>);
}
In the output of the program, the int
parameter has been left unchanged, but the int const
, int[7]
, and int(int)
parameters have decayed to int
, int*
, and int(*)(int)
, respectively.
We can implement a trait that produces the same type conversion of passing by value. To match to the C++ standard library trait std::decay
, we name it DecayT
.9 Its implementation combines several of the techniques described above First, we define the nonarray, nonfunction case, which simply deletes any const
and volatile
qualifiers:
template<typename T>
struct DecayT : RemoveCVT<T> {
};
Next, we handle the array-to-pointer decay, which requires us to recognize any array types (with or without a bound) using partial specialization:
template<typename T>
struct DecayT<T[]> {
using Type = T*;
};
template<typename T, std::size_t N>
struct DecayT<T[N]> {
using Type = T*;
};
Finally, we handle the function-to-pointer decay, which has to match any function type, regardless of the return type or the number of parameter types. For this, we employ variadic templates:
template<typename R, typename… Args>
struct DecayT<R(Args…)> {
using Type = R (*)(Args…);
};
template<typename R, typename… Args>
struct DecayT<R(Args…, …)> {
using Type = R (*)(Args…, …);
};
Note that the second partial specialization matches any function type that uses C-style varargs.10 Together, the primary DecayT
template and its four partial specialization implement parameter type decay, as illustrated by this example program:
traits/decay.cpp
#include <iostream>
#include <typeinfo>
#include <type_traits>
#include "decay.hpp"
template<typename T>
void printDecayedType()
{
using A = typename DecayT<T>::Type;
std::cout << "Parameter type: " << typeid(A).name() << ’\n’;
std::cout << "- is int: " << std::is_same<A,int>::value << ’\n’;
std::cout << "- is const: " << std::is_const<A>::value << ’\n’;
std::cout << "- is pointer: " << std::is_pointer<A>::value << ’\n’;
}
int main()
{
printDecayedType<int>();
printDecayedType<int const>();
printDecayedType<int[7]>();
printDecayedType<int(int)>();
}
As usual, we provide a convenience alias template:
template typename T>
using Decay = typename DecayT<T>::Type;
As written, the C++ standard library also provides a corresponding type traits std::decay<>
, which is described in Section D.4 on page 731.
So far we have studied and developed type functions of a single type: Given one type, provide other related types or constants. In general, however, we can develop type functions that depend on multiple arguments. This also leads to a special form of type traits, type predicates (type functions yielding a Boolean value).
The IsSameT
trait yields whether two types are equal:
traits/issame0.hpp
template<typename T1, typename T2>
struct IsSameT {
static constexpr bool value = false;
};
template<typename T>
struct IsSameT<T, T> {
static constexpr bool value = true;
};
Here the primary template defines that, in general, two different types passed as template arguments differ. so that the value
member is false
. However, using partial specialization, when we have the special case that the two passed types are the same, value
is true
.
For example, the following expression checks whether a passed template parameters is an integer:
if (IsSameT<T, int>::value) …
For traits that produce a constant value, we cannot provide an alias template, but we can provide a constexpr
variable template that fulfills the same role:
template<typename T1, typename T2>
constexpr bool isSame = IsSameT<T1, T2>::value;
The C++ standard library provides a corresponding type trait std::is_same<>
, which is described in Section D.3.3 on page 726
We can significantly improve the definition of IsSameT
by providing different types for the possible two outcomes, true
and false
. In fact, if we declare a class template BoolConstant
with the two possible instantiations TrueType
and FalseType
:
traits/boolconstant.hpp
template<bool val>
struct BoolConstant {
using Type = BoolConstant<val>;
static constexpr bool value = val;
};
using TrueType = BoolConstant<true>;
using FalseType = BoolConstant<false>;
we can define IsSameT
so that, depending on whether the two types match, it derives from TrueType
or FalseType
:
traits/issame.hpp
#include "boolconstant.hpp"
template<typename T1, typename T2>
struct IsSameT : FalseType
{
};
template<typename T>
struct IsSameT<T, T> : TrueType
{
};
IsSameT<T,int>
implicitly converts to its base class TrueType
or FalseType
, which not only provides the corresponding value
member but also allows us to dispatch to different function implementations or partial class template specializations at compile time. For example:
traits/issame.cpp
#include "issame.hpp"
#include <iostream>
template<typename T>
void fooImpl(T, TrueType)
{
std::cout << "fooImpl(T,true) for int called\n";
}
template<typename T>
void fooImpl(T, FalseType)
{
std::cout << "fooImpl(T,false) for other type called\n";
}
template<typename T>
void foo(T t)
{
fooImpl(t, IsSameT<T,int>{}); // choose impl. depending on whether T is int
}
int main()
{
foo(42); // calls fooImpl(42, TrueType)
foo(7.7); // calls fooImpl(42, FalseType)
}
This technique is called tag dispatching and is introduced in Section 20.2 on page 467.
Note that our BoolConstant
implementation includes a Type
member, which allows us to reintroduce an alias template for IsSameT
:
template<typename T>
using IsSame = typename IsSameT<T>::Type;
That alias template can coexist with the variable template isSame
.
In general, traits yielding Boolean values should support tag dispatching by deriving from types such as TrueType
and FalseType
. However, to be as generic as possible, there should be only one type representing true
and one type representing false
instead of having each generic library defining its own types for Boolean constants.
Fortunately, the C++ standard library provides corresponding types in <type_traits>
since C++11: std::true_type and std::false_type. In C++11 and C++14, they are defined as follows:
namespace std {
using true_type = integral_constant<bool, true>;
using false_type = integral_constant<bool, false>;
}
Since C++17, they are defined as
namespace std {
using true_type = bool_constant<true>;
using false_type = bool_constant<false>;
}
where bool_constant
is defined in namespace std
as
template<bool B>
using bool_constant = integral_constant<bool, B>;
See Section D.1.1 on page 699 for further details.
For this reason, we use std::true_type
and std::false_type
directly for the rest of this book, especially when defining type predicates.
Another example of type functions that deal with multiple types are result type traits. They are very useful when writing operator templates. 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&);
Besides the different approaches introduced in Section 1.3 on page 9, a result type template allows us to fill in the question marks in the previous declaration as follows:
template<typename T1, typename T2>
Array<typename PlusResultT<T1, T2>::Type>
operator+ (Array<T1> const&, Array<T2> const&);
or, if we assume the availability of a convenience alias template,
template<typename T1, typename T2>
Array<PlusResult<T1, T2>>
operator+ (Array<T1> const&, Array<T2> const&);
The PlusResultT
trait determines the type produced by adding values of two (possibly different) types with the +
operator:
traits/plus1.hpp
template<typename T1, typename T2>
struct PlusResultT {
using Type = decltype(T1() + T2());
};
template<typename T1, typename T2>
using PlusResult = typename PlusResultT<T1, T2>::Type;
This trait template uses decltype
to compute the type of the expression T1() + T2()
, leaving the hard work of determining the result type (including handling promotion rules and overloaded operators) to the compiler.
However, for the purpose of our motivating example, decltype
actually preserves too much information (see Section 15.10.2 on page 298 for a description of decltype
’s behavior). For example, our formulation of PlusResultT
may produce a reference type, but most likely our Array
class template is not designed to handle reference types. More realistically, an overloaded operator+
might return a value of const
class type:
class Integer { … };
Integer const operator+ (Integer const&, Integer const&);
Adding two Array<Integer>
values will result in an array of Integer const
, which is most likely not what we intended. In fact, what we want is to transform the result type by removing references and qualifiers, as discussed in the previous section:
template<typename T1, typename T2>
Array<RemoveCV<RemoveReference<PlusResult<T1, T2>>>>
operator+ (Array<T1> const&, Array<T2> const&);
Such nesting of traits is common in template libraries and is often used in the context of metaprogramming. Metaprogramming will be covered in detail in Chapter 23. (The convenience alias templates are particularly helpful with multilevel nesting like this. Without them, we’d have to add a typename
and a ::Type
suffix at every level.)
At this point, the array addition operator properly computes the result type when adding two arrays of (possibly different) element types. However, our formulation of PlusResultT
places an undesirable restriction on the element types T1
and T2
: Because the expression T1() + T2()
attempts to value-initialize values of types T1
and T2
, both of these types must have an accessible, nondeleted, default constructor (or be nonclass types). The Array
class itself might not otherwise require value-initialization of its element type, so this is an additional, unnecessary restriction.
Fortunately, it is fairly easy to produce values for the +
expression without requiring a constructor, by using a function that produces values of a given type T
. For this, the C++ standard provides std::declval<>
, as introduced in Section 11.2.3 on page 166. It is defined in <utility>
simply as follows:
namespace std {
template<typename T>
add_rvalue_reference_t<T> declval() noexcept;
}
The expression declval<T>()
produces a value of type T
without requiring a default constructor (or any other operation).
This function template is intentionally left undefined because it’s only meant to be used within decltype
, sizeof
, or some other context where no definition is ever needed. It has two other interesting attributes:
• For referenceable types, the return type is always an rvalue reference to the type, which allows declval
to work even with types that could not normally be returned from a function, such as abstract class types (classes with pure virtual functions) or array types. The transformation from T
to T&&
otherwise has no practical effect on the behavior of declval<T>()
when used as an expression: Both are rvalues (if T
is an object type), while lvalue reference types are unchanged due to reference collapsing (described in Section 15.6 on page 277).11
• The noexcept
exception specification documents that declval
itself does not cause an expression to be considered to throw exceptions. It becomes useful when declval
is used in the context of the noexcept
operator (Section 19.7.2 on page 443).
With declval
, we can eliminate the value-initialization requirement for PlusResultT
:
traits/plus2.hpp
#include <utility>
template<typename T1, typename T2>
struct PlusResultT {
using Type = decltype(std::declval<T1>() + std::declval<T2>());
};
template<typename T1, typename T2>
using PlusResult = typename PlusResultT<T1, T2>::Type;
Result type traits offer a way to determine the precise return type of a particular operation and are often useful when describing the result types of function templates.
The SFINAE principle (substitution failure is not an error; see Section 8.4 on page 129 and Section 15.7 on page 284) turns potential errors during the formation of invalid types and expressions during template argument deduction (which would cause the program to be ill-formed) into simple deduction failures, allowing overload resolution to select a different candidate. While originally intended to avoid spurious failures with function template overloading, SFINAE also enables remarkable compile-time techniques that can determine if a particular type or expression is valid. This allows us to write traits that determine, for example, whether a type has a specific member, supports a specific operation, or is a class.
The two main approaches for SFINAE-based traits are to SFINAE out functions overloads and to SFINAE out partial specializations.
Our first foray into SFINAE-based traits illustrates the basic technology using SFINAE with function overloading to find out whether a type is default constructible, so that you can create objects without any value for initialization. That is, for a given type T
, an expression such as T()
has to be valid.
A basic implementation can look as follows:
traits/isdefaultconstructible1.hpp
#include "issame.hpp"
template<typename T>
struct IsDefaultConstructibleT {
private:
// test() trying substitute call of a default constructor for T passed as U :
template<typename U, typename = decltype(U())>
static char test(void*);
// test() fallback:
template<typename>
static long test(…);
public:
static constexpr bool value
= IsSameT<decltype(test<T>(nullptr)), char>::value;
};
The usual approach to implement a SFINAE-base trait with function overloading is to declare two overloaded function templates named test()
with different return types:
template<…> static char test(void*);
template<…> static long test(…);
The first overload is designed to match only if the requested check succeeds (we will discuss below how that is achieved). The second overload is the fallback:12 It always matches the call, but because it matches “with ellipsis” (i.e., a vararg parameter), any other match would be preferred (see Section C.2 on page 682).
Our “return value” value
depends on which overloaded test
member is selected:
static constexpr bool value
= IsSameT<decltype(test<…>(nullptr)), char>::value;
If the first test()
member—whose return type is char
—is selected, value
will be initialized to isSame<char,char>
, which is true
. Otherwise, it will be initialized to isSame<long,char>
, which is false
.
Now, we have to deal with the specific properties we want to test. The goal is to make the first test()
overload valid if and only if the condition we want to check applies. In this case, we want to find out whether we can default construct an object of the passed type T
. To achieve this, we pass T
as U
and give our first declaration of test()
a second unnamed (dummy) template argument initialized with an construct that is valid if and only if the conversion is valid. In this case, we use the expression that can only be valid if an implicit or explicit default constructor exists: U(). The expression is surrounded by decltype to make this a valid expression to initialize a type parameter.
The second template parameter cannot be deduced, as no corresponding argument is passed. And we will not provide an explicit template argument for it. Therefore, it will be substituted, and if the substitution fails, according to SFINAE, that declaration of test()
will be discarded so that only the fallback declaration will match.
Thus, we can use the trait as follows:
IsDefaultConstructibleT<int>::value //yields true
struct S {
S() = delete;
};
IsDefaultConstructibleT<S>::value //yields false
Note that we can’t use the template parameter T
in the first test()
directly:
template<typename T>
struct IsDefaultConstructibleT {
private:
// ERROR: test() uses T directly:
template<typename, typename = decltype(T())>
static char test(void*);
// test() fallback:
template<typename>
static long test(…);
public:
static constexpr bool value
= IsSameT<decltype(test<T>(nullptr)), char>::value;
};
This doesn’t work, however, because for any T
, always, all member functions are substituted, so that for a type that isn’t default constructible, the code fails to compile instead of ignoring the first test()
overload. By passing the class template parameter T
to a function template parameter U
, we create a specific SFINAE context only for the second test()
overload.
SFINAE-based traits have been possible to implement since before the first C++ standard was published in 1998.13 The key to the approach always consisted in declaring two overloaded function templates returning different return types:
template<…> static char test(void*);
template<…> static long test(…);
However, the original published technique14 used the size of the return type to determine which overload was selected (also using 0
and enum
, because nullptr
and constexpr
were not available):
enum { value = sizeof(test<…>(0)) == 1 };
On some platforms, it might happen that sizeof(char)==sizeof(long)
. For example, on digital signal processors (DSP) or old Cray machines, all integral fundamental types could have the same size. As by definition sizeof(char)
equals 1, on those machines sizeof(long)
and even sizeof(long long)
also equal 1.
Given that observation, we want to ensure that the return types of the test()
functions have different sizes on all platforms. For example, after defining
using Size1T = char;
using Size2T = struct { char a[2]; };
or
using Size1T = char(&)[1];
using Size2T = char(&)[2];
we could define test test()
overloads as follows:
template<…> static Size1T test(void*); // checking test()
template<…> static Size2T test(…); // fallback
Here, we either return a Size1T
, which is a single char
of size 1, or we return (a structure of) an array of two char
s, which has a size of at least 2 on all platforms.
Code using one of these approaches is still commonly found.
Note also that the type of the call argument passed to func()
doesn’t matter. All that matters is that the passed argument matches the expected type. For example, you could also define to pass the integer 42:
template<…> static Size1T test(int); // checking test()
template<…> static Size2T test(…); // fallback
…
enum { value = sizeof(test<…>(42)) == 1 };
As introduced in Section 19.3.3 on page 410, a predicate trait, which returns a Boolean value, should return a value derived from std::true_type
or std::false_type
. This way, we can also solve the problem that on some platforms sizeof(char)==sizeof(long)
.
For this, we need an indirect definition of IsDefaultConstructibleT
. The trait itself should derive from the Type
of a helper class, which yields the necessary base class. Fortunately, we can simply provide the corresponding base classes as return types of the test()
overloads:
template<…> static std::true_type test(void*); // checking test()
template<…> static std::false_type test(…); // fallback
That way, the Type
member for the base class simply can be declared as follows:
using Type = decltype(test<FROM>(nullptr));
and we no longer need the IsSameT
trait.
The complete improved implementation of IsDefaultConstructibleT
therefore becomes as follows:
traits/isdefaultconstructible2.hpp
#include <type_traits>
template<typename T>
struct IsDefaultConstructibleHelper {
private:
// test() trying substitute call of a default constructor for T passed as U:
template<typename U, typename = decltype(U())>
static std::true_type test(void*);
// test() fallback:
template<typename>
static std::false_type test(…);
public:
using Type = decltype(test<T>(nullptr));
};
template<typename T>
struct IsDefaultConstructibleT : IsDefaultConstructibleHelper<T>::Type {
};
Now, if the first test()
function template is valid, it is the preferred overload, so that the member IsDefaultConstructibleHelper::Type
is initialized by its return type std::true_type
. As a consequence, IsConvertibleT<
… >
derives from std::true_type
.
If the first test()
function template is not valid, it becomes disabled due to SFINAE, and IsDefaultConstructibleHelper::Type
is initialized by the return type of the test()
fall-back, that is, std::false_type
. The effect is that IsConvertibleT<
… >
then derives from std::false_type
.
The second approach to implement SFINAE-based traits uses partial specialization. Again, we can use the example to find out whether a type T
is default constructible:
traits/isdefaultconstructible3.hpp
#include "issame.hpp"
#include <type_traits> //defines true_type and false_type
// helper to ignore any number of template parameters:
template<typename…> using VoidT = void;
// primary template:
template<typename, typename = VoidT<>>
struct IsDefaultConstructibleT : std::false_type
{
};
// partial specialization (may be SFINAE’d away):
template<typename T>
struct IsDefaultConstructibleT<T, VoidT<decltype(T())>> : std::true_type
{
};
As with the improved version of IsDefaultConstructibleT
for predicate traits above, we define the general case to be derived from std::false_type
, because by default a type doesn’t have the member size_type
.
The interesting feature here is the second template argument that defaults to the type of a helper VoidT
. It enables us to provide partial specializations that use an arbitrary number of compile-time type constructs.
In this case, we need only one construct:
decltype(T())
to check again whether the default constructor for T
is valid. If, for a specific T
, the construct is invalid, SFINAE this time causes the whole partial specialization to be discarded, and we fall back to the primary template. Otherwise, the partial specialization is valid and preferred.
In C++17, the C++ standard library introduced a type trait std::void_t<>
that corresponds to the type VoidT
introduced here. Before C++17, it might be helpful to define it ourselves as above or even in namespace std
as follows:15
#include <type_traits>
#ifndef __cpp_lib_void_t
namespace std {
template<typename…> using void_t = void;
}
#endif
Starting with C++14, the C++ standardization committee has recommended that compilers and standard libraries indicate which parts of the standard they have implemented by defining agreed-upon feature macros. This is not a requirement
for standard conformance, but implementers typically follow the recommendation to be helpful to their users.16 The macro __cpp_lib_void_t
is the macro recommended to indicate that a library implements std::void_t
, and thus our code above is made conditional on it.
Obviously, this way to define a type trait looks more condensed that the first approach to overload function templates. But it requires the ability to formulate the condition inside the declaration of a template parameter. Using a class template with function overloads enables us to use additional helper functions or helper types.
Whichever technique we use, some boilerplate code is always needed to define traits: overloading and calling two test()
member functions or implementing multiple partial specializations. Next, we will show how in C++17, we can minimize this boilerplate by specifying the condition to be checked in a generic lambda.17
To start with, we introduce a tool constructed from two nested generic lambda expressions:
traits/isvalid.hpp
#include <utility>
// helper: checking validity of f (args…) for F f and Args… args:
template<typename F, typename… Args,
typename = decltype(std::declval<F>()(std::declval<Args&&>()…))>
std::true_type isValidImpl(void*);
// fallback if helper SFINAE’d out:
template<typename F, typename… Args>
std::false_type isValidImpl(…);
// define a lambda that takes a lambda f and returns whether calling f with args is valid
inline constexpr
auto isValid = [](auto f) {
return [](auto&&… args) {
return decltype(isValidImpl<decltype(f),
decltype(args)&&…
>(nullptr)){};
};
};
// helper template to represent a type as a value
template<typename T>
struct TypeT {
using Type = T;
};
// helper to wrap a type as a value
template<typename T>
constexpr auto type = TypeT<T>{};
// helper to unwrap a wrapped type in unevaluated contexts
template<typename T>
T valueT(TypeT<T>); // no definition needed
Let’s start with the definition of isValid
: It is a constexpr
variable whose type is a lambda’s closure type. The declaration must necessarily use a placeholder type (auto
in our code) because C++ has no way to express closure types directly. Prior to C++17, lambda expressions could not appear in constant-expressions, which is why this code is only valid in C++17. Since isValid
has a closure type, it can be invoked (i.e., called), but the item that it returns is itself an object of a lambda closure type, produced by the inner lambda expression.
Before delving into the details of that inner lambda expression, let’s examine a typical use of isValid
:
constexpr auto isDefaultConstructible
= isValid([](auto x) -> decltype((void)decltype(valueT(x))() {
});
We already know that isDefaultConstructible
has a lambda closure type and, as the name suggests, it is a function object that checks the trait of a type being default-constructible (we’ll see why in what follows). In other words, isValid
is a traits factory: A component that generates traits checking objects from its argument.
The type
helper variable template allows us to represent a type as a value. A value x
obtained that way can be turned back into the original type with decltype(valueT(x))
18, and that’s exactly what is done in the lambda passed to isValid
above. If that extracted type cannot be default-constructed, decltype(valueT(x))()
is invalid, and we will either get a compiler error, or an associated declaration will be “SFINAE’d out” (and the latter effect is what we’ll achieve thanks to the details of the definition of isValid
).
isDefaultConstructible
can be used as follows:
isDefaultConstructible(type<int>) //true (int is default-constructible)
isDefaultConstructible(type<int&>) //false (references are not default-constructible)
To see how all the pieces work together, consider what the inner lambda expression in isValid
becomes with isValid
’s parameter f
bound to the generic lambda argument specified in the definition of isDefaultConstructible
. By performing the substitution in isValid
’s definition, we get the equivalent of:19
constexpr auto isDefaultConstructible
= [](auto&&… args) {
return decltype(
isValidImpl<
decltype([](auto x)
-> decltype((void)decltype(valueT(x))())),
decltype(args)&&…
>(nullptr)){};
};
If we look back at the first declaration of isValidImpl()
above, we note that it includes a default template argument of the form
decltype(std::declval<F>()(std::declval<Args&&>()…))>
which attempts to invoke a value of the type of its first template argument, which is the closure type of the lambda in the definition of isDefaultConstructible
, with values of the types of the arguments (decltype(args)&&…
) passed to isDefaultConstructible
. As there is only one parameter x
in the lambda, args
must expand to only one argument; in our static_assert
examples above, that argument has type TypeT<int>
or TypeT<int&>
. In the TypeT<int&>
case, decltype(valueT(x))
is int&
which makes decltype(valueT(x))()
invalid, and thus the substitution of the default template argument in the first declaration of isValidImpl()
fails and is SFINAE’d out. That leaves just the second declaration (which would otherwise be a lesser match), which produces a false_type
value. Overall, isDefaultConstructible
produces false_type
when type<int&>
is passed. If instead type<int>
is passed, the substitution does not fail, and the first declaration of isValidImpl()
is selected, producing a true_type
value.
Recall that for SFINAE to work, substitutions have to happen in the immediate context of the substituted templates. In this case, the substituted templates are the first declaration of isValidImpl
and the call operator of the generic lambda passed to isValid
. Therefore, the construct to be tested has to appear in the return type of that lambda, not in its body!
Our isDefaultConstructible
trait is a little different from previous traits implementations in that it requires function-style invocation instead of specifying template arguments. That is arguably a more readable notation, but the prior style can be obtained also:
template<typename T>
using IsDefaultConstructibleT
= decltype(isDefaultConstructible(std::declval<T>()));
Since this is a traditional template declaration, however, it can only appear in namespace scope, whereas the definition of isDefaultConstructible
could conceivably have been introduced in block scope.
So far, this technique might not seem compelling because both the expressions involved in the implementation and the style of use are more complex than the previous techniques. However, once isValid
is in place and understood, many traits can be implement with just one declaration. For example, testing for access to a member named first
, is rather clean (see Section 19.6.4 on page 438 for the complete example):
constexpr auto hasFirst
= isValid([](auto x) -> decltype((void)valueT(x).first) {
});
In general, a type trait should be able to answer a particular query without causing the program to become ill-formed. SFINAE-based traits address this problem by carefully trapping potential problems within a SFINAE context, turning those would-be errors into negative results.
However, some traits presented thus far (such as the PlusResultT
trait described in Section 19.3.4 on page 413) do not behave well in the presence of errors. Recall the definition of PlusResultT
from that section:
traits/plus2.hpp
#include <utility>
template<typename T1, typename T2>
struct PlusResultT {
using Type = decltype(std::declval<T1>() + std::declval<T2>());
};
template<typename T1, typename T2>
using PlusResult = typename PlusResultT<T1, T2>::Type;
In this definition, the +
is used in a context that is not protected by SFINAE. Therefore, if a program attempts to evaluate PlusResultT
for types that do not have a suitable +
operator, the evaluation of PlusResultT
itself will cause the program to become ill-formed, as in the following attempt to declare the return type of adding arrays of unrelated types A
and B
:20
template<typename T>
class Array {
…
};
// declare + for arrays of different element types:
template<typename T1, typename T2>
Array<typename PlusResultT<T1, T2>::Type>
operator+ (Array<T1> const&, Array<T2> const&);
Clearly, using PlusResultT<>
here will lead to an error if no corresponding operator +
is defined for the array element.
class A {
};
class B {
};
void addAB(Array<A> arrayA, Array<B> arrayB) {
auto sum = arrayA + arrayB; // ERROR: fails in instantiation of PlusResultT<A, B>
…
}
The practical problem is not that this failure occurs with code that is clearly ill-formed like this (there is no way to add an array of A
to an array of B
) but that it occurs during template argument deduction for operator+
, deep in the instantiation of PlusResultT<A,B>
.
This has a remarkable consequence: It means that the program may fail to compile even if we add a specific overload to adding A
and B
arrays, because C++ does not specify whether the types in a function template are actually instantiated if another overload would be better:
// declare generic + for arrays of different element types:
template<typename T1, typename T2>
Array<typename PlusResultT<T1, T2>::Type>
operator+ (Array<T1> const&, Array<T2> const&);
// overload + for concrete types:
Array<A> operator+(Array<A> const& arrayA, Array<B> const& arrayB);
void addAB(Array<A> const& arrayA, Array<B> const& arrayB) {
auto sum = arrayA + arrayB; // ERROR?: depends on whether the compiler
… // instantiates PlusResultT<A,B>
}
If the compiler can determine that the second declaration of operator+
is a better match without performing deduction and substitution on the first (template) declaration of operator+
, it will accept this code.
However, while deducing and substituting a function template candidate, anything that happens during the instantiation of the definition of a class template is not part of the immediate context of that function template substitution, and SFINAE does not protect us from attempts to form invalid types or expressions there. Instead of just discarding the function template candidate, an error is issued right away because we try to call operator+
for two elements of types A
and B
inside PlusResultT<>
:
template<typename T1, typename T2>
struct PlusResultT {
using Type = decltype(std::declval<T1>() + std::declval<T2>());
};
To solve this problem, we have to make the PlusResultT
SFINAE-friendly, which means to make it more resilient by giving it a suitable definition even when its decltype
expression is ill-formed.
Following the example of HasLessT
described in the previous section, we define a HasPlusT
trait that allows us to detect whether there is a suitable +
operation for the given types:
traits/hasplus.hpp
#include <utility> // for declval
#include <type_traits> // for true_type, false_type, and void_t
// primary template:
template<typename, typename, typename = std::void_t<>>
struct HasPlusT : std::false_type
{
};
// partial specialization (may be SFINAE’d away):
template<typename T1, typename T2>
struct HasPlusT<T1, T2, std::void_t<decltype(std::declval<T1>()
+ std::declval<T2>())>>
: std::true_type
{
};
If it yields a true
result, PlusResultT
can use the existing implementation. Otherwise, PlusResultT
needs a safe default. The best default for a trait that has no meaningful result for a set of template arguments is to not provide any member Type
at all. That way, if the trait is used within a SFINAE context—such as the return type of the array operator+
template above—the missing member Type
will make template argument deduction fail, which is precisely the behavior desired for the array operator+
template.
The following implementation of PlusResultT
provides this behavior:
traits/plus3.hpp
#include "hasplus.hpp"
template<typename T1, typename T2, bool = HasPlusT<T1, T2>::value>
struct PlusResultT { //primary template, used when HasPlusT yields true
using Type = decltype(std::declval<T1>() + std::declval<T2>());
};
template<typename T1, typename T2>
struct PlusResultT<T1, T2, false> { //partial specialization, used otherwise
};
In this version of PlusResultT
, we add a template parameter with a default argument that determines if the first two parameters support addition as determined by our HasPlusT
trait above. We then partially specialize PlusResultT
for false
values of that extra parameter, and our partial specialization definition has no members at all, avoiding the problems we described. For cases where addition is supported, the default argument evaluates to true
and the primary template is selected, with our existing definition of the Type
member. Thus, we fulfill the contract that PlusResultT
provide the result type only if in fact the +
operation is well-formed. (Note that the added template parameter should never have an explicit template argument.)
Consider again the addition of Array<A>
and Array<B>
: With our latest implementation of the PlusResultT
template, the instantiation of PlusResultT<A,B>
will not have a Type
member, because A
and B
values are not addable. Therefore, the result type of the array operator+
template is invalid, and SFINAE will eliminate the function template from consideration. The overloaded operator+
that is specific to Array<A>
and Array<B>
will therefore be chosen.
As a general design principle, a trait template should never fail at instantiation time if given reasonable template arguments as inputs. And the general approach is often to perform the corresponding check twice:
1. Once to find out whether the operation is valid
2. Once to to compute its result
We saw that already with PlusResultT
, where we call HasPlusT<>
to find out whether the call of operator+
in PlusResultImpl<>
is valid.
Let’s apply this principle to ElementT
as introduced in Section 19.3.1 on page 401: It produces an element type from a container type. Again, since the answer relies on a (container) type having a member type value_type
, the primary template should attempt to define the member Type
only when the container type has such a value_type
member:
template<typename C, bool = HasMemberT_value_type<C>::value>
struct ElementT {
using Type = typename C::value_type;
};
template<typename C>
struct ElementT<C, false> {
};
A third example of making traits SFINAE-friendly is shown Section 19.7.2 on page 444, where IsNothrowMoveConstructibleT
first has to check whether a move constructor exists before checking whether it is declared with noexcept
.
Details matter. And for that reason, the general approach for SFINAE-based traits might become more complicated in practice. Let’s illustrate this by defining a trait that can determine whether a given type is convertible to another given type—for example, if we expect a certain base class or one of its derived classes. The IsConvertibleT
trait yields whether we can convert a passed first type to a passed second type:
traits/isconvertible.hpp
#include <type_traits> // for true_type and false_type
#include <utility> // for declval
template<typename FROM, typename TO>
struct IsConvertibleHelper {
private:
// test() trying to call the helper aux(TO) for a FROM passed as F :
static void aux(TO);
template<typename F, typename T,
typename = decltype(aux(std::declval<F>()))>
static std::true_type test(void*);
// test() fallback:
template<typename, typename>
static std::false_type test(…);
public:
using Type = decltype(test<FROM>(nullptr));
};
template<typename FROM, typename TO>
struct IsConvertibleT : IsConvertibleHelper<FROM, TO>::Type {
};
template<typename FROM, typename TO>
using IsConvertible = typename IsConvertibleT<FROM, TO>::Type;
template<typename FROM, typename TO>
constexpr bool isConvertible = IsConvertibleT<FROM, TO>::value;
Here, we use the approach with function overloading, as introduced in Section 19.4.1 on page 416. That is, inside a helper class we declare two overloaded function templates named test()
with different return types and declare a Type
member for the base class of the resulting trait:
template<…> static std::true_type test(void*);
template<…> static std::false_type test(…);
…
using Type = decltype(test<FROM>(nullptr));
…
template<typename FROM, typename TO>
struct IsConvertibleT : IsConvertibleHelper<FROM, TO>::Type {
};
As usual, the first test()
overload is designed to match only if the requested check succeeds, while the second overload is the fallback. Thus, the goal is to make the first test()
overload valid if and only if type FROM
converts to type TO
. To achieve this, again we give our first declaration of test
a dummy (unnamed) template argument initialized with a construct that is valid if and only if the conversion is valid. This template parameter cannot be deduced, and we will not provide an explicit template argument for it. Therefore, it will be substituted, and if the substitution fails, that declaration of test()
will be discarded.
Again, note that the following doesn’t work:
static void aux(TO);
template<typename = decltype(aux(std::declval<FROM>()))>
static char test(void*);
Here, FROM
and TO
are completely determined when this member function template is parsed, and therefore a pair of types for which the conversion is not valid (e.g., double*
and int*
) will trigger an error right away, before any call to test()
(and therefore outside any SFINAE context).
For that reason, we introduce F
as a specific member function template parameter
static void aux(TO);
template<typename F, typename = decltype(aux(std::declval<F>()))>
static char test(void*);
and provide the FROM
type as an explicit template argument in the call to test()
that appears in the initialization of value
:
static constexpr bool value
= isSame<decltype(test<FROM>(nullptr)), char>;
Note how std::declval
, introduced in Section 19.3.4 on page 415, is used to produce a value without calling any constructor. If that value is convertible to TO
, the call to aux()
is valid, and this declaration of test()
matches. Otherwise, a SFINAE failure occurs and only the fallback declaration will match.
As a result, we can use the trait as follows:
IsConvertibleT<int, int>::value //yields true
IsConvertibleT<int, std::string>::value //yields false
IsConvertibleT<char const*, std::string>::value //yields true
IsConvertibleT<std::string, char const*>::value //yields false
Three cases are not yet handled correctly by IsConvertibleT
:
1. Conversions to array types should always yield false
, but in our code, the parameter of type TO
in the declaration of aux()
will just decay to a pointer type and therefore enable a “true” result for some FROM
types.
2. Conversions to function types should always yield false
, but just as with the array case, our implementation just treats them as the decayed type.
3. Conversion to (const
/volatile
-qualified) void
types should yield true
. Unfortunately, our implementation above doesn’t even successfully instantiate the case where TO
is a void
type because parameter types cannot have type void
(and aux()
is declared with such a parameter).
For all these cases, we’ll need additional partial specializations. However, adding such specializations for every possible combination of const
and volatile
qualifiers quickly becomes unwieldy. Instead, we can add an additional template parameter to our helper class template as follows:
template<typename FROM, typename TO, bool = IsVoidT<TO>::value
|| IsArrayT<TO>::value
|| IsFunctionT<TO>::value>
struct IsConvertibleHelper {
using Type = std::integral_constant<bool,
IsVoidT<TO>::value
&& IsVoidT<FROM>::value>;
};
template<typename FROM, typename TO>
struct IsConvertibleHelper<FROM,TO,false> {
… //previous implementation of IsConvertibleHelper here
};
The extra Boolean template parameter ensures that for all these special cases the implementation of the primary helper trait is used. It yields false_type
if we convert to arrays or functions (because then IsVoidT<TO>
is false) or if FROM
is void
and TO
is not, but for two void
types it will produce false_type
. All other cases produce a false
argument for the third parameter and therefore pick up the partial specialization, which corresponds to the implementation we already discussed.
See Section 19.8.2 on page 453 for a discussion of how to implement IsArrayT
and Section 19.8.3 on page 454 for a discussion of how to implement IsFunctionT
.
The C++ standard library provides a corresponding type trait std::is_convertible<>
, which is described in Section D.3.3 on page 727.
Another foray into SFINAE-based traits involves creating a trait (or, rather, a set of traits) that can determine whether a given type T
has a member of a given name X
(a type or a nontype member).
Let’s first define a trait that can determine whether a given type T
has a member type size_type
:
traits/hassizetype.hpp
#include <type_traits> // defines true_type and false_type
// helper to ignore any number of template parameters:
template<typename…> using VoidT = void;
// primary template:
template<typename, typename = VoidT<>>
struct HasSizeTypeT : std::false_type
{
};
// partial specialization (may be SFINAE’d away):
template<typename T>
struct HasSizeTypeT<T, VoidT<typename T::size_type>> : std::true_type
{
};
Here, we use the approach to SFINAE out partial specializations introduced in Section 19.4.2 on page 420.
As usual for predicate traits, we define the general case to be derived from std::false_type
, because by default a type doesn’t have the member size_type
.
In this case, we only need one construct:
typename T::size_type
This construct is valid if and only if type T
has a member type size_type
, which is exactly what we are trying to determine. If, for a specific T
, the construct is invalid (i.e., type T
has no member type size_type
), SFINAE causes the partial specialization to be discarded, and we fall back to the primary template. Otherwise, the partial specialization is valid and preferred.
We can use the trait as follows:
std::cout << HasSizeTypeT<int>::value; // false
struct CX {
using size_type = std::size_t;
};
std::cout << HasSizeType<CX>::value; // true
Note that if the member type size_type
is private, HasSizeTypeT
yields false
because our traits templates have no special access to their argument type, and therefore typename T::size_type
is invalid (i.e., triggers SFINAE). In other words, the trait tests whether we have an accessible member type size_type
.
As programmers, we are familiar with the surprises that can arise “on the edges” of the domains we consider. With a traits template like HasSizeTypeT
, interesting issues can arise with reference types. For example, while the following works fine:
struct CXR {
using size_type = char&; // Note: type size_type is a reference type
};
std::cout << HasSizeTypeT<CXR>::value; // OK: prints true
the following fails:
std::cout << HasSizeTypeT<CX&>::value; // OOPS: prints false
std::cout << HasSizeTypeT<CXR&>::value; // OOPS: prints false
and that is potentially surprising. It is true that a reference type has not members per se, but whenever we use references, the resulting expressions have the underlying type, and so perhaps it would be preferable to consider the underlying type in that case. Here, that could be achieved by using our earlier RemoveReference
trait in the partial specialization of HasSizeTypeT
:
template<typename T>
struct HasSizeTypeT<T, VoidT<RemoveReference<T>::size_type>>
: std::true_type {
};
It’s also worth noting that our traits technique to detect member types will also produce a true
value for injected class names (see Section 13.2.3 on page 221). For example:
struct size_type {
};
struct Sizeable : size_type {
};
static_assert(HasSizeTypeT<Sizeable>::value,
"Compiler bug: Injected class name missing");
The latter static assertion succeeds because size_type
introduces its own name as a member type, and that name is inherited. If it didn’t succeed, we would have found a defect in the compiler.
Defining a trait such as HasSizeTypeT
raises the question of how to parameterize the trait to be able to check for any member type name.
Unfortunately, this can currently be achieved only via macros, because there is no language mechanism to describe a “potential” name.21 The closest we can get for the moment without using macros is to use generic lambdas, as illustrated in Section 19.6.4 on page 438.
The following macro would work:
traits/hastype.hpp
#include <type_traits> // for true_type, false_type, and void_t
#define DEFINE_HAS_TYPE(MemType) \
template<typename, typename = std::void_t<>> \
struct HasTypeT_##MemType \
: std::false_type { }; \
template<typename T> \
struct HasTypeT_##MemType<T, std::void_t<typename T::MemType>> \
: std::true_type { } // ; intentionally skipped
Each use of DEFINE_HAS_TYPE(
MemberType)
defines a new HasTypeT_
MemberType trait. For example, we can use it to detect whether a type has a value_type
or a char_type
member type as follows:
traits/hastype.cpp
#include "hastype.hpp"
#include <iostream>
#include <vector>
DEFINE_HAS_TYPE(value_type);
DEFINE_HAS_TYPE(char_type);
int main()
{
std::cout << "int::value_type: "
<< HasTypeT_value_type<int>::value << ’\n’;
std::cout << "std::vector<int>::value_type: "
<< HasTypeT_value_type<std::vector<int>>::value << ’\n’;
std::cout << "std::iostream::value_type: "
<< HasTypeT_value_type<std::iostream>::value << ’\n’;
std::cout << "std::iostream::char_type: "
<< HasTypeT_char_type<std::iostream>::value << ’\n’;
}
We can modify the trait to also check for data members and (single) member functions:
traits/hasmember.hpp
#include <type_traits> // for true_type, false_type, and void_t
#define DEFINE_HAS_MEMBER(Member) \
template<typename, typename = std::void_t<>> \
struct HasMemberT_##Member \
: std::false_type { }; \
template<typename T> \
struct HasMemberT_##Member<T, std::void_t<decltype(&T::Member)>> \
: std::true_type { } // ; intentionally skipped
Here, we use SFINAE to disable the partial specialization when &T::Member
is not valid. For that construct to be valid, the following must be true:
• Member
must unambiguously identify a member of T
(e.g., it cannot be an overloaded member function name, or the name of multiple inherited members of the same name),
• The member must be accessible,
• The member must be a nontype, nonenumerator member (otherwise the prefix &
would be invalid), and
• If T::Member
is a static data member, its type must not provide an operator&
that makes &T::Member
invalid (e.g., by making that operator inaccessible). We can use the template as follows:
traits/hasmember.cpp
#include "hasmember.hpp"
#include <iostream>
#include <vector>
#include <utility>
DEFINE_HAS_MEMBER(size);
DEFINE_HAS_MEMBER(first);
int main()
{
std::cout << "int::size: "
<< HasMemberT_size<int>::value << ’\n’;
std::cout << "std::vector<int>::size: "
<< HasMemberT_size<std::vector<int>>::value << ’\n’;
std::cout << "std::pair<int,int>::first: "
<< HasMemberT_first<std::pair<int,int>>::value << ’\n’;
}
It would not be difficult to modify the partial specialization to exclude cases where &T::Member
is not a pointer-to-member type (which amounts to excluding static data members). Similarly, a pointer-to-member function could be excluded or required to limit the trait to data members or member functions.
Note that the HasMember
trait only checks whether a single member with the corresponding name exists. The trait also will fail if two members exists, which might happen if we check for overloaded member functions. For example:
DEFINE_HAS_MEMBER(begin);
std::cout << HasMemberT_begin<std::vector<int>>::value; // false
However, as explained in Section 8.4.1 on page 133, the SFINAE principle protects against attempts to create both invalid types and expressions in a function template declaration, allowing the overloading technique above to extend to testing whether arbitrary expressions are well-formed.
That is, we can simply check whether we can call a function of interest in a specific way and that can succeed even if the function is overloaded. As with the IsConvertibleT
trait in Section 19.5 on page 428, the trick is to formulate the expression that checks whether we can call begin()
inside a decltype
expression for the default value of an additional function template parameter:
traits/hasbegin.hpp
#include <utility> // for declval
#include <type_traits> // for true_type, false_type, and void_t
// primary template:
template<typename, typename = std::void_t<>>
struct HasBeginT : std::false_type {
};
// partial specialization (may be SFINAE’d away):
template<typename T>
struct HasBeginT<T, std::void_t<decltype(std::declval<T>().begin())>>
: std::true_type {
};
decltype(std::declval<T>().begin())
to test whether, given a value/object of type T
(using std::declval
to avoid any constructor being required), calling a member begin()
is valid.22
We can use the technique above for other kinds of expressions and even combine multiple expressions. For example, we can test whether, given types T1
and T2
, there is a suitable <
operator defined for values of these types:
traits/hasless.hpp
#include <utility> // for declval
#include <type_traits> // for true_type, false_type, and void_t
// primary template:
template<typename, typename, typename = std::void_t<>>
struct HasLessT : std::false_type
{
};
// partial specialization (may be SFINAE’d away):
template<typename T1, typename T2>
struct HasLessT<T1, T2, std::void_t<decltype(std::declval<T1>()
<std::declval<T2>())>>
: std::true_type
{
};
As always, the challenge is to define a valid expression for the condition to check and use decltype
to place it in a SFINAE context, where it will cause a fallback to the primary template if the expression isn’t valid:
decltype(std::declval<T1>() < std::declval<T2>())
Traits that detect valid expressions in this manner are fairly robust: They will evaluate true
only when the expression is well-formed and will correctly return false
when the <
operator is ambiguous, deleted, or inaccessible.23
We can use the trait as follows:
HasLessT<int, char>::value //yields true
HasLessT<std::string, std::string>::value //yields true
HasLessT<std::string, int>::value //yields false
HasLessT<std::string, char*>::value //yields true
HasLessT<std::complex<double>, std::complex<double>>::value //yields false
As introduced in Section 2.3.1 on page 30, we can use this trait to require that a template parameter T
supports operator <
:
template<typename T>
class C
{
static_assert(HasLessT<T>::value,
"Class C requires comparable elements");
…
};
Note that, due to the nature of std::void_t
, we can combine multiple constraints in a trait:
traits/hasvarious.hpp
#include <utility> // for declval
#include <type_traits> // for true_type, false_type, and void_t
// primary template:
template<typename, typename = std::void_t<>>
struct HasVariousT : std::false_type
{
};
// partial specialization (may be SFINAE’d away):
template<typename T>
struct HasVariousT<T, std::void_t<decltype(std::declval<T>().begin()),
typename T::difference_type,
typename T::iterator>>
: std::true_type
{
};
Traits that detect the validity of a specific syntax are quite powerful, allowing a template to customize its behavior based on the presence or absence of a particular operation. These traits will be used again both as part of the definition of SFINAE-friendly traits (Section 19.4.4 on page 424) and to aid in overloading based on type properties (Chapter 20).
The isValid
lambda, introduced in Section 19.4.3 on page 421, provides a more compact technique to define traits that check for members, helping is to avoid the use of macros to handle members if arbitrary names.
The following example illustrates how to define traits checking whether a data or type member such as first
or size_type
exists or whether operator<
is defined for two objects of different types:
traits/isvalid1.cpp
#include "isvalid.hpp"
#include<iostream>
#include<string>
#include<utility>
int main()
{
using namespace std;
cout << boolalpha;
// define to check for data member first:
constexpr auto hasFirst
= isValid([](auto x) -> decltype((void)valueT(x).first) {
});
cout << "hasFirst: " << hasFirst(type<pair<int,int>>) << ’\n’; // true
// define to check for member type size_type:
constexpr auto hasSizeType
= isValid([](auto x) -> typename decltype(valueT(x))::size_type {
});
struct CX {
using size_type = std::size_t;
};
cout << "hasSizeType: " << hasSizeType(type<CX>) << ’\n’; // true
if constexpr(!hasSizeType(type<int>)) {
cout << "int has no size_type\n";
…
}
// define to check for <:
constexpr auto hasLess
= isValid([](auto x, auto y) -> decltype(valueT(x) < valueT(y)) {
});
cout << hasLess(42, type<char>) << ’\n’; //yields true
cout << hasLess(type<string>, type<string>) << ’\n’; //yields true
cout << hasLess(type<string>, type<int>) << ’\n’; //yields false
cout << hasLess(type<string>, "hello") << ’\n’; //yields true
}
Note again that hasSizeType
uses std::decay
to remove the references from the passed x
because you can’t access a type member from a reference. If you skip that, the traits will always yield false
because the second overload of isValidImpl<>()
is used.
To be able to use the common generic syntax, taking types as template parameters, we can again define additional helpers. For example:
traits/isvalid2.cpp
#include "isvalid.hpp"
#include<iostream>
#include<string>
#include<utility>
constexpr auto hasFirst
= isValid([](auto&& x) -> decltype((void)&x.first) {
});
template<typename T>
using HasFirstT = decltype(hasFirst(std::declval<T>()));
constexpr auto hasSizeType
= isValid([](auto&& x)
-> typename std::decay_t<decltype(x)>::size_type {
});
template<typename T>
using HasSizeTypeT = decltype(hasSizeType(std::declval<T>()));
constexpr auto hasLess
= isValid([](auto&& x, auto&& y) -> decltype(x < y) {
});
template<typename T1, typename T2>
using HasLessT = decltype(hasLess(std::declval<T1>(), std::declval<T2>()));
int main()
{
using namespace std;
cout << "first: " << HasFirstT<pair<int,int>>::value << ’\n’; // true
struct CX {
using size_type = std::size_t;
};
cout << "size_type: " << HasSizeTypeT<CX>::value << ’\n’; // true
cout << "size_type: " << HasSizeTypeT<int>::value << ’\n’; // false
cout << HasLessT<int, char>::value << ’\n’; // true
cout << HasLessT<string, string>::value << ’\n’; // true
cout << HasLessT<string, int>::value << ’\n’; // false
cout << HasLessT<string, char*>::value << ’\n’; // true
}
template<typename T>
using HasFirstT = decltype(hasFirst(std::declval<T>()));
allows us to call
HasFirstT<std::pair<int,int>>::value
which results in the call of hasFirst
for a pair of two int
s, which is evaluated as described above.
Let’s finally introduce and discuss some other approaches to define traits.
In the previous section, the final definition of the PlusResultT
trait had a completely different implementation depending on the result of another type trait, HasPlusT
. We can formulate this if-then-else behavior with a special type template IfThenElse
that takes a Boolean nontype template parameter to select one of two type parameters:
traits/ifthenelse.hpp
#ifndef IFTHENELSE_HPP
#define IFTHENELSE_HPP
// primary template: yield the second argument by default and rely on
// a partial specialization to yield the third argument
// if COND is false
template<bool COND, typename TrueType, typename FalseType>
struct IfThenElseT {
using Type = TrueType;
};
// partial specialization: false yields third argument
template<typename TrueType, typename FalseType>
struct IfThenElseT<false, TrueType, FalseType> {
using Type = FalseType;
};
template<bool COND, typename TrueType, typename FalseType>
using IfThenElse = typename IfThenElseT<COND, TrueType, FalseType>::Type;
#endif //IFTHENELSE_HPP
The following example demonstrates an application of this template by defining a type function that determines the lowest-ranked integer type for a given value:
traits/smallestint.hpp
#include <limits>
#include "ifthenelse.hpp"
template<auto N>
struct SmallestIntT {
using Type =
typename IfThenElseT<N <= std::numeric_limits<char> ::max(), char,
typename IfThenElseT<N <= std::numeric_limits<short> ::max(), short,
typename IfThenElseT<N <= std::numeric_limits<int> ::max(), int,
typename IfThenElseT<N <= std::numeric_limits<long> ::max(), long,
typename IfThenElseT<N <= std::numeric_limits<long long> ::max(),
long long, //then
void //fallback
>::Type
>::Type
>::Type
>::Type
>::Type;
};
Note that, unlike a normal C++ if-then-else statement, the template arguments for both the “then” and “else” branches are evaluated before the selection is made, so neither branch may contain ill-formed code, or the program is likely to be ill-formed. Consider, for example, a trait that yields the corresponding unsigned type for a given signed type. There is a standard trait, std::make_unsigned
, which does this conversion, but it requires that the passed type is a signed integral type and no bool
; otherwise its use results in undefined behavior (see Section D.4 on page 729). So it might be a good idea to implement a trait that yields the corresponding unsigned type if this is possible and the passed type otherwise (thus, avoiding undefined behavior if an inappropriate type is given). The naive implementation does not work:
// ERROR: undefined behavior if T is bool or no integral type:
template<typename T>
struct UnsignedT {
using Type = IfThenElse<std::is_integral<T>::value
&& !std::is_same<T,bool>::value,
typename std::make_unsigned<T>::type,
T>;
};
The instantiation of UnsignedT<bool>
is still undefined behavior, because the compiler will still attempt to form the type from
typename std::make_unsigned<T>::type
To address this problem, we need to add an additional level of indirection, so that the IfThenElse
arguments are themselves uses of type functions that wrap the result:
// yield T when using member Type:
template<typename T>
struct IdentityT {
using Type = T;
};
// to make unsigned after IfThenElse was evaluated:
template<typename T>
struct MakeUnsignedT {
using Type = typename std::make_unsigned<T>::type;
};
template<typename T>
struct UnsignedT {
using Type = typename IfThenElse<std::is_integral<T>::value
&& !std::is_same<T,bool>::value,
MakeUnsignedT<T>,
IdentityT<T>
>::Type;
};
In this definition of UnsignedT
, the type arguments to IfThenElse
are both instances of type functions themselves. However, the type functions are not actually evaluated before IfThenElse
selects one. Instead, IfThenElse
selects the type function instance (of either MakeUnsignedT
or IdentityT
). The ::Type
then evaluates the selected type function instance to produce Type
.
It is worth emphasizing here that this relies entirely on the fact that the not-selected wrapper type in the IfThenElse
construct is never fully instantiated. In particular, the following does not work:
template<typename T>
struct UnsignedT {
using Type = typename IfThenElse<std::is_integral<T>::value
&& !std::is_same<T,bool>::value,
MakeUnsignedT<T>::Type,
T
>::Type;
};
Instead, we have to apply the ::Type
for MakeUnsignedT<T>
later, which means that we need the IdentityT
helper to also apply ::Type
later for T
in the else branch.
This also means that we cannot use something like
template<typename T>
using Identity = typename IdentityT<T>::Type;
in this context. We can declare such an alias template, and it may be useful elsewhere, but we cannot make effective use of it in the definition of IfThenElse
because any use of Identity<T>
immediately causes the full instantiation of IdentityT<T>
to retrieve its Type
member.
The IfThenElseT
template is available in the C++ standard library as std::conditional<>
(see Section D.5 on page 732). With it, the UnsignedT
trait would be defined as follows:
template<typename T>
struct UnsignedT {
using Type
= typename std::conditional_t<std::is_integral<T>::value
&& !std::is_same<T,bool>::value,
MakeUnsignedT<T>,
IdentityT<T>
>::Type;
};
It is occasionally useful to determine whether a particular operation can throw an exception. For example, a move constructor should be marked noexcept
, indicating that it does not throw exceptions, whenever possible. However, whether a move constructor for a particular class throws exceptions or not often depends on whether the move constructors of its members and bases throw. For example, consider the move constructor for a simple class template Pair
:
template<typename T1, typename T2>
class Pair {
T1 first;
T2 second;
public:
Pair(Pair&& other)
: first(std::forward<T1>(other.first)),
second(std::forward<T2>(other.second)) {
}
};
Pair
’s move constructor can throw exceptions when the move operations of either T1
or T2
can throw. Given a trait IsNothrowMoveConstructibleT
, we can express this property by using a computed noexcept
exception specification in Pair
’s move constructor. For example:
Pair(Pair&& other) noexcept(IsNothrowMoveConstructibleT<T1>::value &&
IsNothrowMoveConstructibleT<T2>::value)
: first(std::forward<T1>(other.first)),
second(std::forward<T2>(other.second))
{
}
All that remains is to implement the IsNothrowMoveConstructibleT
trait. We can directly implement this trait using the noexcept
operator, which determines whether a given expression is guaranteed to be nonthrowing:
traits/isnothrowmoveconstructible1.hpp
#include <utility> // for declval
#include <type_traits> // for bool_constant
template<typename T>
struct IsNothrowMoveConstructibleT
: std::bool_constant<noexcept(T(std::declval<T>()))>
{
};
Here, the operator version of noexcept
is used, which determines whether an expression is non-throwing. Because the result is a Boolean value, we can pass it directly to define the base class, std::bool_constant<>
, which is used to define std::true_type
and std::false_type
(see Section 19.3.3 on page 411).24
However, this implementation should be improved because it is not SFINAE-friendly (see Section 19.4.4 on page 424): If the trait is instantiated with a type that does not have a usable move or copy constructor—making the expression T(std::declval<T&&>())
invalid—the entire program is ill-formed:
class E {
public:
E(E&&) = delete;
};
…
std::cout << IsNothrowMoveConstructibleT<E>::value; // compile-time ERROR
Instead of aborting the compilation, the type trait should yield a value
of false
.
As discussed in Section 19.4.4 on page 424, we have to check whether the expression computing the result is valid before it is evaluated. Here, we have to find out whether move construction is valid before checking whether it is noexcept
. Thus, we revise the first version of the trait by adding a template parameter that defaults to void
and a partial that uses std::void_t
for that parameter with an argument that is valid only if move construction is valid:
traits/isnothrowmoveconstructible2.hpp
#include <utility> // for declval
#include <type_traits> // for true_type, false_type, and bool_constant<>
// primary template:
template<typename T, typename = std::void_t<>>
struct IsNothrowMoveConstructibleT : std::false_type
{
};
// partial specialization (may be SFINAE’d away):
template<typename T>
struct IsNothrowMoveConstructibleT
<T, std::void_t<decltype(T(std::declval<T>()))>>
: std::bool_constant<noexcept(T(std::declval<T>()))>
{
};
If the substitution of std::void_t<…>
in the partial specialization is valid, that specialization is selected, and the noexcept(…)
expression in the base class specifier can safely be evaluated. Otherwise, the partial specialization is discarded without instantiating it, and the primary template is instantiated instead (producing a std::false_type
result.
Note that there is no way to check whether a move constructor throws without being able to call it directly. That is, it is not enough that the move constructor is public and not deleted, it also requires that the corresponding type is no abstract class (references or pointers to abstract classes work fine). For this reason, the type trait is named IsNothrowMoveConstructible instead of HasNothrowMove-Constructor. For anything else, we’d need compiler support.
The C++ standard library provides a corresponding type trait std::is_move_constructible<>
, which is described in Section D.3.2 on page 721.
One common complaint with type traits is their relative verbosity, because each use of a type trait typically requires a trailing ::Type
and, in a dependent context, a leading typename
keyword, both of which are boilerplate. When multiple type traits are composed, this can force some awkward formatting, as in our running example of the array operator+
, if we would implement it correctly, ensuring that no constant or reference type is returned:
template<typename T1, typename T2>
Array<
typename RemoveCVT<
typename RemoveReferenceT<
typename PlusResultT<T1, T2>::Type
>::Type
>::Type
>
operator+ (Array<T1> const&, Array<T2> const&);
By using alias templates and variable templates, we can make the usage of the traits, yielding types or values respectively more convenient. However, note that in some contexts these shortcuts are not usable, and we have to use the original class template instead. We already discussed one such situation in our MemberPointerToIntT
example, but a more general discussion follows.
As introduced in Section 2.8 on page 39, alias templates offer a way to reduce verbosity. Rather than expressing a type trait as a class template with a type member Type
, we can use an alias template directly. For example, the following three alias templates wrap the type traits used above:
template<typename T>
using RemoveCV = typename RemoveCVT<T>::Type;
template<typename T>
using RemoveReference = typename RemoveReferenceT<T>::Type;
template<typename T1, typename T2>
using PlusResult = typename PlusResultT<T1, T2>::Type;
Given these alias templates, we can simplify our operator+
declaration down to
template<typename T1, typename T2>
Array<RemoveCV<RemoveReference<PlusResultT<T1, T2>>>>
operator+ (Array<T1> const&, Array<T2> const&);
This second version is clearly shorter and makes the composition of the traits more obvious. Such improvements make alias templates more suitable for some uses of type traits.
However, there are downsides to using alias templates for type traits:
1. Alias templates cannot be specialized (as noted in Section 16.3 on page 338), and since many of the techniques for writing traits depend on specialization, an alias template will likely need to redirect to a class template anyway.
2. Some traits are meant to be specialized by users, such as a trait that describes whether a particular addition operation is commutative, and it can be confusing to specialize the class template when most uses involve the alias template.
3. The use of an alias template will always instantiate the type (e.g., the underlying class template specialization), which makes it harder to avoid instantiating traits that don’t make sense for a given type (as discussed in Section 19.7.1 on page 440).
Another way to express this last point, is that alias templates cannot be used with metafunction forwarding (see Section 19.3.2 on page 404).
Because the use of alias templates for type traits has both positive and negative aspects, we recommend using them as we have in this section and as it is done in the C++ standard library: Provide both class templates with a specific naming convention (we have chosen the T
suffix and the Type
type member) and alias templates with a slightly different naming convention (we dropped the T
suffix), and have each alias template defined in terms of the underlying class template. This way, we can use alias templates wherever they provide clearer code, but fall back to class templates for more advanced uses.
Note that for historical reasons, the C++ standard library has a different convention. The type traits class templates yield a type in type
and have no specific suffix (many were introduced in C++11). Corresponding alias templates (that produce the type directly) started being introduced in C++14, and were given a _t
suffix, since the unsuffixed name was already standardized (see Section D.1 on page 697).
Traits returning a value require a trailing ::value
(or similar member selection) to produce the result of the trait. In this case, constexpr
variable templates (introduced in Section 5.6 on page 80) offer a way to reduce this verbosity.
For example, the following variable templates wrap the IsSameT
trait defined in Section 19.3.3 on page 410 and the IsConvertibleT
trait defined in Section 19.5 on page 428:
template<typename T1, typename T2>
constexpr bool IsSame = IsSameT<T1,T2>::value;
template<typename FROM, typename TO>
constexpr bool IsConvertible = IsConvertibleT<FROM, TO>::value;
Now we can simply write
if (IsSame<T,int> || IsConvertible<T,char>) …
instead of
if (IsSameT<T,int>::value || IsConvertibleT<T,char>::value) …
Again, for historical reasons, the C++ standard library has a different convention. The traits class templates producing a result in value
have no specific suffix, and many of them were introduced in the C++11 standard. The corresponding variable templates that directly produce the resulting value were introduced in C++17 with a _v
suffix (see Section D.1 on page 697).25
It is sometimes useful to be able to know whether a template parameter is a built-in type, a pointer type, a class type, and so on. In the following sections, we develop a suite of type traits that allow us to determine various properties of a given type. As a result, we will be able to write code specific for some types:
if (IsClassT<T>::value) {
…
}
or, using the compile-time if
available since C++17 (see Section 8.5 on page 134) and the features for traits convenience (see Section 19.7.3 on page 446):
if constexpr (IsClass<T>) {
…
}
or, by using partial specializations:
template<typename T, bool = IsClass<T>>
class C { //primary template for the general case
…
};
template<typename T>
class C<T, true> { //partial specialization for class types
…
};
Furthermore, expressions such as IsPointerT<T>::value
will be Boolean constants that are valid nontype template arguments. In turn, this allows the construction of more sophisticated and more powerful templates that specialize their behavior on the properties of their type arguments.
The C++ standard library defines several similar traits to determine the primary and composite type categories of a type.26 See Section D.2.1 on page 702 and Section D.2.2 on page 706 for details.
To start, let’s develop a template to determine whether a type is a fundamental type. By default, we assume a type is not fundamental, and we specialize the template for the fundamental cases:
traits/isfunda.hpp
#include <cstddef> // for nullptr_t
#include <type_traits> // for true_type, false_type, and bool_constant<>
// primary template: in general T is not a fundamental type
template<typename T>
struct IsFundaT : std::false_type {
};
// macro to specialize for fundamental types
#define MK_FUNDA_TYPE(T) \
template<> struct IsFundaT<T> : std::true_type { \
};
MK_FUNDA_TYPE(void)
MK_FUNDA_TYPE(bool)
MK_FUNDA_TYPE(char)
MK_FUNDA_TYPE(signed char)
MK_FUNDA_TYPE(unsigned char)
MK_FUNDA_TYPE(wchar_t)
MK_FUNDA_TYPE(char16_t)
MK_FUNDA_TYPE(char32_t)
MK_FUNDA_TYPE(signed short)
MK_FUNDA_TYPE(unsigned short)
MK_FUNDA_TYPE(signed int)
MK_FUNDA_TYPE(unsigned int)
MK_FUNDA_TYPE(signed long)
MK_FUNDA_TYPE(unsigned long)
MK_FUNDA_TYPE(signed long long)
MK_FUNDA_TYPE(unsigned long long)
MK_FUNDA_TYPE(float)
MK_FUNDA_TYPE(double)
MK_FUNDA_TYPE(long double)
MK_FUNDA_TYPE(std::nullptr_t)
#undef MK_FUNDA_TYPE
The primary template defines the general case. That is, in general, IsFundaT<
T>::value
will evaluate to false
:
template<typename T>
struct IsFundaT : std::false_type {
static constexpr bool value = false;
};
For each fundamental type, a specialization is defined so that IsFundaT<
T>::value
is true
. We define a macro that expands to the necessary code for convenience. For example:
MK_FUNDA_TYPE(bool)
expands to the following:
template<> struct IsFundaT<bool> : std::true_type {
static constexpr bool value = true;
};
The following program demonstrates a possible use of this template:
traits/isfundatest.cpp
#include "isfunda.hpp"
#include <iostream>
template<typename T>
void test (T const&)
{
if (IsFundaT<T>::value) {
std::cout << "T is a fundamental type" << ’\n’;
}
else {
std::cout << "T is not a fundamental type" << ’\n’;
}
}
int main()
{
test(7);
test("hello");
}
It has the following output:
T is a fundamental type
T is not a fundamental type
In the same way, we can define type functions IsIntegralT
and IsFloatingT
to identify which of these types are integral scalar types and which are floating-point scalar types.
The C++ standard library uses a more fine-grained approach than only to check whether a type is a fundamental type. It first defines primary type categories, where each type matches exactly one type category (see Section D.2.1 on page 702), and then composite type categories such as std::is_integral
or std::is_fundamental
(see Section D.2.2 on page 706).
Compound types are types constructed from other types. Simple compound types include pointer types, lvalue and rvalue reference types, pointer-to-member types, and array types. They are constructed from one or two underlying types. Class types and function types are also compound types, but their composition can involve an arbitrary number of types (for parameters or members). Enumeration types are also considered nonsimple compound types in this classification even though they are not compound from multiple underlying types. Simple compound types can be classified using partial specialization.
We start with one such simple classification, for pointer types:
traits/ispointer.hpp
template<typename T>
struct IsPointerT : std::false_type { //primary template: by default not a pointer
};
template<typename T>
struct IsPointerT<T*> : std::true_type { //partial specialization for pointers
using BaseT = T; // type pointing to
};
The primary template is a catch-all case for nonpointer types and, as usual, provides its value
constant false
through is base class std::false_type
, indicating that the type is not a pointer. The partial specialization catches any kind of pointer (T*
) and provides the value true
to indicate that the provided type is a pointer. Additionally, it provides a type member BaseT
that describes the type that the pointer points to. Note that this type member is only available when the original type was a pointer, making this a SFINAE-friendly type trait (see Section 19.4.4 on page 424).
The C++ standard library provides a corresponding trait std::is_pointer<>
, which, however, does not provide a member for the type the pointer points to. It is described in Section D.2.1 on page 704.
Similarly, we can identify lvalue reference types:
traits/islvaluereference.hpp
template<typename T>
struct IsLValueReferenceT : std::false_type { //by default no lvalue reference
};
template<typename T>
struct IsLValueReferenceT<T&> : std::true_type { //unless T is lvalue references
using BaseT = T; // type referring to
};
and rvalue reference types:
traits/isrvaluereference.hpp
template<typename T>
struct IsRValueReferenceT : std::false_type { //by default no rvalue reference
};
template<typename T>
struct IsRValueReferenceT<T&&> : std::true_type { //unless T is rvalue reference
using BaseT = T; // type referring to
};
which can be combined into an IsReferenceT<>
trait:
traits/isreference.hpp
#include "islvaluereference.hpp"
#include "isrvaluereference.hpp"
#include "ifthenelse.hpp"
template<typename T>
class IsReferenceT
: public IfThenElseT<IsLValueReferenceT<T>::value,
IsLValueReferenceT<T>,
IsRValueReferenceT<T>
>::Type {
};
In this implementation, we use IfThenElseT
(from Section 19.7.1 on page 440) to select between either IsLValueReference<T>
or IsRValueReference<T>
as our base class, using metafunction forwarding (discussed in Section 19.3.2 on page 404). If T
is an lvalue reference, we inherit from IsLValueReference<T>
to get the appropriate value
and BaseT
members. Otherwise, we inherit from IsRValueReference<T>
, which determines whether the type is an rvalue reference or not (and provides the appropriate member(s) in either case).
The C++ standard library provides the corresponding traits std::is_lvalue_reference<>
and std::is_rvalue_reference<>
, which are described in Section D.2.1 on page 705, and std::is_reference<>
, which is described in Section D.2.2 on page 706. Again, these traits do not provide a member for the type the reference refers to.
When defining traits to determine arrays, it may come as a surprise that the partial specializations involve more template parameters than the primary template:
traits/isarray.hpp
#include <cstddef>
template<typename T>
struct IsArrayT : std::false_type { //primary template: not an array
};
template<typename T, std::size_t N>
struct IsArrayT<T[N]> : std::true_type { //partial specialization for arrays
using BaseT = T;
static constexpr std::size_t size = N;
};
template<typename T>
struct IsArrayT<T[]> : std::true_type { //partial specialization for unbound arrays
using BaseT = T;
static constexpr std::size_t size = 0;
};
Here, multiple additional members provide information about the arrays being classified: their base type and their size (with 0
used to denote an unknown size).
The C++ standard library provides the corresponding trait std::is_array<>
to check whether a type is an array, which is described in Section D.2.1 on page 704. In addition, traits such as std::rank<>
and std::extent<>
allow us to query their number of dimensions and the size of a specific dimension (see Section D.3.1 on page 715).
Pointers to members can be treated using the same technique:
traits/ispointertomember.hpp
template<typename T>
struct IsPointerToMemberT : std::false_type { //by default no pointer-to-member
};
template<typename T, typename C>
struct IsPointerToMemberT<T C::*> : std::true_type { //partial specialization
using MemberT = T;
using ClassT = C;
};
Here, the additional members provide both the type of the member and the type of the class in which that member occurs.
The C++ standard library provides more specific traits, std::is_member_object_pointer<>
and std::is_member_function_pointer<>
, which are described in Section D.2.1 on page 705, as well as std::is_member_pointer<>
, which is described in Section D.2.2 on page 706.
Function types are interesting because they have an arbitrary number of parameters in addition to the result type. Therefore, within the partial specialization matching a function type, we employ a parameter pack to capture all of the parameter types, as we did for the DecayT
trait in Section 19.3.2 on page 404:
traits/isfunction.hpp
#include "../typelist/typelist.hpp"
template<typename T>
struct IsFunctionT : std::false_type { //primary template: no function
};
template<typename R, typename… Params>
struct IsFunctionT<R (Params…)> : std::true_type { //functions
using Type = R;
using ParamsT = Typelist<Params…>;
static constexpr bool variadic = false;
};
template<typename R, typename… Params>
struct IsFunctionT<R (Params…, …)> : std::true_type { //variadic functions
using Type = R;
using ParamsT = Typelist<Params…>;
static constexpr bool variadic = true;
};
Note that each part of the function type is exposed: Type provides the result type, while all of the parameters are captured in a single typelist as ParamsT (typelists are covered in Chapter 24), and variadic indicates whether the function type uses C-style varargs.
Unfortunately, this formulation of IsFunctionT
does not handle all function types, because function types can have const
and volatile
qualifiers as well as lvalue (&
) and rvalue (&&
) reference qualifiers (described in Section C.2.1 on page 684) and, since C++17, noexcept
qualifiers. For example:
using MyFuncType = void (int&) const;
Such function types can only be meaningfully used for nonstatic member functions but are function types nonetheless. Moreover, a function type marked const
is not actually a const
type,27 so RemoveConst
is not able to strip the const
from the function type. Therefore, to recognize function types that have qualifiers, we need to introduce a large number of additional partial specializations, covering every combination of qualifiers (both with and without C-style varargs). Here, we illustrate only five of the many28 required partial specializations:
template<typename R, typename… Params>
struct IsFunctionT<R (Params…) const> : std::true_type {
using Type = R;
using ParamsT = Typelist<Params…>;
static constexpr bool variadic = false;
};
template<typename R, typename… Params>
struct IsFunctionT<R (Params…, …) volatile> : std::true_type {
using Type = R;
using ParamsT = Typelist<Params…>;
static constexpr bool variadic = true;
};
template<typename R, typename… Params>
struct IsFunctionT<R (Params…, …) const volatile> : std::true_type {
using Type = R;
using ParamsT = Typelist<Params…>;
static constexpr bool variadic = true;
};
template<typename R, typename… Params>
struct IsFunctionT<R (Params…, …) &> : std::true_type {
using Type = R;
using ParamsT = Typelist<Params…>;
static constexpr bool variadic = true;
};
template<typename R, typename… Params>
struct IsFunctionT<R (Params…, …) const&> : std::true_type {
using Type = R;
using ParamsT = Typelist<Params…>;
static constexpr bool variadic = true;
};
…
With all this in place, we can now classify all types except class types and enumeration types. We tackle these cases in the following sections.
The C++ standard library provides the trait std::is_function<>
, which is described in Section D.2.1 on page 706.
Unlike the other compound types we have handled so far, we have no partial specialization patterns that match class types specifically. Nor is it feasible to enumerate all class types, as it is for fundamental types. Instead, we need to use an indirect method to identify class types, by coming up with some kind of type or expression that is valid for all class types (and not other type). With that type or expression, we can apply the SFINAE trait techniques discussed in Section 19.4 on page 416.
The most convenient property of class types to utilize in this case is that only class types can be used as the basis of pointer-to-member types. That is, in a type construct of the form X Y::*
, Y
can only be a class type. The following formulation of IsClassT<>
exploits this property (and picks int
arbitrarily for type X
):
traits/isclass.hpp
#include <type_traits>
template<typename T, typename = std::void_t<>>
struct IsClassT : std::false_type { //primary template: by default no class
};
template<typename T>
struct IsClassT<T, std::void_t<int T::*>> // classes can have pointer-to-member
: std::true_type {
};
The C++ language specifies that the type of a lambda expression is a “unique, unnamed non-union class type.” For this reason, lambda expressions yield true
when examining whether they are class type objects:
auto l = []{};
static_assert<IsClassT<decltype(l)>::value, "">; //succeeds
Note also that the expression int T::*
is also valid for union
types (they are also class types according to the C++ standard).
The C++ standard library provides the traits std::is_class<>
and std::is_union<>
, which are described in Section D.2.1 on page 705. However, these traits require special compiler support because distinguishing class
and struct
types from union
types cannot currently be done using any standard core language techniques.29
The only types not yet classified by any of our traits are enumeration types. Testing for enumeration types can be performed directly by writing a SFINAE-based trait that checks for an explicit conversion to an integral type (say, int
) and explicitly excluding fundamental types, class types, reference types, pointer types, and pointer-to-member types, all of which can be converted to an integral type but are not enumeration types.30 Instead, we simply note that any type that does not fall into any of the other categories must be an enumeration type, which we can implement as follows:
traits/isenum.hpp
template<typename T>
struct IsEnumT {
static constexpr bool value = !IsFundaT<T>::value &&
!IsPointerT<T>::value &&
!IsReferenceT<T>::value &&
!IsArrayT<T>::value &&
!IsPointerToMemberT<T>::value &&
!IsFunctionT<T>::value &&
!IsClassT<T>::value;
};
The C++ standard library provides the trait std::is_enum<>
, which is described in Section D.2.1 on page 705. Usually, to improve compilation performance, compilers will directly provide support for such a trait instead of implementing it as “anything else.”
So far, our examples of traits templates have been used to determine properties of template parameters: what sort of type they represent, the result type of an operator applied to values of that type, 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 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. To illustrate this notion, let’s look at a type function that defines a policy for passing read-only parameters.
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>
struct RParam {
using Type = typename IfThenElseT<sizeof(T)<=2*sizeof(void*),
T,
T const&>::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>
struct RParam<Array<T>> {
using Type = Array<T> const&;
};
Because such types are common in C++, it may be safer to mark only small types with trivial copy and move constructors as by value types31 and then selectively add other class types when performance considerations dictate it (the std::is_trivially_copy_constructible
and std::is_trivially_move_constructible
type traits are part of the C++ standard library).
traits/rparam.hpp
#ifndef RPARAM_HPP
#define RPARAM_HPP
#include "ifthenelse.hpp"
#include <type_traits>
template<typename T>
struct RParam {
using Type
= IfThenElse<(sizeof(T) <= 2*sizeof(void*)
&& std::is_trivially_copy_constructible<T>::value
&& std::is_trivially_move_constructible<T>::value),
T,
T const&>;
};
#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 "rparam.hpp"
#include <iostream>
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:
using Type = MyClass2;
};
Now, we 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);
}
Unfortunately, there are some significant downsides to using RParam
. First, the function declaration is significantly messier. 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 that provides perfect forwarding (Section 15.6.3 on page 280), 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>
void foo (T1 && p1, T2 && p2)
{
foo_core<T1,T2>(std::forward<T1>(p1),std::forward<T2>(p2));
}
int main()
{
MyClass1 mc1;
MyClass2 mc2;
foo(mc1,mc2); // same as foo_core<MyClass1,MyClass2>(mc1,mc2)
}
With C++11, type traits became an intrinsic part of the C++ standard library. They comprise more or less all type functions and type traits discussed in this chapter. However, for some of them, such as the trivial operation detection traits and as discussed std::is_union
, there are no known in-language solutions. Rather, the compiler provides intrinsic support for those traits. Also, compilers start to support traits even if there are in-language solutions to shorten compile time.
For this reason, if you need type traits, we recommend that you use the ones from the C++ standard library whenever available. They are all described in detail in Appendix D.
Note that (as discussed) some traits have potentially surprising behavior (at least for the naive programmer). In addition to the general hints we give in Section 11.2.1 on page 164 and Section D.1.2 on page 700, also consider the specific descriptions we provide in Appendix D.
The C++ standard library also defines some policy and property traits:
• The class template std::char_traits
is used as a policy traits parameter by the string and I/O stream classes.
• To adapt algorithms easily to the kind of standard 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.
• Lastly, memory allocation for the standard container types is handled using policy traits classes. Since C++98, the template std::allocator
is provided as the standard component for this purpose. With C++11, the template std::allocator_traits
was added to be able to change the policy/behavior of allocators (switching between the classical behavior and scoped allocators; the latter could not be accommodated within the pre-C++11 framework).
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 (e.g., 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 declare a type alias name for the resulting specializations 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.
Traits can be used as a form of reflection, in which a program inspects its own high-level properties (such as its type structures). Traits such as IsClassT
and PlusResultT
, as well as many other type traits that inspect the types in the program, implement a form of compile-time reflection, which turns out to be a powerful ally to metaprogramming (see Chapter 23 and Section 17.9 on page 363).
The idea of storing properties of types as members of template specializations dates back to at least the mid-1990s. Among the earlier serious applications of type classification templates was the __type_traits
utility in the STL implementation distributed by SGI (then known as Silicon Graphics). The SGI template was meant to represent some properties of its template argument (e.g., whether it was a plain old datatype (POD) or whether its destructor was trivial). This information was then used to optimize certain STL algorithms for the given type. An interesting feature of the SGI solution was that some SGI compilers recognized the __type_traits
specializations and provided information about the arguments that could not be derived using standard techniques. (The generic implementation of the __type_traits
template was safe to use, albeit suboptimal.)
Boost provides a rather complete set of type classification templates (see [BoostTypeTraits]) that formed the basis of the <type_traits>
header in the 2011 C++ standard library. While many of these traits can be implemented with the techniques described in this chapter, others (such as std::is_pod
, for detecting PODs) require compiler support, much like the __type_traits
specializations provided by the SGI compilers.
The use of the SFINAE principle for type classification purposes had been noted when the type deduction and substitution rules were clarified during the first standardization effort. However, it was never formally documented, and as a result, much effort was later spent trying to re-create some of the techniques described in this chapter. The first edition of this book was one of the earliest sources for this technique, and it introduced the term SFINAE. One of the other notable early contributors in this area was Andrei Alexandrescu, who made popular the use of the sizeof
operator to determine the outcome of overload resolution. This technique became popular enough that the 2011 standard extended the reach of SFINAE from simple type errors to arbitrary errors within the immediate context of the function template (see [SpicerSFINAE]). This extension, in combination with the addition of decltype
, rvalue references, and variadic templates, greatly expanded the ability to test for specific properties within traits.
Using generic lambdas like isValid
to extract the essence of a SFINAE condition is a technique introduced by Louis Dionne in 2015, which is used by Boost.Hana (see [BoostHana]), a metaprogramming library suited for compile-time computations on both types and values.
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]).
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.
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.
3 In C++11, you have to declare the return type like type AccT
.
4 Most modern C++ compilers can “see through” calls of simple inline functions. Additionally, the use of constexpr
makes it possible to use the value traits in contexts where the expression must be a constant (e.g., in a template argument).
5 We could generalize this to a policy parameter, which could be a class (as discussed) or a pointer to a function.
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.
7 In C++11, you have to declare the return type as VT
.
8 The order in which the qualifiers is removed has no semantic consequence: We could first remove volatile
and then const
instead.
9 Using the term decay might be slightly confusing because in C it only implies the conversion from array/-function types to to pointer types, whereas here it also includes the removal of top-level const
/volatile
qualifiers.
10 Strictly speaking, the comma prior to the second ellipsis (…
) is optional but is provided here for clarity. Due to the ellipsis being optional, the function type in the first partial specialization is actually syntactically ambiguous: It can be parsed as either R(Args, …)
(a C-style varargs parameter) or R(Args… name)
(a parameter pack). The second interpretation is picked because Args
is an unexpanded parameter pack. We can explicitly add the comma in the (rare) cases where the other interpretation is desired.
11 The difference between the return types T
and T&&
is discoverable by direct use of decltype
. However, given declval
’s limited use, this is not of practical interest.
12 The fallback declaration can sometimes be a plain member function declaration instead of a member function template.
13 However, the SFINAE rules were more limited back then: When substitution of template arguments resulted in a malformed type construct (e.g., T::X
where T
is int
), SFINAE worked as expected, but if it resulted in an invalid expression (e.g., sizeof(f())
where f()
returns void
), SFINAE did not kick in and an error was issued right away.
14 The first edition of this book was perhaps the first source for this technique.
15 Defining void_t
inside namespace std
is formally invalid: User code is not permitted to add declarations to namespace std
. In practice, no current compiler enforces that restriction, nor do they behave unexpectedly (the standard indicates that doing this leads to “undefined behavior,” which allows anything to happen).
16 At the time of this writing, Microsoft Visual C++ is the unfortunate exception.
17 Thanks to Louis Dionne for pointing out the technique described in this section.
18 This very simple pair of helper templates is a fundamental technique that lies at the heart of advanced libraries such as Boost.Hana!
19 This code is not valid C++ because a lambda expression cannot appear directly in a decltype
operand for compiler-technical reasons, but the meaning is clear.
20 For simplicity, the return value just uses PlusResultT<T1,T2>::Type
. In practice, the return type should also be computed using RemoveReferenceT<>
and RemoveCVT<>
to avoid that references are returned.
21 At the time of this writing, the C++ standardization committee is exploring ways to “reflect” various program entities (like class types and their members) in ways that the program can explore. See Section 17.9 on page 363.
22 Except that decltype(
call-expression)
does not require a nonreference, non-void
return type to be complete, unlike call expressions in other contexts. Using decltype(std::declval<T>().begin(), 0)
instead does add the requirement that the return type of the call is complete, because the returned value is no longer the result of the decltype
operand.
23 Prior to C++11’s expansion of SFINAE to cover arbitrary invalid expressions, the techniques for detecting the validity of specific expressions centered on introducing a new overload for the function being tested (e.g., <
) that had an overly permissive signature and a strangely sized return type to behave as a fallback case. However, such approaches were prone to ambiguities and caused errors due to access control violations.
24 In C++11 and C++14, we have to specify the base class as std::integral_constant<bool,
…>
instead of std::bool_constant<
…>
.
25 The C++ standardization committee is further bound by a long-standing tradition that all standard names consist of lowercase characters and optional underscores to separate them. That is, a name like isSame
or IsSame
is unlikely to ever be seriously considered for standardization (except for concepts, where this spelling style will be used).
26 The use of “primary” vs. “composite” type categories should not be confused with the distinction between “fundamental” vs. “compound” types
. The standard describes fundamental types (like int
or std::nullptr_t
) and compound types (like pointer types and class types). This is different from composite type categories (like arithmetic), which are categories that are the union of primary type categories (like floating-point).
27 Specifically, when a function type is marked const
, it refers to a qualifier on the object pointed to by the implicit parameter this
, whereas the const
on a const
type refers to the object of that actual type.
28 The latest count is 48.
29 Most compilers support intrinsic operators like __is_union
to help standard libraries implement various traits templates. This is true even for some traits that could technically be implemented using the techniques from this chapter, because the intrinsics can improve compilation performance.
30 The first edition of this book described enumeration type detection in this way. However, it checked for an implicit conversion to an integral type, which sufficed for the C++98 standard. The introduction of scoped enumeration types into the language, which do not have such an implicit conversion, complicates the detection of enumeration types.
31 A copy or move constructor is called trivial if, in effect, a call to it can be replaced by a simple copy of the underlying bytes.