心血来朝的就想翻译一下Effective Modern C++,非严谨翻译,大伙儿凑合着看吧。
记得刚接触C++是考上大学那会儿,为了更好的融入大学生活,特意在那个开学前的暑假骑了一小时的自行车到隔壁镇,风尘仆仆的进到当时镇上唯一的一家网吧。战战兢兢的打开电脑,学会了CS(Counter Strike_)。学校第一学期就开了C++,刚摸上电脑就接触这么高深莫测的语言真事有够胆战心惊的,这C++的课一开就是一年,结果一年下来,面向对象啥的听起来依旧如天书。
先来看看历史吧
- C++98(哥当年学的)只有一种类型推导:函数模板
- C++11修改了这个规则,添加了两个新的:auto以及decltype
- C++14继续扩展了auto及decltype的应用语境
自打上C++14,这语言就越发的灵活了,有太多的场景会出现类型推导,理解类型推导是怎么进行的就变的尤为重要。
这一章节会解释一下模板类型推导是遵循怎么个规则,auto/decltype又怎么在这个基础上进行类型推导。除此之外,我们还会教你怎么诱骗编译器按你想要的结果去工作,是不是很牛!
Item 1: 理解模板类型推导
无数的码农们每天都在愉快的使用着模板类型推导(“理所应当嘛,你你编译器当然得知道我说的是什么类型了,是吧”),然而对于内部怎么工作就全然不必去关心。
如果你正好是这些愉快码农中的一员,那我这里又一个好消息还有一个坏消息(你要先听哪一个?)。好消息是模板的类型推导和auto的类型推导系出同宗,如果你之前用C++98的template用的很愉悦,那么等你切换到C++11以后,auto的类型推导似乎是一样一样的。坏消息是模板类型推导规则被应用在auto类型推导的场景中时,往往不如模板类型推导那么直观,所以有必要去真正理解一下类型推导的规则。
先来看一段伪代码吧,思考这样的一个函数模板
template<typename T>
void f(ParamType param);
可以这样调用这个函数
f(expr); // call f with some expression
编译过程中,编译器依据expr来推导两个类型,一个是T,另一个是ParamType. 这两个类型经常是不一样的,因为ParamType经常会有一个比如说const的修饰符。
来看个例子,如下的定义模板函数
template<typename T>
void f (const T& param); // param type is const T&
并且这样调用这个函数
int x = 0;
f(x); //call f with an int
T被推导成int,ParamType推导成int&
这个例子里面T的类型就是函数入参expr的类型,x是int,自然能推导出T的类型也是int。但是并不是所有的时候都这样,T的类型推导有时候不单单取决于函数入参expr的类型,它还依赖于ParamType的类型。有如下三种场景
- ParamType是一个指针或者引用类型,但不是一个全局引用(全局引用在item24中有描述。这会儿你只要知道有这样一种引用,它区别于左值引用或右值引用)
- ParamType是一个全局引用
- ParamType既不是指针也不是引用
我们这里有三种类型推导的场景,都以如下的方式定义函数模板并调用
template<typename T>
void f (ParamType param);
f(expr); // deduce T and ParamType from expr
Case 1: ParamType是一个引用或指针,但不是全局引用
最简单的场景是ParamType是一个引用或者指针类型,但不是全局引用。这个时候类型推导是这么工作的
- 如果函数参数expr的类型是引用,忽略参数的引用特性
- 通过匹配expr的类型,获取ParamType的类型进而确定T的类型
例如,这是我们的函数模板
template <typename T>
void f(T& param); // param is a reference
我们定义如下的变量
int x = 27; // x is an int
const in cx = x; // cx is an const int
const int& rx = x; //rx is a reference to a const int
当函数被调用时,类型推导成下面这样
f(x); // T is int, param`s type is int&
f(cx); // T is const int, param`s type is const int&
f(rx); // T is const int, param`s type is const int&
第二个和第三个函数调用中,由于cx和rx指定成const类型,推导出T是const int,从而产生了参数类型是const int&。 这一点对于函数调用者来说很重要。当传 递一个const对象给一个引用类型的参数,函数调用者期望这个对象维持const特性(不可以修改)。例如,期望这个函数参数是一个const引用。这就是为什么传递一个const对象给这样的模板函数(携带T&参数)是安全的,对象常量性也成为了类型T推导的一部分了。
第三个例子中,即使rx的类型是一个引用,推导出来T的类型依旧是一个非引用类型(non-reference)。这是由于在类型推导过程中,rx的引用性(reference-ness)被忽略了。
上述的这些例子都是左值引用类型,但是类型推导的规则对于右值引用参数同样有用。当然,只有右值实参能传递给右值引用参数,但是这个限制不会影响类型推导
如果我们修改了函数f的参数,从T&变成constT&,这时候发生了一点点改变。cx和rx的常量性依旧会得以保留。但是这个时候类型T就不会再有const特性了(不需要推导成const类型了)。
template <typename T>
void f(const T& param); // param is now a ref-to const
int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x); // T is int, param`s type is const int&
f(cx); // T is int, param`s type is const int&
f(rx); // T is int, param`s type is const int&
和之前一样,rx的引用性(reference-ness)在类型推导的过程中忽略了
如果参数是变成了指针(或是一个指向const的指针),类型推导依旧遵循同样的规则
template<typename T>
void f (T* param); //param is now a pointer
int x = 27; // as before
const int *px = &x; // px is a ptr to x as a const int
f(&x); // T is int, param`s type is int*
f(px); // T is const int, param`s type is const int*
开始打盹儿了吧,因为c++的类型推导规则看起来是那么的理所应当的,大家会很自然的认为类型推导不就应该是那样的么。但当我们真正的一条条罗列出来所以然的时候一下就变的好枯燥了。
Case 2: ParamType是一个全局引用
对于模板函数参数是全局引用的场景(T&&),类型推导就不是那么显而易见了。这些参数往往被声明称右值引用(例如,一个函数模板的入参类型T,一个全局引用的声明方法是T&&),当左值参数传递进来时,这两种函数模板的行为是不一样的。在Item24中会详细描述,这里概述一下
- 如果expr是一个左值,T和Paramtype都被推导成为左值引用
- 如果expr是一个右值,使用通常情况下的推导规则
举个例子
template<typename T>
void f(T&& param); // param is now a universal reference
int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x); // x is lvalue, so T is int &
// param`s type is also int&
f(cx); // cx is lvalue, so T is const int &
// param`s type is also const int &
f(rx); // rx is lvalue, so T is const int&
// param`s type is also const int&
f(27); // 27 is rvalue, so T is int
// param`s type is int&&
很明显当使用全局引用的时候,类型推导区分左值参数和右值参数。对于non-universal引用来说,这是从未有过的,Item 24会详细的解释这个原因。
Case 3: ParamType既不是指针也不是引用
这里我们来说说传值调用
template<typename T>
void f(T param); //param is now passed by value
这里param是传入值的一个拷贝,一个全新的对象。param是一个全新对象的事实驱动T的类型推导规则
- 和之前一样,如果expr是引用类型,忽略入参的引用特性(reference-ness)
- 而后如果expr是const类型,一并忽略。如果是volatile类型,继续忽略(volatile不常用,详细参看Item40)
所以
int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x) // T`s and param`s type are both int
f(cx) // T`s and param`s type are again both int
f(rx) // T`s and param`s type are still both int
注意到这里cx和rx虽然代表const值,但param是全新的对象(cx或rx的一个拷贝),它不是const,这就说的通了。这就是为啥expr的这些特性(constness/volatileness/etc.)在类型推导的过程中都被忽略了
这里要记住只有传值参数的时候才会忽略这些const等等。但是当考虑这么个case,expr是一个指向const对象的const指针,然后expr按值传递进函数。如下,
template<typename T>
void f(T param); // param is still passed by value
const char* const ptr = "Fun with pointers" // ptr is const pointer to const object
f(ptr); // pass arg of the type const char* const
ptr这里是一个const指针,不能指向别的地方了,同样也不能设置成null。当ptr作为函数调用参数时,指针自身(ptr)会按值传递,指针(string的地址)复制到了param。ptr的常量性(constness)会被忽略掉,这时候param的类型推导出来是const char*,新的指针param可以指向不同的位置了,但是当前param指向的内容是不能改变的(这也很显而易见的)
数组作为参数
数组类型有别于指针类型,虽然它们有时候看起来可以互换。造成这种假象的原因是,很多场景下,数组会退化成指向数组头的指针。正因为有这种退化,使得下面代码能编译通过
const char name[] = "J.P.Briggs" // name`s type is const char[13]
char char * ptrToName = name; // arrary decays to pointer
这里的ptrToName被初始化成name,name是一个const类型的数组。
但是当传递一个数组给传值调用的模板函数的时候会发生些啥?参看下面的伪代码。
template<typename T>
void f(T param); // template with by-value parameter
f(name); // what types a deduced for T and param?
发现了没,函数的参数好像并没有数组类型嘛!
我们来看一个看起来有点儿像数组类型作为入参的例子。下面的函数定义就是合法的。
void myFunc(int param[]);
但是这里的参数param是被认作为一个指针的,意味着myFunc和下面定义的函数是等价的
void myFunc(int* param); // same function as above
正是有上述例子的存在,才使得数组和指针等价这个假象得以被很多人接受。
由于数组参数声明退化成指针参数,当数组作为一个值传递给一个模板函数,推导出来的类型应该是指针类型,意味着下面的代码中T被推导成const char*。
f(name); // name is array, but T deduced as const char*
接下来我们有一种曲线救国的方法(见证奇迹的时刻),虽然函数不能声明一个数组类型的参数,但是可以声明一个数组的引用类型参数。
template<typename T>
void f(T& param); // template with by-reference parameter
然后传递一个数组给这个函数
f(name); // pass array to f
这个时候T的类型就真正的变成一个array。这个类型还隐含了数组的大小。这个例子里面f的参数类型是const char(&)[13].
有意思的是,声明一个指向数组的引用使得我们可以创建这样一个模板函数,这个模板可以推导一个数组包含的元素个数。
// return size of an array as a compile-time constant. (The
// array parameter has no name, because we care only about
// the number of elements it contains.)
template <typename T, std::size_t N>
constexprstd::size_t arraySize(T (&) [N]) noexcept
{
// see info below on constexpr and noexcept
return N
}
正如Item15中描述的,声明这样的函数constexpr,使得在编译过程中就能获得函数运行结果。所以下面的代码实现就变的可行了,我们可以定义一个新的数组,这个数组的大小和另一个数组一样。
int keyVals[] = { 1, 3, 7, 9, 11, 22, 35}; //keyVals has 7 elements
int mappedVals[arraySize(keyVals)]; // so does mappedVals
当然,你可能更加喜欢std::array来定义数组。
std::array<int, arraySize(keyVals)> mappedVals;// mappedVals size is 7
函数作为参数
C++里面,函数同样也可以退化成函数指针,前面讨论的那些类型推导规则这里同样适用 。
void someFunc(int, double); // someFunc is a function;
// type is void(int, double)
template<typename T>
void f1(T param) ; // in f1, param is passed by value
template<typename T>
void f1(T& param); // in f2, param passed by ref
f1(someFunc); // param deduced as ptr-to-func
// type is void(*)(int, double)
f2(someFunc); // param deduced a ref-to-func;
// type is void(&)(int, double)
到这儿你就知道这些模板类型推导的规则了,所以吧,这些规则看上去就是这么的简单直接。唯一的污点就是universal references场景下的左值参数,还有退化为指针的规则。那能不能更简单点儿,抓住编译器然后命令它“你都给我推导成啥啥类型?”,看看Item 4吧,你会找到答案的
记住以下几点
- 模板类型推导时,忽略引用类型参数的引用性(reference-ness)
- 给universal reference参数进行类型推导时,左值要特别对待
- 传值参数的类型推导,入参的诸如所有const /volatile的特性都会忽略
- 模板板类型推导过程中,数组或函数做微参数时会退化成指针,除非模板函数的参数是引用类型
网友评论