类型推导—Effective modern C++ 学习笔记

auto和template虽然用起来很爽,但是作为程序员我们应该了解C++编译器做了哪些事情,从而确实的保证整套机制能够顺利的运作。

1
2
3
4
5
//模板声明部分
template<typename T>
void f(ParamType param);
//调用部分
f(expr)

模板类型推导就是关于如何根据expr的类型来推断出T的类型以及ParamType的类型。通常来讲,T和ParamType不会一样,因为ParamType往往会给T加上const或者引用之类的修饰。比如T可能是String类型,但ParamType是const String&类型,这在平常的函数模板使用中是非常常见的一种情况。

但是即便刨除T和ParamType之间的类型不同,我们也需要知道这样一个事情,虽然T的类型推导应该完全依赖于expr的类型,但是实际上并非如此。T的类型推导不仅仅依赖于expr的类型,也取决于ParamType的类型。

具体来看的话,ParamType造成的影响可以分为三类来进行讨论。

当ParamType是一个指针或引用,但不是通用引用

1
2
3
4
5
6
7
8
9
10
11
//模板声明部分
template<typename T>
void f(T& param);
//变量声明
int x=27; //x是int
const int cx=x; //cx是const int
const int& rx=x; //rx是指向作为const int的x的引用
//调用部分
f(x); //T是int,param的类型是int&
f(cx); //T是const int,param的类型是const int&
f(rx); //T是const int,param的类型是const int&

这里面注意观察的其实就是f(cx)和f(rx),即便传入参数类型是一个引用,T也并不会被推导为引用类型,这是因为在类型推导中,对象的引用性(reference-ness)会被忽略。不过从另一方面我们也可以看到常量性(constness)是没有被忽略的,传入一个const型的对象,T也会带有const。

同时如果更改一下这个ParamType的类型声明,我们可以看到一些不同的结果。

1
2
3
4
5
6
7
8
9
10
11
//模板声明部分
template<typename T>
void f(const T& param);
//变量声明
int x=27; //x是int
const int cx=x; //cx是const int
const int& rx=x; //rx是指向作为const int的x的引用
//调用部分
f(x); //T是int,param的类型是const int&
f(cx); //T是int,param的类型是const int&
f(rx); //T是int,param的类型是const int&

如果我们将const属性声明在了函数参数的形参中,那么T将不会保留常量性。

当ParamType是一个通用引用

首先对于那些不太了解通用引用的人(Universal Reference),我推荐Scott Meyers也就是本书作者在isocpp.org上的一篇博客,这里面详细讨论了什么是通用引用或者说万能引用。同时通用引用的符号容易让你以为这里的声明是右值引用,但其实两者完全不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//模板声明部分
template<typename T>
void f(T&& param); //param现在是一个通用引用类型
//变量声明
int x=27; //x是int
const int cx=x; //cx是const int
const int& rx=x; //rx是指向作为const int的x的引用
//调用部分
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&&

这里可以看到对于通用引用的类型推导规则是与常规引用完全不同的,当param被声明为通用引用时,T也会带上引用性。也就是说当通用引用被使用时,类型推导会区分左值实参和右值实参,但是对非通用引用时不会区分。

当ParamType既不是指针也不是引用

这种情况即是我们的函数进行按值传递的时候,也就是每次都会拷贝我们所传进来的实参。

1
2
3
4
5
6
7
8
9
10
11
//模板声明部分
template<typename T>
void f(T param);
//变量声明
int x=27; //x是int
const int cx=x; //cx是const int
const int& rx=x; //rx是指向作为const int的x的引用
//调用部分
f(x); //T和param的类型都是int
f(cx); //T和param的类型都是int
f(rx); //T和param的类型都是int

在这种情况下,T和param都会完全忽略传进来的实参的引用性和常量性,因为拷贝出来的对象的特性已经完全不受原来实参的影响了。

auto的类型推导

auto的类型推导其实与模板类型中T的推导大同小异,并且只在一个情况下会出现不同的推导。那就是当我们使用统一初始化(uniform initialization)时。

1
2
3
4
5
auto x1 = 27;                   //类型是int,值是27
auto x2(27); //同上
auto x3 = { 27 }; //类型是std::initializer_list<int>,
//值是{ 27 }
auto x4{ 27 }; //同上

在上述的情形下,auto都可以像常见模板中的T那样被推导出来类型,但以下会出现一种情况,auto可以被推导出来,但是在模板中无法推导出来。

1
2
3
4
5
auto x = { 11, 23, 9 };         //x的类型是std::initializer_list<int>
template<typename T> //带有与x的声明等价的
void f(T param); //形参声明的模板

f({ 11, 23, 9 }); //错误!不能推导出T

这是因为auto类型推导假定花括号表示std::initializer_list而模板类型推导并不会这么做。 并且还有一点值得注意,C++14中auto可以被用在函数的返回值以及lambda的形参说明中,但是auto在这里所使用的推导规则是模板类型推导。

引用

Effective Modern C++ 翻译