C++98只有一组类型推断的规则:函数模板。C++11增加了两组:auto和decltype。C++14则扩展了auto和decltype的适用范围。
我们需要理解类型推断的规则,避免出了问题后脑海中一片白茫茫,Google都不知道该搜什么东西。
Item1: 理解模板类型推断
应用于函数模板的那套类型推断的规则很成功,数以百万的开发者都在用,即使大多数人都说不清楚这些规则。
但理解类型推断的规则非常重要,因为它是auto基础。但auto的有些规则反直觉,因此要好好学习下面的规则。
一个函数模板的例子:
| 1 | template <typename T> | 
以及对它的调用:
| 1 | f(expr); | 
expr要用来推断两个类型:T和ParamType。ParamType通常与T不同,比如带个const或&:
| 1 | template <typename T> | 
如果调用是:
| 1 | int x = 0; | 
那么T被推断为int,而ParamType则是const int&。
这里T就是expr的类型,符合大家的期望。但不总是这样。T的类型不仅与expr有关,也与ParamType的形式有关:
- ParamType是指针或引用(不是- universal reference即普适引用)。
- ParamType是普适引用。
- ParamType不是指针也不是引用。
Case1: ParamType是引用或指针,但不是普适引用
规则:
- 如果expr是引用,就忽略引用。
- 再用ParamType去模式匹配expr,来确定T。
例如:
| 1 | template <typename T> | 
注意cx和rx都令T被推断为const int,ParamType为const int&。这说明把const对象传给T&参数是安全的,不会令const对象被修改。
第三个调用中rx是引用,但T不是,是因为rx的引用属性在推断中被忽略了。
ParamType如果是const T&,那么三个调用中T都会被推断为int,而不再保留expr的const了。
如果ParamType是指针,那么类似:
| 1 | template <typename T> | 
总结起来就是,如果ParamType是引用或指针:
- expr的引用或指针性会被忽略。
- 如果expr带const,那么T和ParamType中需要且只需要有一个带const,来保证expr的const。
Case2: ParamType是普适引用
如果ParamType是T&&(即普适引用),但expr是左值引用,那么规则比较复杂:
- 如果expr是左值,那么T和ParamType都被推断为左值引用。两个不寻常处:- 这是模板类型推断中唯一的T被推断为引用的场景。
- 如果expr是右值,那么同Case1。
 
- 这是模板类型推断中唯一的
例如:
| 1 | template <typename T> | 
Case3: ParamType既不是指针也不是引用
如果ParamType既不是指针也不是引用,那么f就是传值调用,那么param就是一个全新的对象,规则:
- 同上,如果expr是引用,就忽略引用性。
- 如果expr带const或volatile,也忽略。
例子:
| 1 | template <typename T> | 
注意expr如果是指向const对象的指针,那么这个const不能被忽略掉。
数组参数
注意:数组作为参数时与指针的行为不同。
传值调用中,数组会被推断为指针:
| 1 | template <typename T> | 
但如果传引用,数组会被推断为数组:
| 1 | template <typename T> | 
我们可以利用这点在编译期拿到一个数组的长度:
| 1 | template <typename T, size_t N> | 
所以我们可以这么定义数组:
| 1 | int keyVals[] = {1, 3, 7, 9, 11, 22, 35}; // keyVals has 7 elements | 
或者用更现代的方式:
| 1 | std::array<int, arraySize(keyVals)> mappedVals; | 
函数参数
另一个会退化为指针的类型是函数。函数会退化为函数指针,且规则与数组相同。例如:
| 1 | void someFunc(int, double); // someFunc's type is void(int, double) | 
当然在实践上函数和函数指针几乎没有区别。
Item2: 理解auto类型推断规则
auto使用的类型推断规则与模板的规则几乎一样。
| 1 | auto x = 27; | 
auto就相当于上节中的T,而x、cx、rx的类型则是ParamType。
回忆一下上节介绍的ParamType的三种情况,同样可以应用在auto上:
- Case1: auto类型是指针或引用,但不是普适引用。
- Case2: auto类型是普适引用。
- Case3: auto类型既不是指针也不是引用。
| 1 | auto x = 27; // case3: int | 
以及针对数组和函数的规则:
| 1 | const char name[] = "R. N. Briggs"; // name is const char[13] | 
例外:auto会把所有的统一初始化式(花括号初始化式)当作std::initializer_list<T>对待。
在int的初始化中:
| 1 | int x1 = 27; | 
以上四种形式得到的x1到x4都是一个值为27的int。
但如果换成auto,后两者的类型就有些出乎意料了:
| 1 | auto x1 = 27; // x1 is int | 
即使这个初始化式根本没办法匹配成std::initializer_list<T>:
| 1 | auto x5 = {1, 2, 3.0}; // error! | 
如果把std::initializer_list<T>传给一个函数模板,行为则不一样:
- 如果 - param的类型为- T,则报错:- 1 
 2
 3- template <typename T> 
 void f(T param);
 f({11, 23, 9}); // error! can't deduce T
- 如果 - param的类型为- std::initializer_list<T>,则可以:- 1 
 2
 3- template <typename T> 
 void f(std::initializer_list<T> param);
 f({11, 23, 9}); // T is int and param is std::initializer_list<int>
C++14允许auto作为函数的返回类型,及lambda函数的参数类型,但这两种情况下的auto实际应用的是模板的类型推断规则,而不是上面说的auto规则!
| 1 | auto createInitList() | 
Item3: 理解decltype
通常decltype返回表达式的精确类型(注意,与模板和auto不同):
| 1 | const int i = 0; // decltype(i) is const int | 
C++11中,decltype还可以用来表示一个需要推断出来的类型返回类型:
| 1 | template <typename Container, typename Index> // requires refinement | 
一般容器的operator[]都会返回T&,但像vector<bool>就会返回一个中间类型,而用上面的decltype就能处理这种情况。
C++14中我们可以省掉尾部的返回类型:
| 1 | template <typename Container, typename Index> // C++14, not quite correct | 
上面的形式的问题在于:auto会抹去类型中的引用,导致我们想返回T&,但实际却返回了T。
所以实际上C++14我们需要写成decltype(auto):
| 1 | template <typename Container, typename Index> // C++14, requires refinement | 
我们可以把decltype(auto)中的auto替换成return后面的表达式,即decltype(c[i]),这样就能精确的表达我们的意图了。
decltype(auto)不仅能用于模板,同样能用于想拿到表达式精确类型的场合:
| 1 | Widget w; | 
再回头看上面C++14版本的authAndAccess,它的问题是参数类型为左值引用,因此无法接受右值参数(const Container&可以,但返回类型带const)。
什么情况下我们会传入一个容器的右值引用?也许是想拿到一个临时容器的成员的拷贝吧。
我们当然可以用重载来实现同时支持左值和右值引用的目的。如果不用重载的话,就需要把参数类型改成普适引用了:Container&& c。为了能同时处理左值和右值引用,我们要引入std::forward<T>,它在传入右值时返回右值,传入左值时返回左值。
| 1 | template <typename Container, typename Index> | 
C++11中我们需要使用尾部返回类型:
| 1 | template <typename Container, typename Index> | 
decltype一个容易忽视的特性是:如果它的括号中的表达式是左值,且不是一个变量的名字,那么decltype会保留引用。
因此decltype(x)返回int,但decltype((x))返回int&。
因此在使用decltype(auto)时,改变return后面的表达式形式,可能会改变返回的类型:
| 1 | decltype(auto) f1() { | 
Item4: 如何显示推断出来的类型
IDE
略
编译器诊断消息
可以通过编译器在出错时给出的诊断消息来显示推断出的类型。
首先声明一个模板类,但不给出任何定义:
| 1 | template <typename T> | 
然后用你想显示的类型来实例化这个模板:
| 1 | TD<decltype(x)> xType; | 
显然编译会出错,错误消息中就有我们想看到的类型:
| 1 | error: aggregate 'TD<int> xType' has incomplete type and cannot be defined | 
不同的编译器可能会给出不同格式的诊断消息,但基本上都是能帮到我们的。
运行时输出
方法1: std::type_info::name
| 1 | std::cout << typeid(x).name() << std::endl; | 
这种方法输出的类型可能不太好懂,比如在GNU和Clang中int会缩写为i,而const int*会缩写为PKi。
但有的时候std::type_info::name的输出靠不住:
| 1 | template <typename T> | 
在GNU下输出是:
| 1 | PK6Widget | 
意思是const Widget*。
问题来了:根据Item1的规则,这里T应该是const Widget*,而param应该是const Widget* const&啊!
不幸的是,这就是std::type_info::name的要求:它要像传值调用一个模板函数一样对待类型,即“去掉引用、const、volatile”。
所以const Widget* const&最终变成了const Widget*。
方法2: boost::typeindex::type_id_with_cvr
在方法1不奏效的时候,可以用boost::typeindex::type_id_with_cvr来查看类型。
| 1 | 
 | 
GNU输出为:
| 1 | Widget const* | 
而且,因为Boost是一个跨平台的开源库,因此在各个平台的各个编译器下,我们都能得到可用且正确的信息,尽管输出不完全一致。
目录
- Chapter1 类型推断 (Item 1-4)
- Chapter2 auto (Item 5-6)
- Chapter3 现代C++(Item 7-10)
- Chapter3 现代C++(Item 11-14)
- Chapter3 现代C++(Item 15-17)
- Chapter4 智能指针 (Item 18-22)
- Chapter5 右值引用、移动语义、完美转发(Item 23-26)
- Chapter5 右值引用、移动语义、完美转发(Item 27-30)
- Chapter6: Lamba表达式 (Item 31-34)
- Chapter7: 并发API (Item 35-37)
- Chapter7: 并发API (Item 38-40)
- Chapter8: 杂项 (Item 41-42)