该条款的前置知识为 C++ 表达式的值类别:
如下图所示:

名词解释如下:
介绍左值 lvalue:
++x、cout << ' '。"hello world"。T&;⼀个常量只能绑定到常左值引⽤,如 const T&。介绍纯右值 prvalue:
x++、x+1、make_shared(42) 。42、true。const T&,但不可以绑定到⾮常左值引⽤(non-const lvalue reference),如 T&。T&&,⽐左值引⽤多⼀个 & 符号。const 和 volatile 来进⾏修饰。模板的型别推导是 auto 的基础。【注意下面代码中,ParamType 其实是和 T 相关的】
// 函数模板大致形如:
template<typename T>
void f(ParamType param); // ParamType 通常与 T 相关
// 它的一次调用形如:
f(expr);// 以某表达式调用f
// 在编译期,编译器会通过expr推导两个型别:
// 一个是T的型别,另一个是ParamType的型别,这两个型别往往不一样
注意上述代码中,T 的型别推导结果,不仅仅依赖 expr 的型别,还依赖 ParamType 的形式,具体要分下述三种情况讨论。【ParamType 是形参类型,expr 是传参类型】
情况一,ParamType 具有指针或引用型别,但不是万能引用(universal reference)。【万能引用将在条款 24 中介绍】
expr 具有引用型别,先将引用部分忽略。【包括指针】expr 的型别和 ParamType 的型别执行模式匹配,来决定 T 的型别。ParamType 具有引用特性时,代码分析如下:【ParamType 是指针时也是差不多的结果】// 假设模板如下:
template<typename T>
void f(T& param) {} // ParamType是个引用,此处就是 T&
// expr的型别:
int x = 27; // x的型别是int
const int cx = x; // cx的型别是const int
const int& rx = x; // rx是x的型别为const int的引用
// ParamType的型别:
f(x); // T的型别是int, param的型别是int&
f(cx); // T的型别是const int, param的型别是const int&
f(rx); // T的型别是const int, param的型别是const int&, 注意:即使rx具有引用型别,T也并未被推导成一个引用,原因在于,rx的引用性(reference-ness)会在型别推导过程中被忽略
const 对象时,常量性会被保留。【即当 ParamType 是引用时,对该模板传入 const 对象很安全,连 T 也会保留其常量性】expr 的引用性被忽略了。ParamType 具有 const 引用特性时,代码分析如下:【这里和前面比较只有一个小地方不同】// 假设模板如下:
template<typename T>
void f(const T& param) {} // ParamType是个引用,此处就是 const T&
// expr的型别:
int x = 27; // x的型别是int
const int cx = x; // cx的型别是const int
const int& rx = x; // rx是x的型别为const int的引用
// ParamType的型别:
f(x); // T的型别是int, param的型别是const int&
f(cx); // T的型别是int, param的型别是const int& 【只有这里有一点不同,因为 param 本身就是const,所以T的推导不需要保留 const】
f(rx); // T的型别是int, param的型别是const int&
情况二,ParamType 是一个万能引用。【也就是 T&&】【该部分的内容在条款 24 中会有更详细的解释】
expr 是左值,则 T 和 ParamType 都会被推导为左值引用。【这里可以看出,型别推导只有一种特殊情况,只有在这种情况下,T 具有引用特性】expr 是右值,则按照常规来推导,所谓的常规情况就是情况一。// 假设模板如下:
template<typename T>
void f(T&& param) {} // ParamType是个万能引用
// expr的型别:
int x = 27; // x的型别是int
const int cx = x; // cx的型别是const int
const int& rx = x; // rx是x的型别为const int的引用
// ParamType的型别:
f(x); // x是左值,T的型别是int&, param的型别是int&
f(cx); // cx是左值,T的型别是const int&, param的型别是const int&
f(rx); // rx是左值,T的型别是const int&, param的型别是const int&
f(27); // 27是右值,T的型别是int,param的型别是int&&
情况三,ParamType 既非指针也非引用,也就是按值传递。
// 假设模板如下:
template<typename T>
void f(T param) {}
// expr的型别:
int x = 27; // x的型别是int
const int cx = x; // cx的型别是const int
const int& rx = x; // rx是x的型别为const int的引用
// ParamType的型别:
f(x); // T的型别是int, param的型别是int
f(cx); // T的型别是int, param的型别是int
f(rx); // T的型别是int, param的型别是int
expr 是什么,param 都是一个全新对象,也就是一个副本。T 和 param 的推导都是一样的,都会忽略 expr 的 const 特性、& 特性和 volatile 特性。在本条款讨论的内容中,还需要继续讨论数组实参带来的影响。
const char str[] = "abcde"; // name的型别为 const char[6]
const char* ptr = str; // 数组退化为指针
// 假设模板如下:
template<typename T>
void f1(T param) {} // 按值传递
f1(str); // str是个数组,退化为指针后按值传递,因此 T 的型别为 const char*
// 奇怪的点在于如果 param 具有引用形式,情况会有所不同:
template<typename T>
void f2(T& param) {} // ParamType 是 T&
f2(str); // 此时会传递一个数组,因此 T 的型别为 const char[6],同时 param 的型别为 const char& [6]
// 注意上面 T 的推导中带有了数组的长度
总结:
const 特性、volatile 特性都会被忽略。auto 型别推导其实可以等价于模板型别推导,代码分析如下:【auto 推导的情况和条款 1 中模板推导的情况是一致的(包括数组和函数退化成指针的情况)】// 模板型别推导如下:
template<typename T>
void f(ParamType param); // ParamType 通常与 T 相关
// 它的一次调用形如:
f(expr);// 以某表达式调用f
// 在编译期,编译器会通过expr推导 T 和 ParamType
// auto 的使用如下:
auto x = 27; // x 型别饰词为 auto
const auto cx = x; // cx 型别饰词为 const auto
const auto& rx = x; // rx 型别饰词为 const auto&
// auto 与 函数模板推导的等价如下:【只是概念上等价,并非说编译器真的生成了】
template<typename T>
void func_for_x(T param);
func_for_x(27); // 推导 27 的 T 和 ParamType
template<typename T>
void func_for_cx(const T param);
func_for_cx(x);
template<typename T>
void func_for_rx(const T& param);
func_for_rx(x);
// 其他符合的情况可以看书中具体例子
auto 型别会有例外,那就是 auto 与**初始化列表(initializer list)**的结合,具体代码如下:auto x1 = 27; // 型别是 int,值是27
auto x2(27); // 同上
auto x3 = {27}; // 型别是 std::initializer_list,值是27
auto x4{27}; // 同上
auto 型别推导和模板型别推导中加多一个中间层。
std::initializer_list 中的 T 推导失败,则 auto 的推导也会失败,因此大括号里的值需要保持一致的型别。std::initializer_list 其实就是一个和大括号“绑定”的型别(依我的理解是这样),如果想在普通的模板型别推导中使用,需要明确使用 std::initializer_list 而不是 T。【具体看书中代码】auto 关键字,其实是在使用普通的模板型别推导,也就是说这两种情况底下不可以使用大括号。decltype 的主要用途大概就在于声明那些返回值型别依赖于形参型别的函数模板。【通常情况下就是有什么就返回什么】decltype 来计算返回值型别的代码如下:【可以认为 decltype 的使用就是将代码的意图展露出来】// 这段代码就是 decltype 的 c++11 用法
template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i) // 注意这里的 auto 并不会引起条款 2 中的 auto 型别推导
->decltype(c[i]) // 这里使用了C++11中的返回值型别尾序语法,也就是 `->` 和 `decltype` 和 `auto` 使用
// 注意 decltype 的括号里用的是形参
{
authenticateUser();
return c[i]; // 这里容器的 operator[] 通常会返回 T&
}
// operator[]返回值是什么型别,authAndAccess的返回值就是什么型别,和我们期望的结果一致
authAndAccess 这种情况来说,这就意味着在 C++14 中可以去掉返回值型别尾序语法,而只保留前导 auto。auto 确实说明会发生型别推导,具体地说,它说明编译器会依据函数实现来实施函数返回值的型别推导:template<typename Container, typename Index> //C++14;
auto authAndAccess(Container& c, Index i) //不甚正确
{
authenticateUser();
return c[i]; //返回值型别是根据c[i]推导出来,这里 operator[] 通常是返回 T&
}
// 上述代码看起来没有问题,但是下面使用不能通过编译:
vector<int>vec = { 1,2,3 };
authAndAccess(vec, 2) = 6; // 关键在于 authAndAccess 会消去 T[] 带来的引用性(符合条款 1 中的第三种情况),所以函数的返回值是一个右值,不能被赋值,所以不能通过编译
auto 和 decltype 的结合是很有必要的,如前面所述,decltype 的使用能帮助返回值找到本身类型。decltype(auto) 饰词,代码如下:// 这段代码就是 decltype 的 c++14 用法
// 对于C++14来说,可以直接支持返回值为decltype(auto)进行推导,而C++11只能使用尾序推导进行推导
template<typename Container, typename Index> //C++14;
decltype<auto> //能够运行
authAndAccess(Container& c, Index i) //但仍需改进
{
authenticateUser();
return c[i];
}
authAndAccess(Container& c, Index i) 函数后面标注着仍需要改进,其实就是考虑将 Container& c 中的左值引用改为万能引用 Container&& c,这种时候如何修改返回值 c[i] 使得结果能被推导呢?答案如下:【使用 std::foward】// 本段代码的修改是为了让authAndAccess采用一种既能绑定到左值也能够绑定到右值的引用形参。
// C++11 标准下代码如下:
template<typename Container, typename Index> //C++11最终版
auto
authAndAccess(Container&& c, Index i) // 注意到 Index i 是按值传递
->decltype(std::forward<Container>(c)[i])
{
authenticateUser();
return std::forward<Container>(c)[i];
}
// C++14 标准下代码如下:
template<typename Container, typename Index> //C++14最终版
decltype<auto> // decltype 用的是 decltype 的推导规则
authAndAccess(Container&& c, Index i)
{
authenticateUser();
return std::forward<Container>(c)[i];
}
decltype 与普通表达式的结合情况,将 decltype 应用于一个名字之上,就会得出该名字的声明型别。// 正常情况:
decltype(auto) f1()
{
int x = 0; // x 是一个临时变量
...
return x; //decltype(x)是int,所以f1返回的是int
}
// 产生问题的情况:
decltype(auto) f2()
{
int x = 0; // x 是一个临时变量
...
return (x); //decltype((x))是int&,所以f2返回的是int&
}
x 变量是一个左值;表达式 (x) 也是一个左值,是一个比仅有名字更复杂的表达式;此时 decltype 会将结果推导为 T&。
decltype 会推断为 T&,而非 T。auto 遵循模板参数推导规则,总是推导出一个对象类型。decltype(auto) 遵循 decltype 规则,根据值类别推导出引用类型。int x;
int && f();
// expression auto decltype(auto)
// ----------------------------------------
// 10 int int
// x int int
// (x) int int &
// f() int int &&