理解 cpp 是如何进行类型推导的,这样在使用时才不会踩太多坑……
模板类型推导
基础
一个普通的函数模板就像下面这样:
1 |
|
对应的输出为:
The sum of u16 buffer is 15 The sum of u32 buffer is 40 The sum of float buffer is 11.158
使用函数模板来代替函数重载,这是一种简单而优雅的做法。
从表面上来看,似乎编译器是自然而然的就可以根据实参的类型推导出类型T
,然而实际上类型T
和形参所使用的T
是不同的。
就拿上面的 GetSum 函数模板举例,被推导的类型实际上有: 1. 模板类型 T 2. 函数形参 const T*
最终,真正决定T
的类型,是由实参和形参类型共同所决定的,具有以下
3 种情况: 1. 形参是一个指针或引用类型,但并不是通用引用 2.
形参是一个通用引用 3. 形参既不是指针也不是引用
形参是一个指针或引用类型,但并不是通用引用
这种情况下的推导步骤如下: -
如果实参是一个引用(无论是左值还是右值引用)或指针,那么就忽略引用或指针的部分
- 然后再根据实参剩余部分和形参共同决定类型T
1
2
3
4
5
6
7
8
9
10
11
12
13
14/**
* 第一种函数模板形参
*/
template<typename T>
void f(T& param);
// 具有以下 3 种实参
int x = 27;
const int cx = x;
const int & rx = x;
//对应的推导结果就是
f(x); //T 类型为 int,形参类型为 int&
f(cx); //T 类型为 const int,形参类型为 const int&
f(rx); //T 类型为 const int,形参类型为 const int&
可以看到,实参rx
的引用被去掉了,最终类型T
和cx
是一致的情况。
1
2
3
4
5
6
7
8
9
10
11
12
13
14/**
* 第二种函数模板形参
*/
template<typename T>
void f(const T& param);
// 具有以下 3 种实参
int x = 27;
const int cx = x;
const int & rx = x;
//对应的推导结果就是
f(x); //T 类型为 int,形参类型为 const int&
f(cx); //T 类型为 int,形参类型为 const int&
f(rx); //T 类型为 int,形参类型为 const int&
可以看到,这一次的类型T
统一为int
型。 -
由于第一种情况下,形参并没有使用const
限定符,而实参使用了const
限定符,那么用户的期望是希望T
是无法被改变的类型,所以T
被推导成了
const int。 -
但是第二种情况下,形参已经使用了const
限定符,那么实参是否有const
已经不重要了,这种情况下就可以使用更为宽松的int
类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25/**
* 第三种函数模板形参
*/
template<typename T>
void f(T* param);
// 具有以下 2 种实参
int x = 27;
const int *px = &x;
//对应的推导结果就是
f(&x); //T 类型为 int,形参类型为 int*
f(px); //T 类型为 const int,形参类型为 const int*
/**
* 第四种函数模板形参
*/
template<typename T>
void f(const T* param);
// 具有以下 2 种实参
int x = 27;
const int *px = &x;
//对应的推导结果就是
f(&x); //T 类型为 int,形参类型为 const int*
f(px); //T 类型为 int,形参类型为 const int*
可以看到,使用指针的情况也是和引用类似的。
形参是一个通用引用
- 如果实参是一个左值,那么类型 T 和形参都会被推导成左值引用
- 即使形参是右值引用,也会被推导成左值引用
- 如果实参是一个右值,那么就会使用 1.2 节所述规则来进行推导
1
2
3
4
5
6
7
8
9
10
11
12
13//假设如下的函数模板
template<typename T>
void f(T&& param);
//实参如下
int x = 27;
const int cx = x;
const int &rx = x;
//对应的推导结果就是
f(x); //x 是左值,T 类型就是 int&,形参类型也是 int&
f(cx);//cx 是左值,T 类型就是 const int&,形参类型也是 const int&
f(rx);//rx 是左值,T 类型就是 const int&,形参类型也是 const int&
f(27);//27 是右值,T 类型就是 int,形参类型是 int&&
形参既不是指针也不是引用
- 如果实参是一个引用,忽略掉引用部分
- 忽略掉引用后,如果实参部分是
const
或volatile
,也忽略掉const
或volatitle
。
其中的逻辑在于,函数模板此时的形参是
passed by value
的形式。 那么即使实参是const
或volatile
修饰,它的被拷贝副本就可以不用是const
或volatile
修饰了。
1 | //假如如下的函数模板 |
- 虽然
cx
和rx
都是const
类型,但由于函数模板是普通形参,无论实参如何,形参都是对实参的拷贝。所以形参都是int
类型。 - 而
ptr
是指向const char *
型的const
指针,所以ptr
本身的const
被 passed by value,但该ptr
所指向的对象依然应该是const char *
。
数组参数
需要注意的是:对于模版参数而言,数组参数和指针参数不是一个东西
1
2
3
4
5
6
7
8//假如如下的函数模板
template<typename T>
void f(T param);
//实参如下
const char name[] = "J.P. Briggs";
//对应的推导结果就是
f(name); //类型 T 和 形参均为 const char * 型1
2
3
4
5
6
7
8//假如如下的函数模板
template<typename T>
void f(T& param);
//实参如下
const char name[] = "J. P. Briggs";//name 的长度是 13 字节
//对应的推导结果就是
f(name); //类型 T 为 const char [13], 形参为 const char(&)[13]
函数参数
另一个需要注意的就是函数指针: 1
2
3
4
5
6
7
8
9
10
11void someFunc(int, double);
template<typename T>
void f1(T param);
template<typename T>
void f2(T& param);
f1(someFunc);// 类型 T 和形参均为 void(*)(int, double);
f2(someFunc);// 类型 T 为 void()(int, double),形参为 void(&)(int, double)
理解 auto 类型的推导
auto 推导与模版推导的相同之处
有了前面的基础,就可以比较容易的理解 auto
推导的逻辑。
其实 auto
推导和模版推导几乎一致: -
模版中使用实参和形参决定 T
和 形参 - 而 auto
就类似于
T
,其他附加限定符就类似于形参,赋值号右边的就相当于实参
比如如下示例: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//对 auto 的使用
auto x = 27; //auto 为 int,x 为 int
const auto cx = x;//auto 为 int,cx 为 const int
const auto &rx = x;//auto 为 int,rx 为 const int &
auto && uref1 = x;// auto 和 uref1 均为 int &
auto && uref2 = cx;// auto 和 uref2 均为 const int &
auto && uref3 = 27;//auto 为 int,uref3 为 int &&
//分别对应于函数模版
template<typename T>
void func_for_x(T param);
func_for_x(27);//T 和 param 均为 int
template<typename T>
void func_for_cx(const T param);
func_for_cx(x);//T 为 int,param 为 const int
template<typename T>
void func_for_rx(const T& param);
func_for_rx(x);// T 为 int,param 为 const int &
同样的,auto
推导也具有 3 种情况: -
限定符是指针或引用,但不是通用引用 - 限定符是通用引用 -
限定符既不是指针也不是引用
同样的,对于数组和函数指针也有例外: 1
2
3
4
5
6
7
8
9const char name[] = "R. N. Briggs";//name 的长度为 13 字节
auto arr1 = name;//auto 和 arr1 均为 const char *
auto &arr2 = name;//auto 为 const char()[13],arr2 为 const char (&)[13]
void someFunc(int, double);
auto func1 = someFunc;//auto 和 func1 均为 void (*)(int, double)
auto &func2 = someFunc;//auto 为 void()(int, double),fun1 为 void(&)(int, double)1
2
3
4int x1 = 27;
int x2(27);
int x3 = {27};
int x4{27};int
型变量,其值为 27。但如果使用
auto
,情况就有些不同: 1
2
3
4auto x1 = 27; // x1 是 int
auto x2(27);// x2 是 int
auto x3 = {27};//x3 是 std::initializer_list<int> 类型并包含一个元素,其值为 27
auto x4{27};//x4 是 std::initializer_list<int> 类型并包含一个元素,其值为 27
实际上使用初始值列表推导有两个步骤: -
因为右值是初始值列表,所以首次推导为 std::initializer_list
基于以上认识,下面这些情况下推导就会出错:
1 | //错误! |
在 c++ 14 中,函数可以使用 auto
作为返回,但是这种情况下就无法正确的推导初始化列表了:
1
2
3
4auto createInitList() {
//错误,无法推导
return {1, 2, 3};
}
理解 decltype
decltype
规则
decltype
对于变量和表达式的推导,总是忠实的反应其类型- 对于左值表达式,由于其可被赋值,所以推导的类型总是 T&
- c++14 支持
decltype(auto)
,使得auto
以decltype
的规则进行推导
使用 decltype
的场合
c++ 11
在 c++11 中,decltype
经常使用的场景是用于模板函数:当输入的参数类型不一样,得到的函数返回类型也不一样。
1 | template<typename Container, typename Index> |
对于上面的模板函数,要返回 c[i] 类型,但是由于 c 和 i 的类型并无法提前知晓,那么这里使用 decltype 是合理的方式。
c++ 14
对于 c++14 而言,从语法上来讲是可以忽略上面的尾置返回类型的,使用 auto 来推导返回的类型,但是这可能会出错:当需要将函数的返回作为左值时:
1 | std::deque<int> d; |
为了能够在 c++14 中返回引用,也需要使用 decltype
:
1 | template<typename Container, typename Index> |
decltype
与 auto
合用,可以使得以
decltype
的形式进行推导:
1 | Widget w; |
使用 decltype
的注意事项
- 当直接推导变量名时,得到的是变量名对应的类型
- 当变量名由括号所包含时,得到的是变量名对应类型的引用
这种特性使得在函数返回时,很有趣:
1 | decltype(auto) F1(){ |
查看推导的类型
在理解了基本的推导原则后,为了查看及验证推导的类型,使用编译时获取和基于 boost 库获取是最为靠谱的方案。
在编辑器中获取
在大多数 IDE 中的编辑器,如果代码没有语法错误,那么将鼠标指向被推导的变量,就会出现该变量的提示。
但是,在一些稍微复杂的场合,这些提示往往是不准确的。
在编译过程中获取
通过故意使得编译出错,从而使编译展示该类型:
1 |
|
编译过程中便会有如下类似错误:
[Error] aggregate 'TypeDisplay
type1' has incomplete type and cannot be defined [Error] aggregate 'TypeDisplay<int&> type2' has incomplete type and cannot be defined
对于稍微复杂一点的场景也可以:
1 |
|
错误输出如下:
[Error] 'TypeDisplay<const float*> type1' has incomplete type
[Error] 'TypeDisplay<const float* const&> type2' has incomplete type
在运行过程中获取
使用 typeid
很多时候并不能准确的推导类型:
1 |
|
以上代码用 gcc 编译后的输出是:
Hello world i i
i 代表 int
类型,但是第二种情况实际上应该是
int &
。
在运行时的环境中,只有 boost
库提供的方法能够准确的显示被推导的类型。
1 |
|