Default template arguments
A class template parameter can be given a default argument. If the calling code does not provide the corresponding argument, the compiler substitutes the specified default.
Let us add a second parameter named Cont to Stack. It determines the type of the internal container. By default, the stack uses std::vector<T>:
#include <cassert>
#include <type_traits>
#include <vector>
template<typename T, typename Cont = std::vector<T>>
class Stack
{
static_assert(
std::is_same<T, typename Cont::value_type>::value,
"Cont::value_type must be T"
);
public:
using value_type = T;
using container_type = Cont;
using const_reference = typename container_type::const_reference;
void push(const T& value);
void pop()
{
assert(!elems.empty());
elems.pop_back();
}
const_reference top() const
{
assert(!elems.empty());
return elems.back();
}
bool empty() const
{
return elems.empty();
}
private:
container_type elems;
};
template<typename T, typename Cont>
void Stack<T, Cont>::push(const T& value)
{
elems.push_back(value);
}
Only the first argument is provided in the expression Stack<int>:
Stack<int> stack;
The compiler substitutes the parameters as follows:
T = int
Cont = std::vector<int>
A default argument may depend on parameters declared to its left. Therefore, std::vector<T> can use the already known parameter T.
The container type can also be specified explicitly:
#include <deque>
Stack<int, std::deque<int>> stack;
In this case, the parameters have different values:
T = int
Cont = std::deque<int>
A default argument cannot be specified in the template parameter list of an out-of-class member definition. The header template<typename T, typename Cont> lists the parameters of the template itself, while the complete class name is written as Stack<T, Cont>.
The normative rules are defined in the standard section on default template arguments.
Rules for declaring default arguments
Class template parameters follow an ordering rule: after a parameter with a default argument, every subsequent parameter must also have a default argument, except for a trailing parameter pack.
#include <memory>
#include <vector>
template<
typename T,
typename Cont = std::vector<T>,
typename Alloc = std::allocator<T>
>
class Storage;
The following declaration, by contrast, would be invalid:
// Error: the required Alloc parameter follows Cont,
// which already has a default argument.
template<
typename T,
typename Cont = std::vector<T>,
typename Alloc
>
class InvalidStorage;
Default arguments can be accumulated across multiple declarations of the same template in the same scope. The compiler considers them together:
#include <memory>
#include <vector>
template<
typename T,
typename Cont,
typename Alloc = std::allocator<T>
>
class Storage;
template<
typename T,
typename Cont = std::vector<T>,
typename Alloc
>
class Storage
{
};
After the declarations are combined, both Cont and Alloc have default arguments. However, a default argument cannot be assigned to the same parameter more than once in the same scope, even if the repeated expression is identical:
template<typename T, typename Cont = std::vector<T>>
class Queue;
// Error: a default argument has already been specified for Cont.
// template<typename T, typename Cont = std::vector<T>>
// class Queue
// {
// };
Default arguments are also forbidden in the template parameter list of an out-of-class member definition:
template<typename T, typename Cont>
void Stack<T, Cont>::push(const T& value)
{
elems.push_back(value);
}
Default arguments are forbidden in the template parameter list of a partial specialization as well:
template<typename T, typename U>
class PairBox;
// Valid form of a partial specialization:
template<typename T, typename U>
class PairBox<T*, U>;
// A default argument in the parameter list is not allowed:
// template<typename T, typename U = int>
// class PairBox<T*, U>;
Requirements for the Cont parameter
The Cont parameter cannot be replaced with an entirely arbitrary type. The Stack implementation relies on a particular container interface:
elems.push_back(value);
elems.pop_back();
elems.back();
elems.empty();
For the methods used by this implementation, the supplied type must:
- provide the nested types
value_typeandconst_reference; - support adding a value with
push_back(); - support removing the last element with
pop_back(); - provide access to the last element through
back()on a constant container; - allow its emptiness to be checked by calling
empty()on a constant container.
Semantic compatibility matters in addition to the presence of these operations. This implementation requires Cont::value_type to be exactly the same type as T. The static_assert in the class definition enforces this requirement.
Compatible containers
Stack<int, std::vector<int>> vectorStack;
Stack<int, std::deque<int>> dequeStack;
Both containers store int values and provide the operations required by this stack implementation.
If a Stack object is created with its default constructor, its container must support the corresponding initialization. A different container construction strategy would require additional constructors in Stack.
Dependent names and typename
The return type of top() is obtained from the container:
using const_reference = typename Cont::const_reference;
The name Cont::const_reference is dependent: the actual Cont type becomes known only after template arguments are substituted. While parsing the template, the compiler generally cannot determine in advance whether the nested name denotes a type, a static field, or another kind of member.
The typename keyword explicitly tells the compiler that a dependent qualified name denotes a type:
template<typename Cont>
using Iterator = typename Cont::iterator;
Alias for a dependent type
#include <vector>
std::vector<int> values{1, 2, 3};
Iterator<std::vector<int>> it = values.begin();
After substituting Cont = std::vector<int>, the alias denotes std::vector<int>::iterator.
In all alias declarations shown here, the right-hand side after = is a defining type-id—a context in which a type is expected. In C++11 through C++17, a dependent qualified name in this position requires typename. Starting with C++20, typename may be omitted in this particular context. The examples retain it for compatibility with C++11–C++17 and to make the role of the name explicit.
This C++20 relaxation does not remove the general rules for dependent names. Whether typename is required in other contexts must be determined separately.
Dependent names are covered in greater detail by the standard section on name resolution in templates.
Type aliases
A long type name can be given a shorter name with a using declaration:
using IntStack = Stack<int>;
using IntDequeStack = Stack<int, std::deque<int>>;
The alias and the original type can then be used interchangeably:
IntStack first;
Stack<int> second;
IntDequeStack third;
Stack<int, std::deque<int>> fourth;
An alias does not create a new type. For example, IntStack and Stack<int> have the same type identity, so no separate conversion exists between them.
The historical typedef syntax is also available for an ordinary, non-template alias:
typedef Stack<int, std::deque<int>> IntDequeStackOld;
Both forms create an alias for an existing type. The using syntax usually reads more naturally from left to right and is better suited to more complex declarations.
The relevant rules are described in the standard section on names introduced by typedef declarations and alias declarations.
Alias templates
An alias can depend on template parameters. For example, std::deque can be fixed as the stack’s container while leaving only the element type variable:
#include <deque>
template<typename T>
using DequeStack = Stack<T, std::deque<T>>;
This declaration is called an alias template.
DequeStack<int> numbers;
DequeStack<double> measurements;
After substitution, DequeStack<int> denotes the same type as Stack<int, std::deque<int>>.
Alias templates are especially useful when some arguments of a larger template are repeated frequently:
#include <map>
#include <string>
template<typename T>
using StringMap = std::map<std::string, T>;
StringMap<int> counters;
StringMap<std::string> descriptions;
StringMap<int> denotes std::map<std::string, int>.
Alias templates cannot be fully or partially specialized:
template<typename T>
using Pointer = T*;
// Invalid attempt at a full specialization:
// template<>
// using Pointer<void> = const void*;
If a specializable type computation is required, it is usually placed in an auxiliary class template that can be specialized. A convenient alias is then provided for its nested type.
The definition and semantics of these declarations are covered in the standard section on alias templates, while the prohibition against their full and partial specialization follows from the general template declaration rules.
Aliases for nested types
Standard library classes frequently provide nested types:
std::vector<int>::value_type
std::vector<int>::iterator
std::vector<int>::const_reference
An alias template can extract such a type from any suitable class:
template<typename Cont>
using Iterator = typename Cont::iterator;
template<typename Cont>
using Element = typename Cont::value_type;
using IntIterator = Iterator<std::vector<int>>;
using IntElement = Element<std::vector<int>>;
IntIterator denotes std::vector<int>::iterator, while IntElement denotes int. Once again, these declarations only create alternative names for existing types rather than new types.
Aliases inside a class template
The Stack definition already contains three public aliases:
using value_type = T;
using container_type = Cont;
using const_reference = typename container_type::const_reference;
They make the class interface easier to use in generic code. External code can obtain types associated with the stack without repeating its arguments or internal rules:
using Value = Stack<int>::value_type;
using Container = Stack<int>::container_type;
using Reference = Stack<int>::const_reference;
The resulting types are:
Value // int
Container // std::vector<int>
Reference // std::vector<int>::const_reference
If the enclosing type itself depends on a template parameter, C++11 through C++17 require typename before its nested type on the right-hand side of such an alias declaration. Starting with C++20, it may be omitted in this defining type-id. The explicit form is retained for compatibility:
template<typename StackType>
using StackValue = typename StackType::value_type;
using Value = StackValue<Stack<int>>;
Names such as value_type, iterator, and reference are a conventional part of standard container interfaces. They simplify the implementation of algorithms that do not depend on one specific class.
Standard aliases with the _t suffix
Many templates from the <type_traits> header that compute a type expose their result through a nested member named type. A custom alias can therefore be written as follows:
#include <type_traits>
template<typename T>
using AddConstLegacy = typename std::add_const<T>::type;
Starting with C++14, the standard library provides ready-made aliases with the _t suffix for many such transformations:
template<typename T>
using AddConst = std::add_const_t<T>;
using ConstInt = AddConst<int>; // const int
The following pairs of aliases denote the same types:
template<typename T>
using AddConstLong = typename std::add_const<T>::type;
template<typename T>
using AddConstShort = std::add_const_t<T>;
template<typename T1, typename T2>
using CommonTypeLong = typename std::common_type<T1, T2>::type;
template<typename T1, typename T2>
using CommonTypeShort = std::common_type_t<T1, T2>;
template<typename T>
using DecayLong = typename std::decay<T>::type;
template<typename T>
using DecayShort = std::decay_t<T>;
The _t forms shorten the notation and remove the need for calling code to access the nested type member directly. They still do not create new types: they are standard alias templates for the results of the corresponding type traits.
The standard type transformations are defined in the section on transformation traits.
Key points
- A default argument is substituted when the corresponding template argument is not provided explicitly.
- A default argument may use parameters declared to its left.
Stack<int>omitsCont, butStack<>is invalid unlessTalso has a default argument.- After a class template parameter with a default argument, subsequent parameters must also have defaults, except for a trailing parameter pack.
- Default arguments may be accumulated across declarations, but the same parameter cannot be given a default argument more than once in the same scope.
- Default arguments are forbidden in the template parameter list of an out-of-class member definition and in the template parameter list of a partial specialization.
- A type supplied as
Contmust provide the required operations and be semantically compatible with the element typeT. - In the alias declarations shown here, a dependent qualified name requires
typenamein C++11–C++17; starting with C++20, it may be omitted. usingandtypedefgive an existing type another name rather than creating a new type.- Alias templates are declared with
usingand cannot be fully or partially specialized. - Standard aliases with the
_tsuffix are available starting with C++14.