条款 1:理解模板型别推导
-
该条款的前置知识为 C++ 表达式的值类别:
-
如下图所示: -
名词解释如下:
- glvalue,全称 generalized lvalue,?义左值。
- rvalue,通常只能放在等号右边的表达式,右值。
- lvalue,通常可以放在等号左边的表达式,左值。
- xvalue,全称 expiring lvalue,将亡值。
- prvalue,全称 pure rvalue,纯右值。
-
介绍左值 lvalue:
- 左值是有标识符、可以取地址的表达式。
- 变量、函数或数据成员的名字。
- 返回左值引?的表达式,如
++x 、cout << ' ' 。 - 字符串字?量如
"hello world" 。 - 在函数调?时,左值可以绑定到左值引?的参数,如
T& ;?个常量只能绑定到常左值引?,如 const T& 。 -
介绍纯右值 prvalue:
- 纯右值是没有标识符、不可以取地址的表达式。【例如临时对象】
- 返回?引?类型的表达式,如
x++ 、x+1 、make_shared<int>(42) 。 - 除字符串字?量之外的字?量,如
42 、true 。 - 在 C++11 之前:
- 右值可以绑定到常左值引?(const lvalue reference)的参数,如
const T& ,但不可以绑定到?常左值引?(non-const lvalue reference),如 T& 。 - 在 C++11 之后:
- C++ 语??多了?种引?类型?右值引?,右值引?的形式是
T&& ,?左值引?多?个 & 符号。 - 跟左值引??样,我们可以使?
const 和 volatile 来进?修饰。 -
模板的型别推导是 auto 的基础。【注意下面代码中,ParamType 其实是和 T 相关的】
template<typename T>
void f(ParamType param);
f(expr);
-
注意上述代码中,T 的型别推导结果,不仅仅依赖 expr 的型别,还依赖 ParamType 的形式,具体要分下述三种情况讨论。【ParamType 是形参类型,expr 是传参类型】 -
情况一,ParamType 具有指针或引用型别,但不是万能引用(universal reference)。【万能引用将在条款 24 中介绍】
- 若
expr 具有引用型别,先将引用部分忽略。【包括指针】 - 然后对
expr 的型别和 ParamType 的型别执行模式匹配,来决定 T 的型别。 - 当
ParamType 具有引用特性时,代码分析如下:【ParamType 是指针时也是差不多的结果】
template<typename T>
void f(T& param) {}
int x = 27;
const int cx = x;
const int& rx = x;
f(x);
f(cx);
f(rx);
- 上述代码有多个值得注意的地方:
- 首先,当向引用型别的形参传入
const 对象时,常量性会被保留。【即当 ParamType 是引用时,对该模板传入 const 对象很安全,连 T 也会保留其常量性】 - 其次,如前所述,
expr 的引用性被忽略了。 - 而当
ParamType 具有 const 引用特性时,代码分析如下:【这里和前面比较只有一个小地方不同】
template<typename T>
void f(const T& param) {}
int x = 27;
const int cx = x;
const int& rx = x;
f(x);
f(cx);
f(rx);
-
情况二,ParamType 是一个万能引用。【也就是 T&& 】【该部分的内容在条款 24 中会有更详细的解释】
- 如果
expr 是左值,则 T 和 ParamType 都会被推导为左值引用。【这里可以看出,型别推导只有一种特殊情况,只有在这种情况下,T 具有引用特性】 - 如果
expr 是右值,则按照常规来推导,所谓的常规情况就是情况一。 - 代码分析如下:
template<typename T>
void f(T&& param) {}
int x = 27;
const int cx = x;
const int& rx = x;
f(x);
f(cx);
f(rx);
f(27);
-
情况三,ParamType 既非指针也非引用,也就是按值传递。
- 代码分析如下:
template<typename T>
void f(T param) {}
int x = 27;
const int cx = x;
const int& rx = x;
f(x);
f(cx);
f(rx);
- 无论传入的
expr 是什么,param 都是一个全新对象,也就是一个副本。 - 也就是说,
T 和 param 的推导都是一样的,都会忽略 expr 的 const 特性、& 特性和 volatile 特性。 -
在本条款讨论的内容中,还需要继续讨论数组实参带来的影响。
-
总结:
- 在型别推导中,实参的引用性会被忽略。
- 对万能引用形参进行推导,左值实参会带来特殊情况。
- 对按值传递的形参进行推导,实参中的
const 特性、volatile 特性都会被忽略。 - 数组或函数如果面对引用形参则会保持原类型,否则(按值传递)就会退化为指针。
条款 2:理解 auto 型别推导
auto 型别推导其实可以等价于模板型别推导,代码分析如下:【auto 推导的情况和条款 1 中模板推导的情况是一致的(包括数组和函数退化成指针的情况)】
template<typename T>
void f(ParamType param);
f(expr);
auto x = 27;
const auto cx = x;
const auto& rx = x;
template<typename T>
void func_for_x(T param);
func_for_x(27);
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;
auto x2(27);
auto x3 = {27};
auto x4{27};
- 初始化列表的使用相当于在
auto 型别推导和模板型别推导中加多一个中间层。
- 需要注意的是,如果
std::initializer_list<T> 中的 T 推导失败,则 auto 的推导也会失败,因此大括号里的值需要保持一致的型别。 - 还有一点需要注意的是,
std::initializer_list<T> 其实就是一个和大括号“绑定”的型别(依我的理解是这样),如果想在普通的模板型别推导中使用,需要明确使用 std::initializer_list<T> 而不是 T 。【具体看书中代码】 - 最后要讨论的是在 C++14 中添加的内容:如果在函数返回值或者 lambda 式的形参中使用
auto 关键字,其实是在使用普通的模板型别推导,也就是说这两种情况底下不可以使用大括号。
条款 3:理解 decltype
- C++11 中,
decltype 的主要用途大概就在于声明那些返回值型别依赖于形参型别的函数模板。【通常情况下就是有什么就返回什么】 - 使用
decltype 来计算返回值型别的代码如下:【可以认为 decltype 的使用就是将代码的意图展露出来】
template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i)
->decltype(c[i])
{
authenticateUser();
return c[i];
}
- C++11 允许对单表达式的 lambda 式的返回值型别实施推导,而 C++14 则将这个允许返回扩张到了一切 lambda 式和一切函数,包括那些多表达式的。
- 对于
authAndAccess 这种情况来说,这就意味着在 C++14 中可以去掉返回值型别尾序语法,而只保留前导 auto 。 - 在那样的声明形式中,
auto 确实说明会发生型别推导,具体地说,它说明编译器会依据函数实现来实施函数返回值的型别推导:template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i)
{
authenticateUser();
return c[i];
}
vector<int>vec = { 1,2,3 };
authAndAccess(vec, 2) = 6;
- 所以
auto 和 decltype 的结合是很有必要的,如前面所述,decltype 的使用能帮助返回值找到本身类型。 - 但是在 C++14 中有了新的用法,即
decltype(auto) 饰词,代码如下:
template<typename Container, typename Index>
decltype<auto>
authAndAccess(Container& c, Index i)
{
authenticateUser();
return c[i];
}
- 注意上述 C++14 标准下的
authAndAccess(Container& c, Index i) 函数后面标注着仍需要改进,其实就是考虑将 Container& c 中的左值引用改为万能引用 Container&& c ,这种时候如何修改返回值 c[i] 使得结果能被推导呢?答案如下:【使用 std::foward 】
template<typename Container, typename Index>
auto
authAndAccess(Container&& c, Index i)
->decltype(std::forward<Container>(c)[i])
{
authenticateUser();
return std::forward<Container>(c)[i];
}
template<typename Container, typename Index>
decltype<auto>
authAndAccess(Container&& c, Index i)
{
authenticateUser();
return std::forward<Container>(c)[i];
}
- 本条款的最后讨论下
decltype 与普通表达式的结合情况,将 decltype 应用于一个名字之上,就会得出该名字的声明型别。 - 通常情况下就是推导,但是有个特别情况需要注意,问题代码如下:
decltype(auto) f1()
{
int x = 0;
...
return x;
}
decltype(auto) f2()
{
int x = 0;
...
return (x);
}
- 在上面代码中,
x 变量是一个左值;表达式 (x) 也是一个左值,是一个比仅有名字更复杂的表达式;此时 decltype 会将结果推导为 T& 。
- 也就是说,对于复杂表达式,
decltype 会推断为 T& ,而非 T 。 - 因此上述问题代码返回了一个临时变量的引用。
- 最后总结一下:
auto 遵循模板参数推导规则,总是推导出一个对象类型。decltype(auto) 遵循 decltype 规则,根据值类别推导出引用类型。- 代码如下:
int x;
int && f();
条款 4:掌握查看型别推导结果的方法
- 最关键的是理解 C++ 型别推导规则,其实就是理解条款 1 至条款 3。
|