item 1: revise

This commit is contained in:
Cthulhu 2016-11-22 10:45:47 +08:00
parent aec829c230
commit 1cbd7760b3

View File

@ -1,8 +1,8 @@
# CHAPTER 1 Deducing Types
C++98有一套用于模板类型推导的规则C++11修改了其中的一些规则并为**auto**和**decltype**添加了新的规则。类型推导的广泛应用让我们不必再输入那些明显多的类型它让C++程序更具适应性,因为在源代码某处修改类型会通过类型推导自动传播到其地方。但是类型推导也会让代码更复杂,因为由编译器进行的类型推导并不总是如我们期望的那样进行。
C++98有一套用于模板类型推导的规则C++11修改了其中的一些规则并为**auto**和**decltype**添加了新的规则。类型推导的广泛应用让我们不必再输入那些明显多的类型它让C++程序更具适应性,因为在源代码某处修改类型会通过类型推导自动传播到其地方。但是类型推导也会让代码更复杂,因为由编译器进行的类型推导并不总是如我们期望的那样进行。
如果对于类型推导操作没有一个扎实的理解要想写出有现代感的C++程序是不可能的。类型推导随处可见:在函数模板调用中,在**auto**出现的地方,在**decltype**表达式出现的地方以及C++14中**decltype(auto)**的使用
如果对于类型推导操作没有一个扎实的理解要想写出有现代感的C++程序是不可能的。类型推导随处可见:在函数模板调用中,在**auto**出现的地方,在**decltype**表达式出现的地方以及C++14的**decltype(auto)** 中
这一章的内容是每个C++程序员都应该掌握的知识。它解释了模板类型推导是如何工作的,**auto**是如何依赖模板类型推导的,以及**decltype**是如何按照它自己那套独特的规则工作的。它甚至解释了你该如何强制编译器产生他进行类型推导的结果,这能让你确认编译器的类型推导是否按照你期望的那样进行。
@ -10,9 +10,9 @@ C++98有一套用于模板类型推导的规则C++11修改了其中的一些
条款一:理解模板类型推导
对于一个复杂系统的用户来说很多时候他们最关系的是它做了什么而不是它是怎么做的。在这一点上C++中的模板类型推导表现得非常出色。数百万的程序员只需要向模板函数传递参就能然后通过编译器的类型推导就能获得令人满意的结果,尽管他们中的大多数对于传递给函数的那些参是如何引导编译器进行类型推导的只能给出非常模糊的描述,而且还是在被逼无奈的情况。
对于一个复杂系统的用户来说很多时候他们最关心的是它做了什么而不是它怎么做的。在这一点上C++中的模板类型推导表现得非常出色。数百万的程序员只需要向模板函数传递参就能通过编译器的类型推导获得令人满意的结果,尽管他们中的大多数对于传递给函数的那些参是如何引导编译器进行类型推导的只能给出非常模糊的描述,而且还是在被逼无奈的情况。
如果那些人中包括你我有一个好消息和坏消息。好消息是现在C++最重要最吸引人的特性**auto**是建立在模板类型推导的基础上的如果你熟悉C++98的模板类型推导那么你不必害怕C++11的**auto**。坏消息是虽然**auto**是建立在模板类型推导的基础上,但是在某些情况下**auto**不如模板类型推导那么直观容易理解。这个条款便包含了你需要知道的关于模板类型推导的全部内容:
如果那些人中包括你,我有一个好消息和一个坏消息。好消息是现在C++最重要最吸引人的特性**auto**是建立在模板类型推导的基础上的如果你熟悉C++98的模板类型推导那么你不必害怕C++11的**auto**。坏消息是虽然**auto**是建立在模板类型推导的基础上,但是在某些情况下**auto**不如模板类型推导那么直观容易理解。这个条款便包含了你需要知道的关于模板类型推导的全部内容:
如果你不介意浏览少许伪代码,考虑这样一个函数模板:
````cpp
@ -53,7 +53,7 @@ f(expr); //从expr中推导T和ParamType
最简单的情况是**ParamType**是一个指针或者引用但非通用引用,也就是我们这个情景讨论的内容。在这种情况下,类型推导会这样进行:
1. 如果expr的类型是一个引用忽略引用部分
2. 然后通过ParamType与expr的类型进行模式匹配最后决定T
2. 然后剩下的部分决定T然后T与形参匹配得出最终ParamType
举个例子,如果这是我们的模板
````cpp
@ -72,14 +72,14 @@ f(x); //T是intparam的类型是int&
f(cx); //T是const intparam的类型是const int &
f(rx); //T是const intparam的类型是const int &
````
在第二个和第三个调用中注意因为cx和rx被指定为**const**值所以T被推导为**const int**,从而产生了**const int&**类型的**param**。这对于调用者来说很重要,当他们传递一个**const**对象给一个引用类型的参数时,他们传递的对象保留了常量性。这也是为什么向**T&**类型的参数传递**const**对象是安全的:对象T常量性会被保留为T的一部分。
在第二个和第三个调用中注意因为cx和rx被指定为**const**值所以T被推导为**const int**,从而产生了**const int&**类型的**param**。这对于调用者来说很重要,当他们传递一个**const**对象给一个引用类型的参数时,他们传递的对象保留了常量性。这也是为什么向**T&**类型的参数传递**const**对象是安全的对象T常量性会被保留为T的一部分。
在第三个例子中注意即使rx的类型是一个引用T也会被推导为一个非引用 ,这是因为如上面提到的如果**expr**的类型是一个引用,将忽略引用部分。
这些例子只展示了左值引用,但是类型推导会如左值引用一样对待右值引用。通常,右值只能传递给右值引用,但是在模板类型推导中这种限制将不复存在。
## 情景二ParamType一个通用引用
如果**ParamType**是一个通用引用那事情就比情景一更复杂了。如果**ParamType**被声明为通用引用(在函数模板中假设有一个**typename T**,那么通用引用就是**T&&**),它们的行为和**T&**大不相同完整的叙述请参见Item24在这有些最要的你还是需要知道:
如果**ParamType**是一个通用引用那事情就比情景一更复杂了。如果**ParamType**被声明为通用引用(在函数模板中假设有一个模板参数**T**,那么通用引用就是**T&&**),它们的行为和**T&**大不相同完整的叙述请参见Item24在这有些最要的你还是需要知道:
+ 如果**expr**是左值T和ParamType都会被推导为左值引用。这非常不寻常第一这是模板类型推导中唯一一种T和ParamType都被推导为引用的情况。第二虽然ParamType被声明为右值引用类型但是最后推导的结果它是左值引用。
+ 如果**expr**是右值,就使用**情景一**的推导规则
@ -89,23 +89,23 @@ f(rx); //T是const intparam的类型是const int &
template<typename T>
void f(T&& param); //param现在是一个通用引用类型
int x=27; //如之前一样
int x=27; //如之前一样
const int cx=x; //如之前一样
const int & rx=cx; //如之前一样
f(x); //x是左值所以T是int&
//param类型也是int&
//param类型也是int&
f(cx); //cx是左值所以T是const int &
//param类型也是const int&
//param类型也是const int&
f(rx); //rx是左值所以T是const int &
//param类型也是const int&
//param类型也是const int&
f(27); //27是右值所以T是int
//param类型就是int&&
//param类型就是int&&
````
Item24确切的解释了为什么这些例子要这样做。这里关键在于类型推导对于通用引用是不同于普通的左值或者右值引用。特别的,当通用引用被使用时,类型推导会区分左值实参和右值实参,但是**情况一**就不会。
Item24详细解释了为什么这些例子要这样做。这里关键在于类型推导对于通用引用是不同于普通的左值或者右值引用。比如,当通用引用被使用时,类型推导会区分左值实参和右值实参,但是**情况一**就不会。
## 情景三ParamType既不是指针也不是引用
当**ParamType**既不是指针也不是引用时我们通过传值pass-by-value的方式处理
@ -116,11 +116,11 @@ void f(T param); //以传值的方式处理param
这意味着无论传递什么param都会成为它的一份拷贝——一个完整的新对象。事实上param成为一个新对象这一行为会影响T如何从expr中推导出结果。
1. 和之前一样如果expr的类型是一个引用忽略这个引用部分
2. 如果忽略引用之后expr是一个const那就再忽略const。如果是volatile也会被忽略volatile不常见它通常用于驱动程序的开发中。关于volatile的细节请参见Item40)
2. 如果忽略引用之后expr是一个const那就再忽略const。如果是volatile也会被忽略volatile不常见它通常用于驱动程序的开发中。关于volatile的细节请参见Item40)
因此
````cpp
int x=27; //如之前一样
int x=27; //如之前一样
const int cx=x; //如之前一样
const int & rx=cx; //如之前一样
@ -128,7 +128,7 @@ f(x); //T和param都是int
f(cx); //T和param都是int
f(rx); //T和param都是int
````
注意即使**cx**和**rx**表示const值param也不是**const**。这是有意义的。param是一个拷贝自**cx**和**rx**且现在独立的完整对象。具有常量性的**cx**和**rx**不可修改并不代表param也是一样。这就是为什么expr的常量性或易变性volatileness)在类型推导时会被忽略:因为**expr**不可修改并不意味着他的拷贝也不能被修改。
注意即使**cx**和**rx**表示const值param也不是**const**。这是有意义的。param是一个拷贝自**cx**和**rx**且现在独立的完整对象。具有常量性的**cx**和**rx**不可修改并不代表param也是一样。这就是为什么expr的常量性或易变性volatileness)在类型推导时会被忽略:因为**expr**不可修改并不意味着他的拷贝也不能被修改。
认识到只有在传值给形参时才会忽略常量性和易变性这一点很重要正如我们看到的对于形参来说指向const的指针或者指向const的引用在类型推导时const都会被保留。但是考虑这样的情况expr是一个const指针指向const对象expr通过传值传递给**param**
````cpp
@ -138,16 +138,16 @@ void f(T param); //传值
const char* const ptr = //ptr是一个常量指针指向常量对象
" Fun with pointers";
````
在这里,解引用符号(*的右边的const表示ptr本身是一个constptr不能被修改为指向其地址也不能被设置为null解引用符号左边的const表示ptr指向一个字符串这个字符串是const因此它不能被修改。当ptr作为实参传给f时像这种情况ptr自身会传值给形参根据类型推导的第三条规则**ptr**自身的常量性将会被省略所以param是**const char* **。也就是说一个可修改的指针指向const字符串在类型推导中这个指针指向的数据的常量性将会被保留但是指针自身的常量性将会被忽略。
在这里,解引用符号(\*的右边的const表示ptr本身是一个constptr不能被修改为指向其地址也不能被设置为null解引用符号左边的const表示ptr指向一个字符串这个字符串是const因此字符串不能被修改。当ptr作为实参传给f像这种情况ptr自身会传值给形参根据类型推导的第三条规则**ptr**自身的常量性将会被省略所以param是**const char* **。也就是说一个常量指针指向const字符串在类型推导中这个指针指向的数据的常量性将会被保留但是指针自身的常量性将会被忽略。
## 数组实参
上面的内容几乎覆盖了模板类型推导的大部分内容,但这里还有一些小细节值得注意,比如在模板类型推导中指针不同于数组,虽然它们两个有时候是完全等价的。关于这个等价的错觉最常见的例子是在很多上下文中数组会退化为指向它的第一个元素的指针,比如下面就是允许的做法:
上面的内容几乎覆盖了模板类型推导的大部分内容,但这里还有一些小细节值得注意,比如在模板类型推导中指针不同于数组,虽然它们两个有时候是完全等价的。关于这个等价最常见的例子是在很多上下文中数组会退化为指向它的第一个元素的指针,比如下面就是允许的做法:
````cpp
const char name[] = "J. P. Briggs"; //name的类型是const char[13]
const char * ptrToName = name; //数组退化为指针
````
在这里**const char* **指针**ptrToName**会由name初始化而name的类型为**const char[13]**,这两种类型(**const char * **和**const char[13]**)是不一样的,但是由于数组退化为指针的规则,编译器允许这样的代码。
在这里**const char\* **指针**ptrToName**会由name初始化而name的类型为**const char[13]**,这两种类型(**const char \* **和**const char[13]**)是不一样的,但是由于数组退化为指针的规则,编译器允许这样的代码。
但要是一个数组传值给一个模板会怎样?会发生什么?
@ -167,11 +167,11 @@ void myFunc(int *param); //同上
````
这样的等价是C语言的产物C++又是建立在C语言的基础上它让人产生了一种数组和指针是等价的的错觉。
因为数组形参会视作指针形参所以传递给模板的一个数组类型会被推导为一个指针类型。这意味着在模板函数f的调用中它的模板类型参数T会被推导为**const char* **
因为数组形参会视作指针形参所以传递给模板的一个数组类型会被推导为一个指针类型。这意味着在模板函数f的调用中它的模板类型参数T会被推导为**const char\* **
````cpp
f(name); //name是一个数组但是T被推导为const char *
````
但是现在难题来了虽然函数不能接受真正的数组但是可以接受指向数组的引用所以我们修改f为传引用
但是现在难题来了虽然函数不能接受真正的数组但是可以接受指向数组的引用所以我们修改f为传引用
````cpp
template<typename T>
void f(T& param);
@ -193,7 +193,7 @@ constexpr std::size_t arraySize(T (&)[N]) noexcept
return N;
}
````
在Item15提到将一个函数声明为constexpr使得结果在编译期间可用。这使得我们可以用一个花括号声明一个数组然后再第二个数组可以使用这个数组的大小作为第二个数组的大小,就像这样:
在Item15提到将一个函数声明为constexpr使得结果在编译期间可用。这使得我们可以用一个花括号声明一个数组然后第二个数组可以使用第一个数组的大小作为它的大小,就像这样:
````cpp
int keyVals[] = {1,3,5,7,9,11,22,25}; //keyVals有七个元素
@ -206,7 +206,7 @@ std::array<int,arraySize(keyVals)> mappedVals; //mappedVals的size为7
至于**arraySize**被声明为**noexcept**会使得编译器生成更好的代码具体的细节请参见Item14。
## 函数实参
在C++中不止是数组会退化为指针,函数类型也会退化为一个函数指针,我们对于数组的全部讨论都可以应用到函数来:
在C++中不止是数组会退化为指针,函数类型也会退化为一个函数指针,我们对于数组的全部讨论都可以应用到函数来:
````cpp
void someFunc(int, double); //someFunc是一个函数类型是void(int,double)