Template Type Derivation

The following is a common function template.

1
2
template<typename T>
void func(ParamType p);

When we call it as func(x), the compiler will automatically derive the types T and ParamType for us, and they may not be the same because ParamType may have a const or & reference modifier.

Parameter type is a reference or pointer

1
2
template<typename T>
void func(T& p);

This case does not include Universal Reference (&&), and the derivation follows.

  1. if the passed parameter x is a reference, then the reference part is ignored.
  2. afterwards match the parameter ParamType and decide T.

for example for the following different parameters.

1
2
3
4
5
6
7
int x = 0;
const int cx = x;
const int& rx = x;

f(x);   //ParamType 为 int&, T 为 int
f(cx);  //ParamType 为 const int&, T 为 const int
f(rx);  //ParamType 为 const int&, T 为 const int

This case is derived in the most natural way and there are no special cases.

The argument type is a generic reference

1
2
template<typename T>
void func(T&& p);

The Universal Reference is present in the template parameters (T&&) and follows the following rules.

  • If the parameter x is left-valued, then both T and ParamType are derived as left-valued references.
  • If the parameter x is right-valued, then the previous rule can be applied, i.e., the most general case.
1
2
3
4
f(x);       //x 为左值,ParamType 为 int&,T 为 int&
f(cx);      //cx 为左值,ParamType 为 const int&,T 为 const int&
f(rx);      //rx 为左值,ParamType 为 const int&,T 为 const int&
f(0);       //0 为右值,ParamType 为 int&&, T 为 int

There is something weird about this article that although && right-valued references are used, the derivation leads to left-valued references, which is why Meyers introduced the concept of generic references. Generic references exist in the following two contexts.

1
2
3
4
template<typename T>
void f(T&& param);

auto&& rx = x;

The similarity between these two is that they both use type derivation, where && is a generic reference when the exact type is unknown, and the above rules apply to type derivation, while for specific types it is a normal right-valued reference.

It is worth mentioning that for example push_back(T&&) of vector, although the argument is a type-derived right-valued reference, this function is actually instantiated when vector is instantiated, which means that when you use the function, the function signature is determined, and actually no type derivation is used, so it is not a generic reference. But for emplace_back(Args&&... args) the function can only be instantiated when you call the function with parameters, so here the type derivation is used, it is a global reference.

The generic reference has two meanings, one is the most basic right-valued reference, and the other represents a left-valued or right-valued reference. This feature makes it possible to bind to both left and right values in code that looks like T&&.

Although the concept of generic references looks good and is not biased in any way for understanding. The real reason for this derivation is Reference Collapsing. By default, C++ does not allow references to references like int& &x, but in template derivation it does, with the following rules

  • right-valued references to right-valued references collapse to right-valued references
  • All other cases collapse left-valued references

The right-valued reference is given in the template parameters, which is collapsed to a right-valued reference only when we give a right-valued reference, and collapsed to a left-valued reference in all other cases. This explains why the term generic reference is only available in the template derivation and why both references can be bound. This feature is the key to achieving features such as perfect forwarding and effectively reducing the amount of code of this type.

Other cases

At this point the template parameter is neither a pointer nor a reference, then it is a value pass.

1
2
template<typename T>
void func(T param);

At this point a new copied out object is passed anyway, so the derivation rule is

  • if the parameter x is a reference, then discard the reference part
  • If the argument x is const or volatile, it is also discarded

It is worth noting that references to constants and pointers to constants are not constant values, i.e., they can point to other things, but the content of the pointer is still constant and unchangeable.

When the passed argument is an array or a function, the derivation degrades it to a pointer (a pointer to the first element of the array or a function pointer), unless the target is a reference.

auto type derivation

auto has the same derivation rules as the template, the following two are the same case.

1
2
3
4
5
const auto x = 12;

template<typename T>
void func(const T p);
func(12);

In the auto type derivation auto plays the role of T, the modifier plays the role of ParamType, and the initialization value given is the argument passed to the function.

The only difference is the treatment of std::initializer_list, which can be explicitly initialized to initializer_list with = in auto. Without = from C++17 onwards, the derivation of an initializer list containing only a single variable is reserved for the type of that element (the initializer list is treated as a constructor argument to that element). Initialization value lists passed to function templates will not be compiled.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
auto a = {1, 2}; // 结果为 std::initializer_list<int>
auto b = {1};    // 结果为 std::initializer_list<int>
//auto c{1, 2};    // 结果为 std::initializer_list<int>,C++17 起错误
auto d{1};       // C++17 起结果为 int ,之前为 initializer_list<int>

template<typename T>
void f1(T param);
template<typename T>
void f2(std:initializer_list<T> list);

f1({1,2,3});    //错误!无法推导
f2({1,2,3});    //推导成 void f2(std:initializer_list<int>)

Since C++14 auto can also derive the return value type of functions, or when used in lambda expressions, the template type derivation rule is applied, so the following code does not compile.

1
2
3
auto f(){
    return {1, 2};    //无法通过编译
} 

decltype type derivation

decltype gets almost the actual type given, without degradation or modification, and can be used for both expressions and variables. The type is modified when using auto, and in conjunction with decltype it is possible to have the compiler derive and get the exact type.

1
2
3
const int& cx = x;
auto x1 = cx;            //类型为 int
decltype(auto) x2 = cx;  //类型为 const int &

If the argument is an expression of type T and

  • if the value class of the expression is deceased, then the result is T&&; * if the value class of the expression is left-valued, then the result is T&;
  • if the value class of the expression is left-valued, the result is T&; * if the value class of the expression is left-valued, the result is T&; and
  • If the value class of the expression is purely right-valued, the result is T.

Including a variable in parentheses is treated as an expression with a left-valued result, so that for a variable x of type int, decltype(x) results in int and decltype((x)) results in int&.