前段时间看了《Effective Modern C++》这本书,收获颇多,书中讲解了许多C++11/14的特性,都是之前不太了解或者模糊的,看了之后茅塞顿开,强烈建议C++学习者看一看。
现在是第二遍,打算把之前看的总结一下,本文大量引用了原书的内容,如有不适,请提出。
《Effective Modern C++》开头说到:
C++98 had a single set of rules for type deduction: the one for function templates. C++11 modifies that ruleset a bit and adds two more,one for auto and one for decltype. C++14 then extends the usage contexts in which auto and decltype may be employed.
正如上面所说的,C++11/14中的新增的两个重要成分auto和decltype,它们在类型推导里起了很大的作用。这一节不讲这两个,下一节讲auto。
在function template中,例如:
template <typename T>
void f(ParamType param);
当我们调用:
f(expr); // call f with some expression
在编译期的时候,编译器通过调用expr来推导两个类型:一个是T,另一个是ParamType。(这里顺便说下,template的类型推导是在编译期完成的)
因此,这里就有三种情况:
注:通用引用(universal reference)是Scott Meyers提出的说法。
a universal reference’s declared type is T&&
例如:
template <typename T>
void f(T& param);
当我们写下:
int x = 27;
const int cx = x;
const int& rx = x;
函数调用如下:
f(x)
x是int类型,T推导为int类型,param的类型为int&类型
f(cx)
cx是const int类型,T推导为const int类型,param的类型为const int&类型
f(rx)
rx是const int&类型,T推导为const int类型,param的类型为const int&类型
第一个函数调用,相信大家都没什么疑问。第二个函数调用,相信有很大一部分人会半懂不懂,即便他推导正确!所以现在讲下第二个函数调用。
对于f(cx)的调用,cx是一个const int类型,由于paramType是一个引用类型,在C++中,引用就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。当cx作为实参传进函数的时候,形参param也是就是cx对象,由于cx是一个不可变变量,所以引用也是不可变的。这就是为什么T推导为const int类型。而不是说cx的类型为const int所以,T为const int类型。
理解了这点,我们可以提前举个例子,例如:
template <typename T>
void f(T param);
此时
int x = 27;
const int cx = x;
我们调用f(cx)的时候,T的类型是int,而不是const int
所以,这里有个很重要的知识点:将一个const对象传递给一个带有&参数的模板是安全的,对象的const属性将会变为T的类型推导的一部分。
对于第三个函数调用,类型推导会将rx的引用部分忽视。
如果将代码改为:
template <typename T>
void f(const T& param); // 区别于前面的一点就是这里加了const
当我们写下:
int x = 27;
const int cx = x;
const int& rx = x;
f(x),f(cx),f(rx)的T和param推导如上,这里就不写出来了,有兴趣的可以自己做下。
例如:
template <typename T>
void f(T* param); // 区别于前面的一点就是这里加了const
当我们写下:
int x = 27;
const int* px = &x;
函数调用如下:
f(x)
x是int类型,T推导为int类型,param的类型为int*类型
f(px)
cx是const int*类型,T推导为const int类型,param的类型为const int*类型
首先,我们要明白什么是左值,什么是右值。如果不清楚的话,请自行查阅资料。
template <typename T>
void f(ParamType param);
// ...
f(expr);
这里有两条规则:
1. 如果expr是一个左值(lvalue),T和paramType的类型推导为左值引用
2. 如果expr是一个右值(rvalue),正常(”normal”)的类型推导规则
例如:
template <typename T>
void f(T&& param);
当我们写下:
int x = 27;
const int cx = x;
const int& rx = 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&&
传参既不是指针(pass-by-pointer)也不是引用(pass-by-reference),那么就是传值(pass-by-value)。
例如:
template <typename T>
void f(T param);
由于是传值,所以也意味着param是一个拷贝后的临时对象。
所以,两条准则:
1. 如果expr的类型是一个引用,忽略它的引用
2. 如果expr的类型中含有const,volatile,也忽略它(注意,这里的const指的是顶层const)。
当我们写下:
int x = 27;
const int cx = x;
const int& rx = x;
const char* const ptr = "Fun with pointers";
函数调用如下:
f(x)
x是int,T为int,param为int
f(cx)
cx是const int,T为int,param为int
f(rx)
rx是const int&,T为int,param为int
f(ptr)
ptr是const char* const,T为const char*,param为const char*
这里主要说下第四个调用,对于ptr的类型来说,
const char* const ptr : 左边的const 是一个底层const,右边的const是一个顶层const。
在C++中,顶层const(top-level const)表示指针本身是个常数,而底层const(low-level const)表示指针所指对象是个常数,根据前面的规则,传值时,参数是拷贝后的临时对象,所以顶层引用在这里就不起作用了。
当我们通过传值调用函数的时候,如过参数是一个数组,会有许多不同。例如:
const char name[] = "J. P. Briggs"; // const char[13]
const char* ptrToName = name; // pointer
当我们调用f(name)时,T将会被推导为const char*。对于C/C++的代码:
void myFunc(int param[]);
void myFunc(int* param);
这里两个函数是一样的。知道这一点后,我们就能够理解为什么T会被推导为const char*。
it fosters the illusion that array and pointer types are the same.
所以,要推导为数组,我们需要加一个引用!例如:
template <typename T>
void f(T& param);
f(name);
此时T的类型为const char[13]
书中列出的一个代码用于得出输出数组的大小,代码如下:
template <typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)N) noexcept
{
return N;
}
例如:
void someFunc(int, double);
template <typename T>
void f1(T param);
template <typename T>
void f2(T& param);
当我们调用:
f1(someFunc); // param's type is void (*)(int, double);
f2(someFunc) // param's type is void (&)(int, double);