In this post, I will introduce you to C++ template programming. Some of the topics I will cover are function templates, class templates, variable templates, variadic templates, type traits, SFINAE, and C++ 20 concepts.
Contents
- 1 Introduction
- 2 Terminology
- 3 Basics
- 4 Template Type Deduction
- 5 decltype Specifier
- 6 std::declval()
- 7 Type Traits
- 7.1 integral_constant
- 7.2 bool_constant
- 7.3 true_type
- 7.4 false_type
- 7.5 type_identity
- 7.6 void_t
- 7.7 enable_if
- 7.8 remove_const
- 7.9 remove_volatile
- 7.10 remove_cv
- 7.11 remove_reference
- 7.12 remove_cvref
- 7.13 remove_extent
- 7.14 remove_all_extents
- 7.15 remove_pointer
- 7.16 add_const
- 7.17 add_volatile
- 7.18 add_cv
- 7.19 add_lvalue_reference
- 7.20 add_rvalue_reference
- 7.21 add_pointer
- 7.22 conditional
- 7.23 conjunction
- 7.24 disjunction
- 7.25 negation
- 7.26 is_same
- 7.27 is_void
- 7.28 is_null_pointer
- 7.29 is_any_of
- 7.30 is_integral
- 7.31 is_floating_point
- 7.32 is_arithmetic
- 7.33 is_const
- 7.34 is_reference
- 7.35 is_bounded_array
- 7.36 is_unbounded_array
- 7.37 is_array
- 7.38 is_function
- 7.39 decay
- 8 SFINAE
- 9 Concepts
- 10 Conclusion
- 11 Bibliography
Introduction
C++ Template Programming has been around for a long time and there are plenty of books and articles on the internet that describe C++ template programming. Although the information is in abundance, there doesn’t seem to be a comprehensive source for learning how to apply template programming and demystify some of the more complex features of C++ template programming such as using and writing type traits, template meta programming, and Substitution Failure Is Not An Error (SFINAE) which are a few of the topics that I would like to cover in this article.
Terminology
Before delving into the details of C++ template programming, it is important to establish some common terminology when describing template programming.
Value categories are a way to categorize how a value can be used. There are five value categories (lvalue, prvalue, xvalue, glvalue, and rvalue). You may already be familiar with lvalue, and rvalue, but might not have encountered prvalue, xvalue, or glvalue yet.
Understanding the difference between template parameters and template arguments is also important when talking about template programming. In short, template parameters are used to declare the template and template arguments are used to define the template. Template parameters can be either typed, or non typed.
Let’s first look at value categories.
Expressions and Value Categories
All C++ programmers work with value categories but may not be able to describe which category a value belongs.
Value categories are often defined in terms of the result of an expression. An example of an expression is assignment, increment and decrement, arithmetic, logical, function calls, etc. An expression consists of one or more operands and one operator (or two operators in the case of the ternary operator). Expressions are usually terminated with a semicolon (;
) but can also be nested inside other expressions or used inside conditional constructs (like if
, while
, and for
loops).
Expressions are categorized according to the following taxonomy:
According to Stroustrup[2], value categories can be identified by two independent properties:
- “has identity” (i): A value is identified by a name. Pointers and references also represent identities.
- “can be moved from” (m): A value can be moved. Expressions that use move semantics like the move constructor and the move assignment operator are examples of move expressions.
When a value category has the identity property, it can be denoted with a lower-case i. When a value type does not have the identity property, it will be denoted with an upper-case I.
Similarly for the move property: if the value category can be moved, it will be denoted with a lower-case m, otherwise it will be denoted with an upper-case M.
There are five value categories (three primary, and two mixed categories) that are discussed in this section:
- Primary categories
- lvalue
- prvalue
- xvalue
- Mixed categories
- glvalue
- rvalue
lvalue, prvalue, and xvalue are primary value categories because they are mutually exclusive. glvalue and rvalue categories are mixed categories because they are a generalization of the primary value categories.
lvalue
An lvalue is a value that is named by an identifier (i) but cannot be moved (M). An lvalue is something that has a location in memory. For this reason, lvalues are sometimes referred to as locator values.
The following code snippet shows some examples of lvalues.
1 2 3 4 |
int i = 3; // i is an lvalue. std::string s = "Hello, world!"; // s is an lvalue. int* p = nullptr; // p is an lvalue. int& r = *p; // r is an lvalue. |
It might be tempting to think of lvalues as something that only appears on the left-hand side of an assignment operator, but this is not a good way to think of them. For example, a const value is an lvalue, but it cannot be assigned to after initialization.
1 2 |
const int i = 3; // i is an lvalue. i = 4; // error: assignment of read-only variable 'i' |
Of course, lvalues can also appear on the right side of the assignment operator.
1 2 |
const int i = 3; // i is an lvalue. int j = i; // i is an lvalue on the right side of the assignment operator. |
However, calling i
an lvalue in this context (when it appears on the right side of the assignment operator) is not entirely correct. In this case, i
is implicitly cast to an rvalue (the contents of i
) which is what gets assigned to j
[4].
prvalue
A prvalue is a pure rvalue. Pure rvalues do not have an identifier (I) but can be moved (m). Literal values are examples of prvalues.
1 2 3 4 |
int i = 3; // 3 is a prvalue. std::string s = "Hello, world!"; // "Hello, world!" is a prvalue. int* p = nullptr; // nullptr is a prvalue. bool b = true; // true is a prvalue. |
It is possible to assign a prvalue to an lvalue (as demonstrated in the previous code example) but it is not possible to assign an lvalue to a prvalue.
1 2 3 |
int i = 3; // prvalue 3 is assigned to lvalue i. int j = 4; // prvalue 4 is assigned to lvalue j. 3 = i + j; // error: lvalue required as left operand of assignment |
It is also an error to try to get the address of a prvalue.
1 2 3 |
bool* b = &true; // error: lvalue required as unary '&' operand int* i = &3; // error: lvalue required as unary '&' operand int* j = &( 3 + 4 ); // error: lvalue required as unary '&' operand |
The result of an expression using built-in operators are also prvalues.
1 2 3 |
(1 + 3); // prvalue (true || false); // prvalue (true && false); // prvalue |
One special thing to note is that prvalues can be candidates for move operations. The following example is valid:
1 2 3 4 5 6 7 8 9 10 11 12 |
int MoveMe(int&& i) // MoveMe takes an rvalue reference. { int j = i; return j; } int main() { int i = 0; i = MoveMe(3); // prvalue (3) is used where an rvalue reference is expected. return 0; } |
This code compiles fine since the prvalue (3
) is moved into the i
parameter in the MoveMe
function.
xvalue
An xvalue are values that are named using an identifier (i) and are movable (m). Any function that returns an rvalue reference (such as std::move
) is an xvalue expression. xvalues are the result of casting an lvalue to an rvalue reference as shown in the following example:
1 2 |
int i = 0; // i is an lvalue. int&& j = std::move(i); // The result of std::move is an xvalue. |
The result of std::move
is an xvalue and can be assigned to an rvalue reference.
The xvalue is referred to as an “expiring value” since it is used to refer to an object that is expiring since its resources are likely only going to be moved to another object soon[5].
The xvalue value category is somewhat esoteric and likely only important for compiler writers and people working on the C++ standard documentation. For this article, it’s only important to know of its existence.
glvalue
A glvalue is a “generalized lvalue”[2]. glvalues can be either an lvalue or an xvalue. You can think of it as a movable (m) lvalue. glvalues can also be named by an identifier (i). Some examples of glvalues are:
1 2 3 4 |
int i; // i is a glvalue (lvalue) int* p = &i; // p is a glvalue (lvalue) int& f(); // the result of f() is a glvalue (lvalue) int&& g(); // the result of g() is an glvalue (xvalue) |
A glvalue is anything that is not a prvalue.
rvalue
An rvalue is a generalization of prvalues and xvalues. An rvalue is not named with an identifier (I) but can be moved (m). To avoid ambiguity with prvalues, rvalues are only denoted with the lower-case (m) to indicate the value “can be moved from”.
1 2 3 |
int i = 3; // 3 is an rvalue (prvalue). std::string s = "Hello, world!" // "Hello, world!" is an rvalue (prvalue) int&& g(); // The result of g() is an rvalue (xvalue) |
An rvalue is anything that is not an lvalue.
Template Arguments versus Template Parameters
Suppose we have the following template class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
template<typename T, size_t N> class Array { public: Array() : m_Data{} {} size_t size() const { return N; } T& operator[](size_t i) { assert(i < N); return m_Data[i]; } const T& operator[](size_t i) const { assert(i < N); return m_Data[i]; } private: T m_Data[N]; }; |
And we also have the following instantiation of the template class:
1 2 3 |
Array<float, 3> Position; Array<float, 3> Normal; Array<float, 2> TexCoord; |
In this example, the template parameters are T
and N
and the type template argument is float
and the non-type template arguments are 3
, and 2
. You can say that “parameters are initialized by arguments“.
Unlike function arguments, value of non-type template arguments must be determined at compile-time and the definition of a template with its arguments is called the template-id.
Each parameter in a template parameter list can be one of following types:
- A type template parameter
- A non-type template parameter
- A template template parameter
The Array
class template demonstrates the use of both type (T
), and non-type (N
) template parameters. In the next section, all three parameter types are explored.
Type Template Parameter
The most common template parameter is a type template parameter. A type template parameter starts with either typename
or class
and (optionally) followed by the parameter name. The name of the parameter is used as an alias of the type within the template and has the same naming rules as an identifier used in a typedef or a type alias.
A type template parameter may also define a default argument. If a type template parameter defines a default value, it must appear at the end of the parameter list.
A type template parameter can also be a parameter pack. A parameter pack starts with typename...
(or class...
) and is used to list an unbounded number of template parameters. Since parameter packs apply to all template parameter categories, parameter packs are discussed in the section about template parameter packs.
The following example demonstrates the use of type template parameters.
1 2 3 4 5 6 7 8 9 10 11 |
// typename introduces a type template parameter. template<typename T> class Array { ... }; // class also introduces a type template parameter. template<class T> struct MyStruct { ... }; // A type template parameter with a default argument. template<typename T = void> struct RType { ... }; // The parameter name is optional. template<typename> struct Tag { ... }; |
Non-type Template Parameter
Non-type template parameters can be used to specify a value rather than a type in the template parameter list.
Non-type template parameters can be:
- An integral type (
bool
,char
,int
,size_t
, and unsigned variants of those types) - An enumeration type
- An lvalue reference type (a references to an existing object or function)
- A pointer type (a pointer to an existing object or function)
- A pointer to a member type (a pointer to a member object or a member function of a class)
- std::nullptr_t
C++20 also adds floating-point types and literal class types (with some limitations explained here) to the list of allowed non-type template parameters.
Similar to type template parameters, the name of the template parameter is optional and non-type template parameters may also define default values.
The following shows examples of non-type template parameters:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// Integral non-type template parameter. template<int C> struct Integral {}; // Also an Integral non-type template parameter. template<bool B> struct Boolean {}; // Enum non-type template parameter template<MyEnum E> struct Enumeration {}; // lvalue reference can also be used as a non-type template parameter. template<const int& C> struct NumRef {}; // lvalue reference to an object is also allowed. template<const std::string& S> struct StrRef {}; // Pointer to function template<void(*Func)()> struct Functor {}; // Pointer to member object or member function. template<typename T, void(T::*Func)()> struct MemberFunc {}; // std::nullptr_t is also allowed as a non-type template parameter. template<std::nullptr_t = nullptr> struct NullPointer {}; // Floating-point types are allowed in C++20 template<double N> struct FloatingPoint {}; // Literal class types are allowed in C++20. template<MyClass C> struct Class {}; |
Non-type template parameters must be constant values that are evaluated at compile-time.
Template Template Parameter
Templates can also be used as template parameters:
1 2 3 4 5 6 7 8 |
// C is a template class that takes a single type template parameter. template<template<typename T> class C> struct TemplateClass {}; // C is a template class that takes a type and non-type template parameter. template<template<typename T, size_t N> class C> struct ArrayClass {}; // keyword typename is allowed in C++17. template<template<typename T> typename C> struct TemplateTemplateClass {}; |
Note that until C++17, unlike type template parameters, template template parameters can only use the keyword class
and not typename
.
Template Parameter Pack
A template parameter pack is a placeholder for zero or more template parameters. A template parameter pack can be applied to type, non-type, and template template parameters but the parameter types cannot be mixed in a single parameter pack.
A few examples of template parameter packs:
1 2 3 4 5 6 7 8 9 10 11 |
// Function template with a parmeter pack. template<typename... Args> void func(Args... args); // Type template parameter pack. template<typename... Args> struct TypeList {}; // Non-type template parameter pack. template<size_t... Ns> struct IntegralList {}; // Template template parameter pack. template<template<typename T> class... Ts> struct TemplateList {}; |
A pack that is followed by an ellipsis expands the pack. A pack is expanded by replacing the pack with a comma-separated list of the template arguments in the pack.
For example, if a function template is defined with a parameter pack:
1 2 3 4 5 |
template<typename... Args> void func(Args... args) { std::tuple<Args...> values(args...); } |
And invoking the function:
1 |
func(4, 3.0, 5.0f); |
Will result in the following expansion:
1 2 3 4 |
void func(int arg1, double arg2, float arg3) { std::tuple<int, double, float> values(arg1, arg2, arg3); } |
More examples of using template parameter packs are shown later in the section about variadic templates.
Basics
The following sections introduce the basics of templates. If you are already familiar with templates, then you may want to skip to the next section.
Function Templates
Function templates provide a mechanism to define a function that operates on different types. Function templates look like ordinary functions but start with the template
keyword followed by a list of (one or more) template parameters surrounded by angle brackets.
1 2 3 4 5 |
template<typename T> T max(T a, T b) { return a > b ? a : b; } |
The max
function accepts a single template parameter T
. The max
function template defines a family of functions that can take different types. The type is defined when the function is invoked either by explicitly specifying the type or by allowing the compiler to deduce the type:
1 2 |
int m = max<int>(3, 5); // Explicit type. int n = max(3, 5); // Implicit type deduction. |
On the first line, int
is explicitly provided as the template argument. On the second line, the template argument is not specified but the complier automatically deduces it as int
because the 3
and the 5
are deduced as int
. In both cases, the same function is instantiated.
Implicit template type deduction does not work if you want to mix types as in the following case:
1 |
double x = max(3, 5.0); |
In this case, the max
function template is being instantiated with 3
(int
) and 5.0
(double
) and the compiler does not know how to implicitly determine the template argument. In this case, the compiler will generate an error. There are a few ways this can be fixed:
- Use an explicit template argument
- Cast all function arguments to the same type
- Multiple template parameters
Explicitly specifying the template arguments will ensure that all of the parameters are cast to the correct type through implicit casting:
1 |
double x = max<double>(3, 5.0); |
3
is implicitly cast to a double
. In this case, the compiler may not even issue a warning since this type of cast does not cause any truncation of the original type. However, if a narrowing conversion occurs, the compiler will very likely generate a warning. To avoid this warning, an explicit cast can be used:
1 |
int x = max(3, static_cast<int>(5.0)); |
In this example, an explicit cast is used to convert 5.0
from a double
to an int
. The compiler no longer generates a warning and T
is implicitly deduced to int
.
The other solution to this problem is to allow a
and b
to be different types:
1 2 3 4 5 |
template<typename T, typename U> T max(T a, U b) { return a > b ? a : b; } |
Now a
and b
can be different types and we can call the function template with mixed types without the compiler issuing any warnings… right? What about the return type? If T
is “narrower” than U
then the compiler will have to perform a narrowing conversion again and likely issue a warning when this happens. So what should the return type be? Is it possible to let the compiler decide?
Since the compiler will do anything to prevent data loss, it will try to convert all arguments to the largest (widest) type before performing the comparison and the return type will be the widest type. We can use the auto return type deduction, trailing return type, and the decltype
specifier to automatically determine the safest type to use for the return value of the function template:
1 2 3 4 5 |
template<typename T, typename U> auto max(T a, U b) -> decltype(a > b ? a : b) { return a > b ? a : b; } |
max
function template may still generate a warning about returning a reference to a temporary, but we can use type traits to avoid this. Type traits are discussed later so I won’t complicate this example more than necessary for now. Using this version of the function template, any type can be used for a
and b
and the return value is the widest type of a
or b
. For example:
1 |
auto x = max(3.0, 5); |
x
is automatically deduced to double
because comparing 3.0
(double
) and 5
(int
) results in a conversion to double
and the max
function template returns double
.
The decltype
specifier is explained in more detail later.
There is also a solution to determine the return type using std::common_type
but requires knowledge of type traits which is discussed later.
Class Templates
Similar to Function Templates, classes can also be parameterized with one or more template parameters. The most common use case for class templates are containers for other types. If you have used any of the container types in the Standard Template Library (STL) (such as std::vector
, std::map
, or std::array
), then you have already used class templates. In this section, I describe how to create class templates.
Consider the Array
class template from the previous section. Here it is again for clarity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
template<typename T, size_t N> class Array { public: Array() : m_Data{} {} size_t size() const { return N; } T& operator[](size_t i) { assert(i < N); return m_Data[i]; } const T& operator[](size_t i) const { assert(i < N); return m_Data[i]; } private: T m_Data[N]; }; |
The Array
class template demonstrates two kinds of template parameters:
- Type template parameters (denoted with
typename
orclass
) - Non-type template parameters (denoted with an integral type such as
size_t
)
The Array
class template defines a simple container for a static (fixed-size) array similar to the std::array
implementation from the STL.
Inside the Array
class template, T
can be used wherever a type is expected (such as the declaration of the m_Data
member variable or the return value of a member function) and N
can be used wherever the number of elements is required (such as in the assert
‘s in the index operator member functions).
A class template is instantiated when a variable that uses the class is defined:
1 2 |
Array<float, 3> Position; Array<float, 2> TexCoord; |
Here, Array<float, 3>
represents the type of the Position
variable and Array<float, 2>
is the type of the TexCoord
variable. Although both types are instantiated from the same class template, they are in no way related. You cannot use Array<float, 3>
where an Array<float, 2>
is expected. For example, the following code will not compile:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Array<float, 2> add(const Array<float, 2>& a, const Array<float, 2>& b) { Array<float, 2> c; c[0] = a[0] + b[0]; c[1] = a[1] + b[1]; return c; } ... Array<float, 3> Position1; Array<float, 3> Position2; auto Position3 = add(Position1, Position2); // Error: Array<float, 3> is not compatible with Array<float, 2> |
Although this is a pretty contrived example, it demonstrates that different combinations of template arguments create different (unrelated) types.
Variable Templates
Variable templates were added to the C++ standard with C++14. Variable templates allow you to define a family of variables or static data members of a class using template syntax.
1 2 |
template<typename T> constexpr T pi = T(3.1415926535897932384626433832795L); |
The variable template pi
can now be used with varying levels of precision:
1 2 3 4 |
std::cout << std::setprecision(30); std::cout << PI<int> << std::endl; std::cout << PI<float> << std::endl; std::cout << PI<double> << std::endl; |
Will print:
1 2 3 |
3 3.1415927410125732421875 3.14159265358979311599796346854 |
Variable templates can also have both type and non-type template parameters:
1 2 |
template<typename T, T N> constexpr T integral_constant = N; |
T
is a type template parameter and N
is a non-type template parameter of type T
.
1 2 |
integral_constant<int, 3> i; // ERROR integral_constant<int, 3> is not a type. auto i = integral_constant<int, 3>; // OK: i is an int with the value 3. |
Variable templates can also be specialized:
1 2 3 4 5 6 7 8 |
template<size_t N> constexpr size_t Fib = Fib<N-1> + Fib<N-2>; template<> constexpr size_t Fib<0> = 0; template<> constexpr size_t Fib<1> = 1; |
The Fib
variable template computes the Nth Fibonacci number.
1 |
std::cout << Fib<10> << std::endl; |
This will print 55
to the console.
Variable templates can also be used as a limited form of type traits:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
template<typename T> constexpr bool is_integral = false; template<> constexpr bool is_integral<short> = true; template<> constexpr bool is_integral<int> = true; template<> constexpr bool is_integral<long> = true; // ... specialized for all other integral types. |
If T
is an integral type then is_integral<T>
is true
. For all other types, is_integral<T>
is false
.
Alias Template
Templates can be aliased using the using
keyword:
1 2 3 4 5 |
template<typename T, T v> constexpr T integral_constant = v; template<bool v> using bool_constant = integral_constant<bool, v>; |
On line 5, bool_constant
is defined as an alias template of the integral_constant
variable template where T
is bool
. The value v
remains open as a non-type template parameter.
typename Keyword
Besides being used as the keyword used to introduce a type template parameter, the typename
keyword is also used in a class or function template definition to declare that a type is dependent on a template parameter.
For example, suppose we have the following class template:
1 2 3 4 5 |
template<typename T> struct MyClassTemplate { using type = T; }; |
The MyClassTemplate
class template has a single template parameter (T
) and type
is a type alias of T
.
Now suppose we have a function template that uses MyClassTemplate
:
1 2 3 4 5 6 7 |
template<typename U> U MyFuncTemplate(U a) { MyClassTemplate<U>::type b; // ERROR: Use of dependent type must be prefixed with 'typename' b = a; return b; } |
The MyFuncTemplate
function template has a single template parameter (U
) and declares a local variable (b
) whose type is MyClassTemplate<U>::type
. Since MyClassTemplate<U>::type
names a type and that type is dependent on the template parameter (U
), then MyClassTemplate<U>::type
must be proceeded by typename
:
1 2 3 4 5 6 7 |
template<typename U> U MyFuncTemplate(U a) { typename MyClassTemplate<U>::type b; // OK: Use of dependent type is prefixed with 'typename' b = a; return b; } |
The need for the typename
keyword in this case, can be avoided by using an alias template:
1 2 3 4 5 6 7 8 9 10 |
template<typename T> using MyClassTemplate_t = typename MyClassTemplate<T>::type; template<typename U> U MyFuncTemplate(U a) { MyClassTemplate_t<U> b; // OK: MyClassTemplate_t names a type. b = a; return b; } |
typename
keyword is used to name a type that is dependent on a template parameter unless it was already established as a type (by using a typedef
or a (template) type alias.Template Specialization
Both function templates and class templates can be specialized for specific types. When all template parameters are specialized, then it is called fully specialized. Suppose we have the following function template definition:
1 2 3 4 5 |
template<typename T, typename U> auto add(T a, U b) -> decltype(a + b) { return a + b; } |
The function template can be specialized by declaring the function with an empty template parameter list template<>
and specifying the specialized template arguments after the function name:
1 2 3 4 5 |
template<> double add<double, double>(double a, double b) { return a + b; } |
All occurrences of template parameters (T
and U
) in the function must also be replaced with the specialized template arguments (double
).
1 2 3 4 |
double add(double a, double b) { return a + b; } |
It is perfectly legal to overload function templates in this way.
The compiler will use the specialized (or overloaded) version of the function template if all of the substituted template arguments (either explicitly or implicitly) match the specialized version:
1 2 |
float a = add(3.0f, 4.0f); // Uses generic version. double b = add(3.0, 4.0); // Uses specialized version for doubles. |
Similar to function templates, class templates can also be specialized. If we take the Array
class template from the Class Template section and we want to specialize it for 4-component floating-point values:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
template<> class Array<float, 4> { public: Array() : m_Vec(_mm_setzero_ps()) {} size_t size() const { return 4; } float& operator[](size_t i) { assert(i < 4); return m_Data[i]; } const float& operator[](size_t i) const { assert(i < 4); return m_Data[i]; } private: union { __m128 m_Vec; // Vectorized data. float m_Data[4]; // Float data. }; }; |
The specialized version of the Array
class template allows you to provide a different implementation of the class depending on its template arguments. In this case, we provide a specialization for Array<float, 4>
that allows for some SSE optimizations to be made.
It is important to note that if you specialize a class template, you must also specialize all of the member functions of that class. This can be quite cumbersome for large classes, especially if you decide to refactor the generic class template, you must also update all specialized versions of the class.
Keep in mind that the compiler will only generate code for class template member functions that are used. That is, if you never call a specific member function of a specialized class template, then no code will be generated for that version of the member function. If a specialized class template defines a member function that just doesn’t make any sense for a certain specialized type, and you are sure that the member function is not being used anywhere in the codebase, then you can leave that function undefined in the specialized version of the class template.
Partial Specialization
Although it is not possible to partially specialize function templates, we can achieve something similar by using function template overloading. Let’s consider the max
function template introduced in the previous section on Function Templates. Suppose we want to provide an implementation for pointer types:
1 2 3 4 5 |
template<typename T, typename U> auto max(const T* a, const U* b) -> decltype(*a > *b ? *a : *b) { return *a > *b ? *a : *b; } |
This version of the function template is used whenever pointers are used as the arguments to the function as in the following example:
1 2 3 4 |
double d = 3.0; int i = 5; auto k = max(&d, &i); // Uses double max(const double* a, const int* b); |
Unlike function templates, class templates can be partially specialized. Special implementations of class templates can be created to handle specific situations. Consider the Array
class template from before. A special implementation can be created if the array holds pointer types:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
template<typename T, size_t N> class Array<T*, N> { public: explicit Array(T* data) : m_Data(data) {} T& operator[](size_t i) { assert(i < N); return m_Data[i]; } const T& operator[](size_t i) const { assert(i < N); return m_Data[i]; } private: T* m_Data; }; |
A class template can be partially specialized by specifying the template
keyword followed by a list of template parameters surrounded by angle brackets, just as with the non-specialized class template. The specialized template parameters are specified after the class name (T*
and N
in this case).
This implementation of the Array
class template will be used when T
is a pointer type. This may not seem like a very useful thing, but now we have a class template that can provide all the functionality of the original Array
class template, but instead of allocating a static array, it now works with arbitrary data that is allocated elsewhere.
1 2 3 4 |
float p[16] = {}; auto a = Array<float*, 16>(p); // a provides all the functionality of Array on arbitrary data. a[8] = 1.0f; |
Similarly to a fully specialized class template, if one of the template parameters is fully defined then it does not need to be listed in the template parameter list, but must be specified in the template argument list (after the class name):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Specialized for arrays of size 4 and arbitrary type. template<typename T> class Array<T, 4> { ... }; // Specialized for float arrays of arbitrary size. template<size_t N> class Array<float, N> { ... }; |
Partial template specialization is the cornerstone of type traits and SFINAE.
Variadic Templates
Variadic templates are function templates or class templates that can accept an unbounded number of template arguments.
For example, suppose we want a function that creates an std::unique_ptr
from an arbitrary type:
1 2 3 4 5 |
template<typename T, typename... Args> std::unique_ptr<T> make_unique(Args&&... args) { return std::unique_ptr<T>(new T(std::forward<Args>(args)...)); } |
std::make_shared
was introduced in C++11 but std::make_unique
wasn’t introduced until C++14. This example provides a possible implementation of std::make_unique
for C++11 compilers.There are a few things to note here:
- The template parameter list contains a template parameter pack in the form of
typename... Args
- An arbitrary number of arguments are passed to the function in the form of
Args&&...
. Not to be mistaken by an rvalue reference, this is called a forwarding reference which preserves the value category of the function arguments when used in conjunction withstd::forward
- The arguments are unpacked by performing a pack expansion which replaces the parameter pack by a comma-separated list of the arguments using the pattern immediately preceding the
...
(ellipsis)
For example, suppose we have the following class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Object { public: Object(int i, float f, int* ip, double d) : m_i(i) , m_f(f) , mp_i(ip) , m_d(d) {} private: int m_i; float m_f; int* mp_i; double m_d; }; |
And if the make_unique
function template was invoked with:
1 2 3 4 5 |
int i = 3; float f = 4.0f; double d = 6.0; auto o = make_unique<Object>(i, f, &i, d); |
Then the make_unique
function would generate something like this:
1 2 3 4 |
std::unique_ptr<Object> make_unique(int args0, float args1, int* args2, double args3) { return std::unique_ptr<Object>(new Object(std::forward<int>(args0), std::forward<float>(args1), std::forward<int*>(args2), std::forward<double>(args3))); } |
Recursive Variadic Templates
Suppose you want to write a function that prints an arbitrary number of arguments to the standard output stream. Using a C++17 compiler, this can be accomplished using fold expressions:
1 2 3 4 5 |
template<typename... Args> void print(Args... args) { (std::cout << ... << args) << std::endl; } |
But how could this be accomplished without a C++17 compiler? To accomplish this without fold expressions, we need to create a recursive template function. To do this we must:
- Define the base case (only a single template parameter)
- Define the recursive case (where multiple template parameters are passed in a parameter pack)
First, let’s define the case where only a single argument is passed:
1 2 3 4 5 |
template<typename Arg> void print(Arg arg) { std::cout << arg << std::endl; } |
And the recursive case with a parameter pack:
1 2 3 4 5 6 |
template<typename Arg, typename... Args> void print(Arg arg, Args... args) { std::cout << arg; print(args...); } |
The subtle trick here is that the recursive case has two template parameters:
typename Arg
typename... Args
This way, the first argument can be extracted from the parameter pack and the rest of the arguments are passed to the recursive print
function. When there is only a single argument left in the parameter pack, the base case (with a single template argument) is used.
At this point, you should have a pretty good idea of how to use templates in your code. In the following sections, I will show a few more complex uses of templates.
Template Type Deduction
Template type deduction is the process the compiler performs to determine the type that is used to instantiate a function or class template. Many programmers use templates with a reasonable amount of success without really understanding how template type deduction works. This might be sufficient for simple use cases but becomes complicated (and perhaps unintuitive) in more complex applications of templates.
Understanding template type deduction forms the foundation for understanding how the decltype
specifier works.
Scott Meyers provides a very good explanation of how type deduction works[7]. I will attempt to summarize Scott Meyers’ explanation here.
As we’ve seen in previous examples, function templates have the following basic form:
1 2 |
template <typename T> void f(ParamType param); |
In the snippet above, T
and ParamType
may be different in the case that ParamType
has modifiers (const
, pointer (*
), or reference (&
) qualifiers). For example, if the template is declared like this:
1 2 |
template<typename T> void f(const T& param); // ParamType is const T& |
Now suppose the template function is invoked like this:
1 2 |
int x = 0; f(x); // Call f with an int (lvalue) |
In this case, T
is deduced to int
and ParamType
is deduced to const int&
.
In this case, it seems obvious that T
is deduced to int
since f
was invoked with an int
argument, but it’s not always that obvious. The type deduced for T
is dependent on not only the argument type, but also the form of ParamType
. There are three forms of ParamType
that must be considered:
ParamType
is a pointer or reference type, but not a forwarding referenceParamType
is a forwarding referenceParamType
is neither a pointer nor a reference
&&
) being applied to template parameters (not to be mistaken with rvalue references). Since the C++ standard uses the term forwarding reference, I will use that term in this article.std::forward
.For each of the three cases, consider the general form of invoking the template function:
1 2 3 4 |
template <typename T> void f(ParamType param); f(expr); // Deduce T and ParamType from expr. |
Case 1: ParamType is a Reference or Pointer
In the first case, ParamType
is a reference or pointer type, but not a forwarding reference. In this case, type deduction works like this:
- If
expr
evaluates to a reference, ignore the reference part. - Then match
expr
‘s type againstParamType
to determineT
.
For example, if the function template is declared like this:
1 2 |
template<typename T> void f(T& param); // param is a reference. |
Then given the following variables:
1 2 3 4 5 6 7 |
int i = 3; // i is an int. const int ci = i; // ci is a const int. const int& rci = i; // rci is a reference to a const int. f(i); // T is int, param's type is int& f(ci); // T is const int, param's type is const int& f(rci); // T is const int, param's type is const int& |
Notice on lines 6, and 7 where expr
is a const int
or const int&
, then T
is deduced to be const int
. The constness of expr
becomes part of the type deduced for T
.
If, on the other hand, ParamType
is changed to const T&
then type deduction works slightly differently:
1 2 3 4 5 6 7 8 9 10 |
template<typename T> void f(const T& param); // param is now a reference to const T. int i = 3; // i is an int. const int ci = i; // ci is a const int. const int& rci = i; // rci is a reference to a const int. f(i); // T is int, param's type is const int& f(ci); // T is int, param's type is const int& f(rci); // T is int, param's type is const int& |
Since the constness is now part of param
‘s type, there is no need for const
to be part of T
‘s type deduction.
If param
were a pointer or a pointer to const
, then the type deduction for T
works the same way:
1 2 3 4 5 6 7 8 |
template<typename T> void f(T* param); // param is now a pointer to T. int i = 3; // i is an int. const int* pi = &i; // pi is a pointer to const int. f(&i); // T is int, param's type is const int* f(pi); // T is const int, param's type is const int* |
This may seem obvious so far, but becomes less obvious if ParamType
is a forwarding reference.
Case 2: ParamType is a Forwarding Reference
Now let’s consider the case where ParamType
is a forwarding reference:
1 2 3 4 5 6 7 8 9 10 11 |
template<typename T> void f(T&& param); // param is now a forwarding reference. int i = 3; // i is an int (lvalue). const int ci = i; // ci is a const int (lvalue). const int& rci = i; // rci is a reference to a const int (lvalue). f(i); // T is int&, param's type is also int& f(ci); // T is const int&, param's type is also const int& f(rci); // T is const int&, param's type is also const int& f(3); // T is int, param's type is int&& |
On line 4, an int
variable (i
) is defined. This is an lvalue
(according to the value category rules defined at the beginning of this article). On line 9, i
is passed to f
. In this case, ParamType
is deduced to int&
(lvalue reference) and T
is deduced to int&
(lvalue reference).
On line 9, ci
(lvalue) is passed to f
and ParamType
is deduced to const int&
(lvalue reference) and T
is deduced to const int&
(lvalue reference). Similar deduction rules are applied to rci
on line 10.
On line 11, the value 3
(prvalue, which is a primary value category of rvalue) is passed to f
. In this case ParamType
is deduced to int&&
(rvalue reference) and T
is deduced to int
. The deduction rules for rvalues are the same as Case 1 above (expr
‘s type is matched against ParamType
to determine T
).
The general rules of type deduction when ParamType
is a forwarding reference, are:
- If
expr
is an lvalue, bothT
andParamType
are deduced to be lvalue references. - If
expr
is a rvalue (prvalue or xvalue), then the rules for Case 1 are applied.
In short, use a forwarding reference when you need to maintain the value category of the template argument. This is almost always the case when the arguments are being forwarded to another function.
Case 3: ParamType is Neither a Pointer nor a Reference
If ParamType
is neither a pointer nor a reference, then we say that the parameter is passed-by-value:
1 2 |
template<typename T> void f(T param); // param is now passed-by-value. |
In this case, the rules for type deduction are:
- If
expr
‘s type is a reference, ignore the reference part - If
expr
‘s type is const, ignore that too.
Then we have:
1 2 3 4 5 6 7 |
int i = 3; // i is an int. const int ci = i; // ci is a const int. const int& rci = i; // rci is a reference to a const int. f(i); // T is int, param's type is also int f(ci); // T is int, param's type is also int f(rci); // T is int, param's type is also int |
Note that despite ci
and rci
being const
values, param
doesn’t become const
. Just becase expr
can’t be modified doesn’t mean that a copy of it can’t be.
This pretty much summarizes the rules that are applied during template parameter type deduction. There are a few more cases that can be considered, for example how static arrays and function objects decay to their pointer types (see the decay type trait for more information). I encourage the reader to consult Scott Meyers’ books[7] for more information regarding the edge cases of template parameter type deductions. But at this point, you should have a good foundation for understanding the decltype
specifier and std::declval
which is the subject of the next sections.
decltype Specifier
The decltype
specifier is used to inspect the declared type and value category of an expression.
Earlier, in the section about Function Templates, the decltype
specifier was used to determine the return type for the max
function template. If you read the warning that followed the code example, you might know that in certain cases, the compiler will generate a warning about returning a reference to a temporary. But under what circumstances does this happen?
In most cases, the decltype
produces the expected type:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
int h = 0; int* i = &h; int& j = h; const int k = 0; const int* l = &k; const int& m = k; decltype(h) n = 0; // n is int decltype(i) o = &n; // o is int* decltype(j) p = n; // p is int& decltype(k) q = 0; // p is const int decltype(l) r = &q; // r is const int* decltype(m) s = q; // s is const int& |
No surprises here. decltype
gives the exact type as the provided expression maintaining const
(and volatile
), reference (&
) and pointer (*
) attributes.
When the the expression passed to decltype
is parenthesized, then the expression is treated as an lvalue and decltype
adds a reference to the expression:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
int h = 0; int* i = &h; int& j = h; const int k = 0; const int* l = &k; const int& m = k; decltype((h)) n = h; // n is int& decltype((i)) o = i; // o is int*& decltype((j)) p = j; // p is int& decltype((k)) q = k; // q is const int& decltype((l)) r = l; // r is const int*& decltype((m)) s = m; // s is const int& |
Okay, but this doesn’t explain why the max
function template can sometimes return a reference. To understand when this happens, we need to look at the deduction guide for the ternary (conditional) operator. The ternary operator has the form:
1 |
condition ? expr1 : expr2 |
First, condition
is evalutated and (implicitly converted) to bool
. If the result is true
, then expr1
is evaluated. If the result is false
, then expr2
is evaluated. The deduction rules for the resulting value of the ternary operator are complex and you can read the full guide here.
One of the rules of the deduction guide for the ternary operator states that if expr1
and expr2
are glvalues (lvalues or xvalues) of the the same type and the same value category, then the result has the same type and value category.
Here is the max
function template again:
1 2 3 4 5 |
template<typename T, typename U> auto max(T a, U b) -> decltype(a > b ? a : b) { return a > b ? a : b; } |
So if T
and U
are the same type and value category (see Case 3 above when the template parameter is passed-by-value) then decltype(a > b ? a : b)
will have the same type and value category of both a
and b
. If both a
and b
are the same type and they are always both lvalues (since they are identified by a name), then in order to avoid creating a temporary, decltype(a > b ? a : b)
results in an lvalue references that refers to either a
or b
(whichever is larger).
Let’s take a look at a few examples of this:
1 2 3 4 5 6 7 8 9 |
int a = 3; int b = 5; double c = 3.0; double d = 5.0; decltype(a > b ? a : b) g; // g is int& decltype(a > c ? a : c) h; // h is double decltype(c > d ? c : d) i; // i is double& decltype(3 > 5 ? 3 : 5) k; // k is int |
On line 6, both a
and b
are int
s and they are both lvalues. The result of the ternary operator is an lvalue reference.
On line 7, a
is an int
and c
is a double
. In this case, they are not the same type and the result of the ternary operator is the the type of the widest operand (in this case, double
)
On line 8, both c
and d
are double
s and they are both lvalues. The result of the tenary operator is an lvalue reference.
On line 9, both operands are prvalues of type int
. In this case, the result of the ternary operator is also a prvalue.
Consequently, the result of the ternary operator has an interesting side effect that you should be aware of. You can sometimes assign a value to the result of the ternary operator:
1 2 3 4 5 6 |
int a = 3; int b = 5; ( a > b ? a : b ) = 10; // OKAY, b now has the value 10. ( 3 > b ? 3 : b ) = 10; // ERROR: expression must be a modifiable lvalue. ( 3 > 5 ? 3 : 5 ) = 10; // ERROR: expression must be a modifiable lvalue. |
In the case where the the result of the ternary operator is an lvalue reference, you can actually assign a value to that result. In all other cases, the result of the ternary operator is a temporary prvalue that can’t be modified directly (unless it is stored in a lvalue first).
Okay, you’ve probably heard enough about the ternary operator. This should be enough knowledge to know under which circumstance the max
function template returns a reference, but how can we fix this? In later sections, I’ll talk about using type traits to coerce declval
to give us what we want. But before we get to type traits, there is one more tool we need in our template toolbox, and that’s the std::declval
.
std::declval()
std::declval
is a utility function that converts any type to a reference type without the need to create an instance of an object.
Okay, maybe that doesn’t help to understand why we need std::declval
, so let’s take a look at an example. Suppose we have an abstract base class:
1 2 3 4 5 |
struct Abstract { virtual ~Abstract() = default; virtual int value() const = 0; }; |
We know that Abstract
is an abstract type because it declares at least one pure virtual function. Pure virtual functions are not required to (but may) have a definition. Classes with pure virtual functions are called abstract classes and cannot be instantiated.
Now suppose we wanted to determine the type that is returned from the Abstract::value
method. As explained in the previous section, we can use the decltype
keyword for this:
1 2 3 |
decltype(Abstract().value()) a; // ERROR: cannot instantiate abstract class. decltype(Abstract::value()) b; // ERROR: illegal call of non-static member function. decltype(std::declval<Abstract>().value()) c; // OK: c is type int. |
On line 10, we try to to determine the return type by constructing an instance of Abstract
and inspect the return value of the value
method. In this case, the compiler complains since, as was previously established, Abstract
is an abstract class and can’t be instantiated (even in unevaluated expression like the decltype
operator).
On line 11, we try to determine the return value by using a scope resolution operator (::
). This only works if Abstract::value
is a static function.
On line 12, the std::declval
function template allows for the use of non-static member functions of abstract base classes, (or with types with deleted or private constructors, which is common when dealing with singletons) without requiring an instance of the type.
The std::declval
utility function can be implemented like this:
1 2 |
template<typename T> typename std::add_rvalue_reference<T>::type declval() noexcept; |
We haven’t looked at type traits yet, but I think you can guess that std::add_rvalue_reference<T>
makes T
an rvalue reference.
You may have noticed that the declval
function template only provides a declaration but not a definition. This is no mistake. This function does not have a definition! It simply converts T
to an rvalue reference so that it can be used in an unevaluated context such as decltype
.
std::declval
converts any type (T
) to a reference type to enable the use of member functions without the need to construct an instance of T
.
Since std::declval
is not defined and therefore, it can only be used in an unevaluated context such as decltype
.
Type Traits
C++11 introduces the type_traits
library.
Type traits defines a compile-time template-based interface to query or modify the properties of types.
Type traits allow you to discover certain things about a type at compile-time. Some things you may want to know about types are:
- Is it an integral type?
- Is it a floating-point type?
- Is it a class type?
- Is it a function type?
- Is it a pointer type?
- Are two types the same?
- Is one type derived from the other?
- Is one type convertible to another?
And the list goes on… There are many things we might want to know about one or more types that can be determined at compile-time.
Don’t confuse type traits with Run-Time Type Information (RTTI) which is used to query type information at run-time. Type traits are resolved at compile-time and impose no run-time overhead.
Type traits are the cornerstone for “Substitution Failure Is Not An Error” (SFINAE). But before we look at SFINAE, let’s investigate a few type traits that we can use as the basis for SFINAE later.
Keep in mind that a lot of the type traits described in following sections comes from the Standard Template Library (STL). You don’t need to define these types yourself in your own code. You can find the original source code for the type_traits
library here:
- Microsoft STL: https://github.com/microsoft/STL
- GCC: https://github.com/gcc-mirror/gcc
- Clang/LLVM: https://github.com/llvm/llvm-project
My motivation for describing the type traits in this article is to give the reader a better understanding of how they work. Once you know how they work, you will have a better understanding of how to use them correctly.
integral_constant
The integral_constant
structure is the base class for the type traits library. It is a wrapper for a static constant of a specified type. It wraps both the type and a value in a struct so it can be used as a type. You’ll see why this is useful later.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
template<typename T, T v> struct integral_constant { // Member types using value_type = T; using type = integral_constant; // Member constants static constexpr T value = v; // Member functions constexpr operator value_type() const noexcept { return value; } constexpr value_type operator()() const noexcept { return value; } }; |
The integral_constant
class (struct) template is composed of a type template parameter (T
) and a non-type template parameter v
(of type T
).
The value type that was used to instantiate the integral_constant
can be queried through the value_type
type alias and the type of the integral_constant
itself can be queried through the type
type alias.
The operator value_type()
member function defined on line 12 is an implicit conversion operator which allows an instance of the integral_constant
template to be converted to value_type
at compile-time. This allows an instance of integral_constant
to be used in place where value_type
is expected (in mathematical expressions for example).
The value_type operator()
member function defined on line 17 is a function call operator that takes no parameters and returns value
. This allows integral_constant
to be used as a function object that takes no parameters and returns the stored value.
1 2 3 4 5 6 7 8 9 10 |
using three_t = integral_constant<int, 3>; using five_t = integral_constant<int, 5>; three_t three; five_t five; auto fifteen = three_t() * five_t(); fifteen = three * five; std::cout << "3 * 5 = " << fifteen << std::endl; |
On lines 1 and 2, two type aliases of the integral_constant
template are defined: three_t
which is a type that represents 3
, and five_t
which is a type that represents 5
.
On lines 4 and 5, two instances are instantiated using the type aliases that were just defined. three
is an instance of type_constant<int, 3>
and five
is an instance of type_constant<int, 5>
.
On line 7 and 8, two different methods to get the internal value are demonstrated. The first method on line 7 uses the function call operator to retrieve the internal value. On line 8, the implicit conversion operator is used to convert three
and five
to their integer equivalents to be multiplied together. Both expressions result in 15. If you run this program, the following is printed to the console:
3 * 5 = 15
That might be the most contrived method of computing the value 15 I’ve ever seen, but in the next section it will be become clear why this is useful.
bool_constant
With the definition of integral_constant
from the previous section, other types can be derived from integral_constant
. One useful type that can be derived from integral_constant
is bool_constant
. It is not necessary to define a full class for this as a template alias will do:
1 2 |
template<bool v> using bool_constant = integral_constant<bool, v>; |
The bool_constant
defines a “boolean” integral_constant
. And as you may have guessed, we can define two new types based on bool_constant
.
true_type
true_type
is a type alias of bool_constant
with a value
of true
:
1 |
using true_type = bool_constant<true>; |
false_type
false_type
is a type alias of bool_constant
with a value
of false
:
1 |
using false_type = bool_constant<false>; |
Both true_type
and false_type
are aliases of integral_type
which means that they can be used wherever a class or struct type can be used. For example, we can create a partial specialization of a class that is derived from either true_type
or false_type
. But before I get into that, I need to introduce another useful tool for our type traits library: enable_if
.
type_identity
At first glance, the type_identity
class template may not seem very useful. It simply mimics the type T
that was specified in the template argument.
1 2 3 4 5 6 7 8 |
template<class T> struct type_identity { using type = T; }; template<class T> using type_identity_t = typename type_identity<T>::type; |
The type_identity
class template becomes useful in a non-deduced context (for example when used with the decltype
specifier). We’ll use it later to help form SFINAE enabled types (see add_lvalue_reference
and add_rvalue_reference
).
void_t
void_t
is an alias template that maps any number of type template parameters to void
. This is useful in the context of SFINAE where you only want to check if a certain set of operations is valid on a type but you don’t care about the return type of that check. void_t
allows you to form these expressions without concern for the return type.
1 2 |
template<class... T> using void_t = void; |
The void_t
alias template is commonly used to check if a certain operation is valid on a specific type. For example, if we want to check if a type supports the pre-increment operator, but we don’t care about the the actual result type, then we could do something like this:
1 2 3 4 5 6 7 |
template<class, class = void> struct has_pre_increment_operator : false_type {}; template<class T> struct has_pre_increment_operator<T, void_t<decltype(++std::declval<T&>())>> : true_type {}; |
In this example, the primary template for has_pre_increment_operator
is derived from false_type
. The primary template is only chosen if there isn’t a partial specialization that is a better match. In order to get the compiler to choose the specialized version of the template definition, the operation ++std::declval<&T>()
must succeed. But since we only want to see if the operation succeeded, but we don’t care what the return value is, we can wrap the result of performing the pre-increment operator in the void_t
alias template.
Since void_t
takes a variadic number of template parameters, void_t
can be used to check if any number of operations are valid on one or more types.
When the compiler fails to instantiate the second template argument in the specialized version of the has_pre_increment_operator
, this is called substitution failure and is the basis of SFINAE (Substitution Failure Is Not An Error). SFINAE is used to express type trait operations and the void_t
template alias is used to help formulate those expressions.
enable_if
The enable_if
struct template provides a convenient mechanism to leverage SFINAE in our template classes. “Substitution Failure Is Not An Error” (or SFINAE) is a C++ technique to conditionally remove specific functions from overload resolution based on a type’s traits. We’ll get into more details of SFINAE later, for now let’s take a look at how we can define the enable_if
template:
1 2 3 4 5 6 7 8 9 |
template<bool B, class T = void> struct enable_if {}; template<class T> struct enable_if<true, T> { using type = T; }; |
The primary template for enable_if
is a class template that has two template parameters:
bool B
: A non-type template parameter that can be eithertrue
orfalse
.class T
: A type template parameter that can be any type. If no type is provided,void
is used by default.
The magic trick comes from the partial specialization that is defined when B
is true
. The type
type alias is not defined in the primary template and is only defined when B
is true
. Any attempt to access the type
type alias when B
is false
will fail (since it’s just not defined in this case).
We’ll see how we can use this to leverage SFINAE later. Before we can do that, we’ll need some type trait templates to work with.
To simplify coding, we’ll also define a helper template alias for the enable_if
template:
1 2 |
template<bool B, class T = void> using enable_if_t = typename enable_if<B, T>::type; |
Now, instead of typing typename enable_if<B, T>::type
, we only need to type enable_if_t<B, T>
. As you can see, this saves us a lot of typing.
enable_if_t
template alias was introduced in C++14 but there is nothing stopping you from defining these kinds of template aliases in your C++11 code.remove_const
Sometimes is it convenient to express a type without the const
qualifier associated with it. The remove_const
class template allows us to do just that:
1 2 3 4 5 6 7 8 9 10 11 |
template<class T> struct remove_const { using type = T; }; template<class T> struct remove_const<const T> { using type = T; }; |
The primary template for remove_const
is only used when T
is a non-const type. If T
is const, then the partial specialization kicks-in to remove the const modifier from T
.
And again, we’ll also define a helper template alias:
1 2 |
template<class T> using remove_const_t = typename remove_const<T>::type; |
remove_const
class template only removes the topmost const qualifier from T.remove_volatile
Similar to remove_const
, the remove_volatile
class template can be used to remove the volatile
qualifier from a type:
1 2 3 4 5 6 7 8 9 10 11 |
template<class T> struct remove_volatile { using type = T; }; template<class T> struct remove_volatile<volatile T> { using type = T; }; |
And the helper template alias:
1 2 |
template<class T> using remove_volatile_t = typename remove_volatile<T>::type; |
remove_cv
The remove_cv
class template is used to remove the topmost const
, volatile
, or both qualifiers if present:
1 2 3 4 5 6 7 8 9 |
template<class T> struct remove_cv { using type = remove_const_t<remove_volatile_t<T>>; }; // Helper template alias. template<class T> using remove_cv_t = typename remove_cv<T>::type; |
In this example, the remove_const
and remove_volatile
are combined on line 4 to remove both const
and volatile
qualifiers from T
(if present).
remove_reference
The remove_reference
class template is used to remove any reference (or rvalue references) from a type.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
template<class T> struct remove_reference { using type = T; }; template<class T> struct remove_reference<T&> { using type = T; }; template<class T> struct remove_reference<T&&> { using type = T; }; // Helper template alias. template<class T> using remove_reference_t = typename remove_reference<T>::type; |
If T
is a reference (or rvalue reference) type, then the remove_reference
class template will strip off the reference from the type.
remove_cvref
Now we can combine remove_cv
and remove_reference
to remove const
, volatile
, and references from a type:
1 2 3 4 5 6 7 8 |
template<class T> struct remove_cvref { using type = remove_cv_t<remove_reference_t<T>>; }; template<class T> using remove_cvref_t = typename remove_cvref<T>::type; |
remove_extent
It might be useful to extract the type of an array. The remove_extent
class template can be used to extract the type of an array. For example, if T
is an (bounded or unbounded) array of type X
, then remove_extent_t<T>
evaluates to X
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
template<class T> struct remove_extent { using type = T; }; template<class T> struct remove_extent<T[]> { using type = T; }; template<class T, std::size_t N> struct remove_extent<T[N]> { using type = T; }; |
The primary template (lines 1-5) is used if T
is not an array type. If T
is an unbounded array (lines 7-11) or a bounded array (lines 13-17), then remove_extent<T>::type
is defined to be the type of the array with the extents removed.
T
is a multidimensional array, remove_extent<T>
only removes the first dimension.A short-hand for the remove_extent
class template:
1 2 |
template<typename T> using remove_extent_t = typename remove_extent<T>::type; |
remove_all_extents
Since the remove_extent
class template only removes the first dimension of multidimensional arrays, the remove_all_extents
class template can be used to remove all of the extents of multidimensional arrays.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
template<class T> struct remove_all_extents { using type = T; }; template<class T> struct remove_all_extents<T[]> { using type = typename remove_all_extents<T>::type; }; template<class T, std::size_t N> struct remove_all_extents<T[N]> { using type = typename remove_all_extents<T>::type; }; |
The primary template (lines 1-5) for the remove_all_extents
class template looks similar to the remove_extent
class template. The primary template is only used when T
is not an array type or all of the extents have already been removed from the type.
If T
is an unbounded (line 7-11) or bounded (line 13-17) array type, then the appropriate partial specialization is used. In this case, the first dimension is removed from T
and the remove_all_extents
class template is invoked recursively to remove the next extent. This process continues until the primary template is reached.
And the shorthand form:
1 2 |
template<class T> using remove_all_extents_t = typename remove_all_extents<T>::type; |
remove_pointer
The remove_pointer
class template can be used to remove the pointer from a type. Any const
or volatile
qualifiers added to the pointer are also removed.
Any const
or volatile
qualifiers added to the pointed-to type are not removed. For example, const int*
becomes const int
, but int* const
becomes int
and const int* const
becomes const int
. If you also want to remove the const
or volatile
qualifiers from the pointed-to type, then you must use the remove_cv
class template as well.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
template<class T> struct remove_pointer { using type = T; }; template<class T> struct remove_pointer<T*> { using type = T; }; template<class T> struct remove_pointer<T* const> { using type = T; }; template<class T> struct remove_pointer<T* volatile> { using type = T; }; template<class T> struct remove_pointer<T* const volatile> { using type = T; }; |
The primary template (lines 1-5) is used if T
is not a pointer type. Partial specializations (lines 7-29) are used if T
is a (const
or volatile
qualified) pointer type.
And a short-hand version:
1 2 |
template<class T> using remove_pointer_t = typename remove_pointer<T>::type; |
add_const
In some cases, you may want to add the const
qualifier to a type. The add_const
class template can be used to add the const
qualifier to any type T
(except for function and reference types, since these can’t be const
qualified).
1 2 3 4 5 6 7 8 9 10 11 |
#pragma warning(push) #pragma warning(disable: 4180) // Disable C4180: qualifier applied to function type has no meaning; ignored template<class T> struct add_const { using type = const T; }; #pragma warning(pop) template<class T> using add_const_t = typename add_const<T>::type; |
Due to const
collapsing rules, if T
is already const
, then adding another const
to T
does not change the constness of T
.
If we try to apply the add_const
class template to a function type, then the compiler will generate a C4180 warning that applying a const
to a function type does not make sense and has no meaning. #pragma warning(disable: 4180)
causes this warning to be suppressed in Visual Studio.
add_volatile
Similar to add_const
class template, the add_volatile
class template can be used to add the volatile
qualifier to a type (except for function and reference types, since these can’t be volatile
qualified).
1 2 3 4 5 6 7 8 9 10 11 |
#pragma warning(push) #pragma warning(disable: 4180) // Disable C4180: qualifier applied to function type has no meaning; ignored template<class T> struct add_volatile { using type = volatile T; }; #pragma warning(pop) template<class T> using add_volatile_t = typename add_volatile<T>::type; |
Similar to the add_const
class template, the add_volatile
class template, adds the volatile
qualifier to a type (T
). Adding the volatile
qualifier if T
is already volatile
does not change T
.
Similar to add_const
, if T
is a function type, then the compiler will generate a C4180 warning. #pragma warning(disable: 4180)
suppresses this warning in Visual Studio.
add_cv
The add_cv
template combines add_const
and add_volatile
.
1 2 3 4 5 6 7 8 9 10 11 |
#pragma warning(push) #pragma warning(disable: 4180) // Disable C4180: qualifier applied to function type has no meaning; ignored template<class T> struct add_cv { using type = const volatile T; }; #pragma warning(pop) template<class T> using add_cv_t = typename add_cv<T>::type; |
Similar to add_const
and add_volatile
, we also need to suppress the C4180 compiler warning if we try to use add_cv
with a function type.
add_lvalue_reference
The add_lvalue_reference
class template is used to create an lvalue reference from a type. We have to be careful since trying to add a reference to non-referenceable type (for example, void
is a non-referenceable type) will generate a compilation error. To account for this, we’ll use a helper template that is specialized for referenceable types. Trying to use add_lvalue_reference
with a non-referenceable type should produce the original type.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
template<class T, class = void> struct add_lvalue_reference_helper { using type = T; }; template<class T> struct add_lvalue_reference_helper<T, void_t<T&>> { using type = T&; }; template<class T> struct add_lvalue_reference : add_lvalue_reference_helper<T> {}; template<class T> using add_lvalue_reference_t = typename add_lvalue_reference<T>::type; |
The add_lvalue_reference_helper
class template uses SFINAE to safely convert T
to T&
. If T
is a referenceable type, then the partial specialization (lines 7-11) succeeds and add_lvalue_reference_helper<T>::type
evaluates to T&
. If the partial specialization fails, then T
is a non-referenceable type, and the primary template is chosen. In this case, add_lvalue_reference_helper<T>::type
evaluates to T
.
On lines 13-15, the add_lvalue_reference
class template is defined which is derived from add_lvalue_reference_helper
allowing the add_lvalue_reference
to be defined with a single template parameter. Theoretically, the add_lvalue_reference
class template could be implemented without add_lvalue_reference_helper
, but then we’d need to define add_lvalue_reference
using two template parameters. Since the second template parameter is only used for SFINAE, using add_lvalue_reference_helper
allows us to define the add_lvalue_reference
class template using a single template parameter.
add_rvalue_reference
Similar to add_lvalue_reference
, the add_rvalue_reference
class template is used to create an rvalue reference from a type. Once again, we’ll use a helper class template to account for non-referenceable types (such as void
).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
template<class T, class = void> struct add_rvalue_reference_helper { using type = T; }; template<class T> struct add_rvalue_reference_helper<T, void_t<T&&>> { using type = T&&; }; template<class T> struct add_rvalue_reference : add_rvalue_reference_helper<T> {}; template<class T> using add_rvalue_reference_t = typename add_rvalue_reference<T>::type; |
The derivation for the add_rvalue_reference
class template is similar to that of add_lvalue_reference
, so I won’t repeat it here.
add_rvalue_reference<T&>
will result in T&
not T&&
. There are two reference collapsing rules:
- An rvalue reference to an rvalue reference becomes becomes an rvalue reference.
- All other references to references (all combinations involving an lvalue reference) becomes an lvalue reference.
In the case where T
is an lvalue reference, then add_rvalue_reference<T&>
will collapse into a lvalue reference[8].
add_pointer
Similar to add_lvalue_reference
and add_rvalue_reference
class templates, the add_pointer
class template adds a pointer to a give type T
. If T
is a reference type, then add_pointer<T>::type
becomes remove_reference_t<T>*
, that is, a pointer is added to the type T
, after removing the reference.
T*&
), it is not possible to add a pointer to a reference (T&*
). Attempting to create a pointer to a reference type will result in a compiler error.
1 2 3 4 5 6 7 8 9 10 11 12 |
template<class T> auto add_pointer_helper(int) -> type_identity<remove_reference_t<T>*>; template<class T> auto add_pointer_helper(...) -> type_identity<T>; template<class T> struct add_pointer : decltype(add_pointer_helper<T>(0)) {}; template<class T> using add_pointer_t = typename add_pointer<T>::type; |
Although it is possible to implement the add_pointer
class template using a similar technique that is used to implement the add_lvalue_reference
and add_rvalue_reference
class templates, I want to demonstrate a different technique that utilizes SFINAE to achieve a similar result. Instead of using a struct with partial specialization, we declare two functions.
The first function declared on lines 1-2 takes an int
parameter and returns the template argument T
with the reference removed and a pointer added to the type wrapped in the type_identity
template (which provides the type
member).
If T
a const
, volatile
, or reference-qualified function type, then trying to add a pointer to T
(T*
) would normally generate a compiler error. Instead of generating an error, the compiler will consider the second overload of add_pointer_helper
instead.
The second function declared on lines 4-5 is a fallback function that takes any other parameter type using the ellipsis (...
) and since the compiler considers this the worst possible match, it only chooses this overload if the compiler fails to instantiate the first version of the function. In this case, the template argument T
is wrapped in the identity_type
unmodified.
This technique that forces the compiler to choose worse match during function overload resolution is discussed in more detail later in the article (see SFINAE Out Function Overloads)
int
) because then the function signatures would become ambiguous and would fail to compile. Using the ellipsis (...
) tells the compiler to only match this version of the function if it fails to instantiate the first version of the function.You’ll notice that we provide a declaration of the add_pointer_helper
functions but do not provide a definition. This is perfectly fine, as long as these functions are only used in a non-deduced context such as an expression that is only evaluated using the decltype
specifier.
On lines 7-9, the add_pointer
class template is defined and is derived from the return value of the add_pointer_helper
function.
And, as usual, the short-hand alias of the add_pointer
class template is defined on lines 11-12.
conditional
In some cases, it is useful to choose a specific type based on some condition. The conditional
class template can be used to return one type or another type based on some condition (usually based on another type trait). The conditional
class template is equivalent to an if
condition in regular C++.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
template<bool B, class T, class F> struct conditional { using type = T; }; template<class T, class F> struct conditional<false, T, F> { using type = F; }; // Helper template alias. template<bool B, class T, class F> using conditional_t = typename conditional<B, T, F>::type; |
On lines 1-5 the primary template is defined. When B
is true
, then the type
is an alias of T
(let’s call this the true type). However, when the partial specialization where B
is false
is matched, then type
is F
(the false type).
Pretty simple right? Let’s see how we can use the conditional
class template to implement more complex logical template types.
conjunction
A logical conjunction is a truth-functional operator that is true if and only if all of its operands are true. This is equivalent to a logical AND (&&
) operation.
The conjunction
class template works by passing any number of type traits that are derived from either true_type
or false_type
(or has a static member variable value
that is convertible to bool
) as template arguments to conjunction
. The conjunction
class template is derived from the first template argument whose value
member variable is (or is convertible to) false
. If all type traits passed to conjunction
are true
, then conjunction is derived from the last type trait in the template argument list.
We haven’t looked at many type trait templates that are derived from true_type
or false_type
yet, but these are introduced later in the article. For now, we just have to keep in mind that a class template that derives from either true_type
or false_type
has a static const
member variable value
that is true
if it is derived from true_type
or false
if it is derived from false_type
.
We’ll use a recursive variadic template to implement the conjunction
class template. First, let’s take a look at the primary template.
1 2 3 4 |
// Primary template template<class...> struct conjunction : true_type {}; |
The primary template is a class template that doesn’t specialize any of the template parameters. The compiler will choose this version of the conjunction
class template only if there isn’t a specialization of the class template that is a better match. As we’ll see in a second, the primary template will only be chosen when conjunction
is used without any template arguments (which is probably a mistake by the programmer). Although the primary template should never be chosen by the compiler, we need it before we can specialize it.
Now let’s take a look at the base case of the recursive variadic template, that is, when conjunction
has only a single template argument.
1 2 3 4 |
// Specialized for a single template argument. template<class T> struct conjunction<T> : T {}; |
In the base case, when conjunction
has only a single template argument (T
), then conjunction
is derived from T
. Since all of the template arguments passed to conjunction
must have a static member variable called value
that is convertible to bool
(like true_type
and false_type
), then conjunction::value
is true
when T::value
is true
and false
when T::value
is false
.
Now let’s look at the recursive case:
1 2 3 4 |
// Specialized for 2 or more template arguments. template<class T, class... Tn> struct conjunction<T, Tn...> : conditional_t<bool(T::value), conjunction<Tn...>, T> {}; |
The recursive case is used when conjunction
has two or more template arguments. In this case, the conditional
class template is used to conditionally select either conjunction<Tn...>
(recursively calling itself) if T::value
is true
or T
when T::value
is false
.
Consequently, if T::value
is false
then the expansion of Tn...
stops and no further types need to be instantiated to determine the type of conjunction
. This is in contrast to using fold expressions (... && Tn::value)
where every T
in Tn
is instantiated before the expression is evaluated.
In order to reduce some typing later, we’ll also define an inline variable template for conjunction::value
:
1 2 3 |
// Requires C++17 template<class... T> inline constexpr bool conjunction_v = conjunction<T...>::value; |
Now, instead of typing conjunction<...>::value
, we only need to type conjunction_v<...>
. Small victory, but every little bit helps.
inline
specifier can be dropped if you need to support C++14.
In my own projects, I use the following macro to support both C++14 and C++17 when available:
1 2 3 4 5 |
#if defined(__cpp_inline_variables) && __cpp_inline_variables >= 201606L #define INLINE_VAR inline constexpr #else #define INLINE_VAR constexpr #endif |
Then prepend inline variable templates with the INLINE_VAR
macro instead of inline constexpr
.
disjunction
Similar to conjunction
, the disjunction
class template is equivalent to a logical OR operator.
The disjunction
class template works by passing any number of type traits that are derived from either true_type
or false_type
(or has a static member variable value
that is convertible to bool
) as template arguments. The disjunction
class template is derived from the first template argument whose value
member variable is (or is convertible) to true
. If all template arguments passed to disjunction
are false
, then disjunction
is derived from the last type trait in the template argument list.
Similar to the implementation of the conjunction
class template, we’ll use a recursive variadic template to implement the disjunction
class template. First, let’s take a look at the primary template.
1 2 3 4 |
// Primary template template<class...> struct disjunction : false_type {}; |
The primary template doesn’t specialize any of the template parameters. The compiler will use the primary template only if there isn’t a specialization of the disjunction
class template that is a better match. Since there is a specialization for a single template argument and a specialization for two or more template arguments (see below), the primary template is only chosen when disjunction
is used without any template arguments (which is probably a mistake). Although the primary template should never be chosen by the compiler, we need to define it before we can specialize it.
The base case for the recursive variadic template has only a single template parameter:
1 2 3 4 |
// Base case template<class T> struct disjunction<T> : T {}; |
In the base case, when the disjunction
class template has only a single template argument (T
), then disjunction
is derived from T
. Since all of the template arguments passed to disjunction
must have a static member variable called value
that is convertible to bool
, then disjunction::value
is true
when T::value
is true
and false
if T::value
is false
.
Now, let’s look at the recursive case.
1 2 3 |
template<class T, class... Tn> struct disjunction<T, Tn...> : conditional_t<bool(T::value), T, disjunction<Tn...>> {}; |
The recursive case is instantiated when disjunction
is used with two or more template arguments. In this case, the conditional
class template is used to conditionally select either T
if T::value
is true
or disjunction<Tn...>
(recursively calling itself) if T::value
is false
.
Consequently, if T::value
is true
, then the expansion of Tn...
stops and no further types need to be instantiated to determine the type of disjunction
. This is in contrast to using the fold expression (... || Tn::value)
which must instantiate every T
in Tn
before the expression is evaluated.
And similar to conjunction
, we’ll define an inline variable template to create a short-hand for disjunction::value
1 2 3 |
// Requires C++17 template<class... T> inline constexpr bool disjunction_v = disjunction<T...>::value; |
Now, instead of typing disjunction<...>::value
, we only need to type disjunction_v<...>
.
negation
The negation
class template forms a logical negation of the type trait T
.
1 2 3 |
template<class T> struct negation : bool_constant<!bool(T::value)> {}; |
The negation
class template is derived from the bool_constant
class template. If T::value
is true
, then negation::value
is false
(which is equivalent to being derived from false_type
) and if T::value
is false
, then negation::value
is true
(which is equivalent to being derived from true_type
).
And a short-hand version of negation::value
:
1 2 3 |
// Requires C++17 template<class T> inline constexpr bool negation_v = negation<T>::value; |
With the definition of conditional
, conjunction
, disjunction
, and negation
, we have the logical operators that are needed to make any logical combination of type traits that we need. In the following sections, we’ll use these logical class templates as the basis for other type traits.
is_same
The is_same
class template is the first class in our type traits library that can actually be considered a type trait. Most type traits result in a boolean value
that is either true
or false
. This is accomplished by inheriting from either true_type
if the type trait is true
or false_type
if the type trait is false
.
In most cases, the primary template for the type trait is derived from false_type
and one or more partial specializations exist that are derived from true_type
.
First, let’s look at the primary template for the is_same
type trait.
1 2 3 4 |
// Primary template. template<class T, class U> struct is_same : false_type {}; |
The primary template is chosen by the compiler when T
and U
are different types. Next, we’ll create a partial specialization of is_same
when T
and U
are the same types.
1 2 3 4 |
// Specialization for matching types. template<class T> struct is_same<T, T> : true_type {}; |
The partial specialization is only chosen when T
and U
are exactly the same types. That means that their const
, volatile
, reference or pointer attributes must also be the same. If you want to ignore any const
, volatile
, or references that might adorn the type, then wrap the type in either the remove_cv
or remove_cvref
class template.
A short-hand for is_same::value
can be defined using a variable template (requires C++14):
1 2 3 |
// Requires C++17 template<class T, class U> inline constexpr bool is_same_v = is_same<T, U>::value; |
is_void
With the is_same
and remove_cv
type traits defined, we can use these to define other type traits which can be used to identify all of the primary types. The is_void
type trait evaluates to true_type
if the template argument is void
(ignoring any const
and volatile
qualifiers) and false_type
otherwise.
1 2 3 |
template<class T> struct is_void : is_same<void, remove_cv_t<T>> {}; |
As was shown in the previous section, the is_same
type trait is derived from true_type
when the first and second template arguments are exactly the same type, and false_type
otherwise. We can use this to define a type trait that checks for a primary type (like int
s and float
s which we’ll look at in the following sections).
We’ll also define an inline variable template called is_void_v
that can be used as a short-hand for is_void::value
:
1 2 3 |
// Requires C++17 template<class T> inline constexpr bool is_void_v = is_void<T>::value; |
is_null_pointer
Similar to the is_void
type trait, the is_null_pointer
type trait is derived from true_type
if the template argument is std::nullptr_t
.
1 2 3 4 5 6 7 8 9 |
#include <cstddef> // for std::nullptr_t template<class T> struct is_null_pointer : is_same<std::nullptr_t, remove_cv_t<T>> {}; // Requires C++17 template<class T> inline constexpr bool is_null_pointer_v = is_null_pointer<T>::value; |
The implementation of is_null_pointer
is similar to is_void
.
is_any_of
The is_any_of
type trait can be used to check if a certain type template argument matches any one of the other type template arguments. We’ll use the disjunction
and the is_same
type traits defined earlier to implement the is_any_of
type trait.
The is_any_of
type trait does not currently exist in the C++ standard, but we’ll use this type trait to simplify the defintion of other type traits later in this article.
1 2 3 4 5 6 7 |
template<class T, class... Tn> struct is_any_of : disjunction<is_same<T, Tn>...> {}; // Requires C++17 template<class T, class... Tn> inline constexpr bool is_any_of_v = is_any_of<T, Tn...>::value; |
The implementation of the is_any_of
type trait is super simple since we can rely on the disjunction
and the is_same
type traits that were previously defined. Variadic template arguments are used to check if the type T
matches any one of types in the pack represented by Tn
.
is_integral
With the is_any_of
type trait defined previously, it is now super simple to implement other type traits. The is_integral
type trait is true_type
if the template argument is one of the following types:
Or any other equivalent type including signed or unsigned, with const, and volatile qualified variants. Otherwise, it is false_type
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
template<class T> struct is_integral : is_any_of<remove_cv_t<T>, bool, char, signed char, unsigned char, wchar_t, char16_t, char32_t, short, unsigned short, int, unsigned int, long, unsigned long, long long, unsigned long long #if defined(__cpp_char8_t) && __cpp_char8_t >= 201811L , char8_t // Since C++20 #endif > {}; // Requires C++17 template<class T> inline constexpr bool is_integral_v = is_integral<T>::value; |
Using the is_any_of
type trait defined previously, the is_intgral
type trait becomes much simpiler. If the template argument T
is any one of the types listed (after removing const
and volatile
qualifiers), then is_integral
is true_type
, otherwise it is false_type
.
is_floating_point
Similar to the is_integral
type trait, the is_floating_point
type trait is true_type
if the template argument is one of the following types:
Including const
and volatile
qualified variants. Otherwise, it is false_type
.
1 2 3 4 5 6 7 8 9 10 11 |
template<class T> struct is_floating_point : is_any_of<remove_cv_t<T>, float, double, long double > {}; // Requires C++17 template<class T> inline constexpr bool is_floating_point_v = is_floating_point<T>::value; |
is_arithmetic
The is_arithmetic
type trait is true_type
if its template argument is either an integral type or a floating-point type, and false_type
otherwise. As you may have guessed, we can use the is_integral
and is_floating_point
type traits defined earlier in combination with disjunction
to implement this type trait.
1 2 3 4 5 6 7 |
template<class T> struct is_arithmetic : disjunction<is_integral<T>, is_floating_point<T>> {}; // Requires C++17 template<class T> inline constexpr bool is_arithmetic_v = is_arithmetic<T>::value; |
An an alternative and equivalent implementation of the is_arithmetic
type trait uses the bool_constant
class template directly:
1 2 3 |
template<class T> struct is_arithmetic : bool_constant<is_integral_v<T> || is_floating_point_v<T>> {}; |
Although it doesn’t change the meaning of the is_arithmetic
type trait, it demonstrates that you can use the logical OR operator (||
) to evaluate template arguments. Note however that the disjunction
class template expects its template arguments to be types, while the bool_constant
class template expects a non-type template argument that is convertible to bool
. In this case, we can use the _v
short-hand variants of the type traits to get the value
member variable of the type trait.
is_const
The is_const
type trait checks to see if a type is const-qualified. The is_const
type trait is derived from true_type
if the type is const-qualified, or false_type
otherwise.
1 2 3 4 5 6 7 8 9 10 11 |
template<class T> struct is_const : false_type {}; template<class T> struct is_const<const T> : true_type {}; // Required C++17 template<class T> inline constexpr bool is_const_v = is_const<T>::value; |
Similar to the remove_const
class template that was previously shown, the is_const
type trait uses partial specialization to detect const-qualified types.
is_reference
The is_reference
type trait can be used to check if a type is either an lvalue reference or rvalue reference type.
1 2 3 4 5 6 7 8 9 10 11 |
template<class T> struct is_reference : false_type {}; template<class T> struct is_reference<T&> : true_type {}; template<class T> struct is_reference<T&&> : true_type {}; |
The is_reference
type trait is derived from true_type
if the type T
is either an lvalue (line 6) or an rvalue (line 10) reference. Otherwise, is_reference
is derived from false_type
.
And the short-hand version:
1 2 |
template<class T> inline constexpr bool is_reference_v = is_reference<T>::value; |
is_bounded_array
The is_bounded_array
type trait checks if T
is an array type with a known size.
1 2 3 4 5 6 7 |
template<class T> struct is_bounded_array : false_type {}; template<class T, std::size_t N> struct is_bounded_array<T[N]> : true_type {}; |
If T
is a bounded array (an array with a known size in the form of T[N]
), then is_bounded_array
is derived from true_type
, otherwise it is derived from false_type
.
And the short-hand variant:
1 2 3 |
// Requires C++17 template<class T> inline constexpr bool is_bounded_array_v = is_bounded_array<T>::value; |
is_unbounded_array
The type trait for an unbounded array is very similar to that of the bounded array. The primary difference is that the size of the array is unspecified.
1 2 3 4 5 6 7 8 9 10 11 |
template<class T> struct is_unbounded_array : false_type {}; template<class T> struct is_unbounded_array<T[]> : true_type {}; // Requires C++17 template<class T> inline constexpr bool is_unbounded_array_v = is_unbounded_array<T>::value; |
is_array
The is_array
type trait can be used to check if a type is either a bounded or unbounded array.
1 2 3 4 5 6 7 |
template<class T> struct is_array : bool_constant<is_bounded_array_v<T> || is_unbounded_array_v<T>> {}; // Requires C++17 template<class T> inline constexpr bool is_array_v = is_array<T>::value; |
is_function
The is_function
type trait can be used to check if a type is a function. The is_function
type trait only considers free functions and static member functions of a class to be function types. std::function
, lambdas, callable function objects (classes or structs with overloaded function call operator operator()) and pointers to functions are not considered function types.
1 2 3 4 5 6 7 8 9 |
#pragma warning(push) #pragma warning(disable: 4180) // Disable C4180: qualifier applied to function type has no meaning; ignored template<class T> struct is_function : bool_constant<!is_const_v<const T> && !is_reference_v<T>> {}; #pragma warning(pop) template<class T> inline constexpr bool is_function_v = is_function<T>::value; |
The is_function
type trait works on the basis that only function types and reference types can’t be const-qualified. Adding the const
qualifier to a function type does not change its constness. If T
is a function type (or a reference type), then is_const_v<const T>
will be false
. If is_reference_v<T>
is also false
, then T
must be a function type.
In fact, trying to add a const
qualifier to a function type will generate a warning (C4180) in Visual Studio. The warning about applying a const qualifier to a function type can be disabled using the #pragma warning(disable: 4180)
pragma as shown in the code snippet.
decay
The decay
class template is used to perform the same conversion operations that are applied to template arguments when passed by value. The decay
class template will do one of three things depending on the argument type:
- Array: If
T
is an array of typeU
(either an unbounded array of the formU[]
or a bounded array of the formU[N]
) thendecay<T>::type
will beU*
. That is, array types decay to pointers to the array element type. - Function: If
T
is a function type or a reference to a function, thendecay<T>::type
will beadd_pointer_t<T>
. That is, function types become pointers to functions. - Neither array nor function:
decay<T>::type
removes any reference,const
, andvolatile
qualifiers from the type usingremove_cv_t<remove_reference_t<T>>
We’ll use a piecewise technique to implement the decay
class template that is split into 3 parts:
- The primary template which is used when
T
is neither an array type nor a function type. - A partial specialization when
T
is an array type. - A partial specialization when
T
is a function type.
Similar to how we implemented add_lvalue_reference
, and add_rvalue_reference
, we’ll use a helper class template so that the decay
class template only requires a single template parameter.
First, we’ll look at the primary template for the decay_helper
class template.
1 2 3 4 5 6 7 8 |
template<class T, bool IsArray = is_array_v<T>, bool IsFunc = is_function_v<T> > struct decay_helper { using type = remove_cv_t<T>; }; |
The primary template for the decay_helper
class template takes three template arguments:
T
: The type to decay,IsArray
: A boolean non-type template parameter that istrue
ifT
is an array type orfalse
otherwise.IsFunc
: A boolean non-type template parameter that istrue
ifT
is a function type orfalse
otherwise.
The IsArray
template parameter defaults to is_array_v<T>
and the IsFunc
template parameter defaults to is_function_v<T>
.
Since we will provide a partial specialization when T
is an array type and another specialization for when T
is a function type, the primary template is only used when T
is neither an array nor a function type (that is, both IsArray
and IsFunc
are false
).
When T
is neither an array nor a function type, we’ll use the remove_cv
class template to remove any const
, and volatile
qualifiers from T
.
Next, we’ll provide a partial specialization of decay_helper
when T
is an array type.
1 2 3 4 5 |
template<class T> struct decay_helper<T, true, false> { using type = remove_extent_t<T>*; }; |
This partial specialization will be used when is_array_v<T>
evaluates to true
and is_function_v<T>
evaluates to false
. In this case, the resulting type is remove_extent_t<T>*
, which removes the array extent from the type and adds a pointer to the resulting element type.
Next, we’ll provide a partial specialization of decay_helper
when T
is a function type.
1 2 3 4 5 |
template<class T> struct decay_helper<T, false, true> { using type = add_pointer_t<T>; }; |
This partial specialization is used when is_array_v<T>
evaluates to false
and is_function_v<T>
evaluates to true
. In this case, we just add a pointer to the function type using the add_pointer
class template.
With all the possible combinations handled, we can now define the decay
class template.
1 2 3 4 5 6 7 8 |
template<class T> struct decay { using type = typename decay_helper<remove_reference_t<T>>::type; }; template<class T> using decay_t = typename decay<T>::type; |
The decay
class template uses the decay_helper
class template to determine the type of the type
member after removing any references from T
.
And of course, on lines 28-29, the short-hand alias template for typename decay<T>::type
is defined.
SFINAE
SFINAE (sfee-nay) is an acronym for “Substitution Failure Is Not An Error” and it refers to a technique used by the compiler to turn potential errors into “deduction failures” during template argument deduction. It is a technique used to eliminate certain functions from being chosen during overload resolution or to choose a particular class template specialization based on characteristics of the template arguments. If the compiler finds an invalid expression during template argument deduction, instead of generating a compiler error, it removes that function or class specialization from the set of possible candidates.
We’ve already seen a few examples of using SFINAE to generate a few of the type traits in the previous sections. The add_pointer
type trait uses a set of function overloads to SFINAE-out the case where adding a pointer to T
would result in an error. For example, if T
was a const
, volatile
or reference qualified function type, then attempting to add a pointer to T
would generate a compile-time error. Instead of generating an error, the compiler eliminates the first function from the set of overloads and considers the next function.
Another SFINAE technique is used to define the add_lvalue_reference
and add_rvalue_reference
type traits. Instead of function overloads, partial specialization of a class template is used to SFINAE-out cases where adding a reference to T
would result in a compiler error (for example, if T
is void
).
SFINAE-Out Function Overloads
To demonstrate SFINAE using function template overloads, we’ll create a SFINAE-based type trait to determine if a type T
is constructible using a particular set of arguments (Args...
).
The approach to implementing SFINAE-based traits with function overloads is to declare two overloaded function templates named test
(you can use any name for this function, but test
is traditionally used for SFINAE-based traits).
1 |