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
3template <typename T>
void f(T param);
f({11, 23, 9}); // error! can't deduce T如果
param
的类型为std::initializer_list<T>
,则可以:1
2
3template <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)