mirror of
https://github.com/CnTransGroup/EffectiveModernCppChinese.git
synced 2024-12-28 05:40:43 +08:00
fix typos
This commit is contained in:
parent
4ecd7cff3d
commit
97e8e13582
@ -116,7 +116,7 @@ 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
|
||||
@ -138,7 +138,7 @@ void f(T param); //传值
|
||||
const char* const ptr = //ptr是一个常量指针,指向常量对象
|
||||
" Fun with pointers";
|
||||
````
|
||||
在这里,解引用符号(\*)的右边的const表示ptr本身是一个const:ptr不能被修改为指向其它地址,也不能被设置为null(解引用符号左边的const表示ptr指向一个字符串,这个字符串是const,因此字符串不能被修改)。当ptr作为实参传给f,像这种情况,ptr自身会传值给形参,根据类型推导的第三条规则,**ptr**自身的常量性将会被省略,所以param是** const char\* **。也就是说一个常量指针指向const字符串,在类型推导中这个指针指向的数据的常量性将会被保留,但是指针自身的常量性将会被忽略。
|
||||
在这里,解引用符号(\*)的右边的const表示ptr本身是一个const:ptr不能被修改为指向其它地址,也不能被设置为null(解引用符号左边的const表示ptr指向一个字符串,这个字符串是const,因此字符串不能被修改)。当ptr作为实参传给f,像这种情况,ptr自身会传值给形参,根据类型推导的第三条规则,**ptr**自身的常量性将会被省略,所以param是**const char\***。也就是说一个常量指针指向const字符串,在类型推导中这个指针指向的数据的常量性将会被保留,但是指针自身的常量性将会被忽略。
|
||||
|
||||
## 数组实参
|
||||
上面的内容几乎覆盖了模板类型推导的大部分内容,但这里还有一些小细节值得注意,比如在模板类型推导中指针不同于数组,虽然它们两个有时候是完全等价的。关于这个等价最常见的例子是在很多上下文中数组会退化为指向它的第一个元素的指针,比如下面就是允许的做法:
|
||||
|
@ -88,7 +88,7 @@ auto x2(27);
|
||||
auto x3={27};
|
||||
auto x4{27};
|
||||
````
|
||||
这些声明都能通过编译,但是他们不像替换之前那样有相同的意义。前面两个语句确实声明了一个类型为int值为27的变量,但是后面两个声明了一个存储一个元素27的 **std::initializer_list<int\> **类型的变量。
|
||||
这些声明都能通过编译,但是他们不像替换之前那样有相同的意义。前面两个语句确实声明了一个类型为int值为27的变量,但是后面两个声明了一个存储一个元素27的 **std::initializer_list<int\>**类型的变量。
|
||||
````cpp
|
||||
auto x1=27; //类型是int,值是27
|
||||
auto x2(27); //同上
|
||||
|
@ -138,13 +138,13 @@ authAndAccess(Container&& c,Index i)
|
||||
|
||||
为了完全理解decltype的行为,你需要熟悉一些特殊情况。它们大多数都太过晦涩以至于几乎没有书进行有过权威的讨论,这本书也不例外,但是其中的一个会让我们更加理解decltype的使用。
|
||||
|
||||
对一个名字使用decltype将会产生这个名字被声明的类型。名字是左值表达式,但那不影响decltype的行为,decltype确保产生的类型总是左值引用。换句话说,如果一个左值表达式除了名字外还有类型,那么decltype将会产生**T&**LEIX .这几乎没有什么太大影响,因为大多数左值表达式的类型天生具备一个左值引用修饰符。举个例子,函数返回左值,几乎也返回了左值引用。
|
||||
对一个名字使用decltype将会产生这个名字被声明的类型。名字是左值表达式,但那不影响decltype的行为,decltype确保产生的类型总是左值引用。换句话说,如果一个左值表达式除了名字外还有类型,那么decltype将会产生 **T&** LEIX .这几乎没有什么太大影响,因为大多数左值表达式的类型天生具备一个左值引用修饰符。举个例子,函数返回左值,几乎也返回了左值引用。
|
||||
|
||||
这个行为暗含的意义值得我们注意,在:
|
||||
````cpp
|
||||
int x =0;
|
||||
````
|
||||
中,x是一个变量的名字,所以**decltype(x)** 是**int**。但是如果用一个小括号包覆这个名字,比如这样**(x)** ,就会产生一个比名字更复杂的表达式。对于名字来说,**x**是一个左值,C++11定义了表达式**(x)** 也是一个左值。因此**decltype((x))** 是**int&** 。用小括号覆盖一个名字可以改变decltype对于名字产生的结果。
|
||||
中,x是一个变量的名字,所以**decltype(x)** 是**int**。但是如果用一个小括号包覆这个名字,比如这样 **(x)** ,就会产生一个比名字更复杂的表达式。对于名字来说,**x**是一个左值,C++11定义了表达式 **(x)** 也是一个左值。因此**decltype((x))** 是**int&** 。用小括号覆盖一个名字可以改变decltype对于名字产生的结果。
|
||||
|
||||
在C++11中这稍微有点奇怪,但是由于C++14允许了**decltype(auto)** 的使用,这意味着你在函数返回语句中细微的改变就可以影响类型的推导:
|
||||
````cpp
|
||||
|
@ -33,8 +33,8 @@ w1 = w2; //是一个赋值运算符,调用operator=函数
|
||||
````
|
||||
甚至对于一些初始化语法,在一些情况下C++98没有办法去表达初始化。举个例子,要想直接表示一些存放一个特殊值的STL容器是不可能的(比如Item1,3,5)
|
||||
|
||||
C++11使用统一初始化(uniform initialization)来整合这些混乱且繁多的初始化语法,所谓统一初始化是指使用单一初始化语法在任何地方_[0]_表达任何东西。
|
||||
它基于花括号,出于这个原因我更喜欢称之为括号初始化_[1]_。统一初始化是一个概念上的东西,而括号初始化是一个具体语法构型。
|
||||
C++11使用统一初始化(uniform initialization)来整合这些混乱且繁多的初始化语法,所谓统一初始化是指使用单一初始化语法在任何地方 _[0]_ 表达任何东西。
|
||||
它基于花括号,出于这个原因我更喜欢称之为括号初始化 _[1]_。统一初始化是一个概念上的东西,而括号初始化是一个具体语法构型。
|
||||
括号初始化让你可以表达以前表达不出的东西。使用花括号,指定一个容器的元素变得很容易:
|
||||
|
||||
````cpp
|
||||
@ -70,7 +70,7 @@ int sum2(x + y +z); //可以(表达式的值被截为int)
|
||||
|
||||
int sum3 = x + y + z; //同上
|
||||
````
|
||||
另一个值得注意的特性是括号表达式对于C++最令人头疼的解析问题_[2]_有天生的免疫性。
|
||||
另一个值得注意的特性是括号表达式对于C++最令人头疼的解析问题 _[2]_ 有天生的免疫性。
|
||||
C++规定任何能被决议为一个声明的东西必须被决议为声明。这个规则的副作用是让很多程序员备受折磨:当他们想创建一个使用默认构造函数构造的对象,却不小心变成了函数声明。
|
||||
问题的根源是如果你想使用一个实参调用一个构造函数,你可以这样做:
|
||||
|
||||
@ -183,7 +183,7 @@ Widget w2{10, true}; // 使用花括号初始化,调用第一个构造函
|
||||
Widget w3(10, 5.0); // 使用小括号初始化,调用第二个构造函数
|
||||
Widget w4{10, 5.0}; // 使用花括号初始化,调用第二个构造函数
|
||||
````
|
||||
代码的行为和我们刚刚的论述如出一辙。这里还有一个有趣的边缘情况_[3]_。
|
||||
代码的行为和我们刚刚的论述如出一辙。这里还有一个有趣的边缘情况 _[3]_。
|
||||
假如你使用的花括号初始化是空集,并且你欲构建的对象有默认构造函数,也有std::initializer_list构造函数。
|
||||
你的空的花括号意味着什么?如果它们意味着没有实参,就该使用默认构造函数,
|
||||
但如果它意味着一个空的std::initializer_list,就该调用std::initializer_list构造函数。
|
||||
|
@ -23,7 +23,7 @@ f(NULL); //可能不会被编译,一般来说调用f(int),绝对不
|
||||
而f(NULL)的不确定行为是由NULL的实现不同造成的。
|
||||
如果NULL被定义为**0L**(指的是**0**为**long**类型),这个调用就具有二义性,因为从**long**到**int**的转换或从**long**到**bool**的转换或**0L**到**void\* **的转换都会被考虑。
|
||||
有趣的是源代码表现出的意思(我指的是使用NULL调用f)和实际想表达的意思(我指的是用整型数据调用f)是相矛盾的。
|
||||
这种违反直觉的行为导致C++98程序员都将避开同时重载指针和整型作为编程准则_[0]_。
|
||||
这种违反直觉的行为导致C++98程序员都将避开同时重载指针和整型作为编程准则 _[0]_。
|
||||
在C++11中这个编程准则也有效,因为尽管我这个条款建议使用**nullptr**,可能很多程序员还是会继续使用**0**或**NULL**,哪怕**nullptr**是更好的选择。
|
||||
|
||||
**nullptr**的优点是它不是整型。
|
||||
@ -128,7 +128,7 @@ auto result3 = lockAndCall(f3, f3m, nullptr); // 没问题
|
||||
在模板**lockAndCall**中使用**0**之所以失败是因为得到的是**int**但实际上模板期待的是一个
|
||||
**std::shared_ptr<Widget>**
|
||||
|
||||
第二个使用**NULL**调用的分析也是一样的。当**NULL**被传递给**lockAndCall**,形参ptr被推导为整型_[1]_,
|
||||
第二个使用**NULL**调用的分析也是一样的。当**NULL**被传递给**lockAndCall**,形参ptr被推导为整型 _[1]_,
|
||||
然后当ptr——一个int或者类似int的类型——传递给f2的时候就会出现类型错误。当ptr被传递给f3的时候,
|
||||
隐式转换使**std::nullptr_t**转换为**Widget\* **,因为**std::nullptr_t**可以隐式转换为任何指针类型。
|
||||
|
||||
|
@ -22,7 +22,7 @@ typedef void (*FP)(int, const std::string&); // typedef
|
||||
using FP = void (*)(int, const std::string&); // 别名声明
|
||||
````
|
||||
|
||||
当然,两个结构都不是非常让人满意,没有人喜欢花大量的时间处理函数指针类型的别名_[0]_,所以至少在这里,没有一个吸引人的理由让你觉得别名声明比**typedef**好。
|
||||
当然,两个结构都不是非常让人满意,没有人喜欢花大量的时间处理函数指针类型的别名 _[0]_,所以至少在这里,没有一个吸引人的理由让你觉得别名声明比**typedef**好。
|
||||
|
||||
不过有一个地方使用别名声明吸引人的理由是存在的:模板。特别的,别名声明可以被模板化但是**typedef**不能。
|
||||
这使得C++11程序员可以很直接的表达一些C++98程序员只能把**typedef**嵌套进模板化的**struct**才能表达的东西,
|
||||
@ -91,7 +91,7 @@ private:
|
||||
如果你尝试过模板元编程(TMP), 你一定会碰到取模板类型参数然后基于它创建另一种类型的情况。
|
||||
举个例子,给一个类型**T**,如果你想去掉**T**的常量修饰和引用修饰,比如你想把**const std::string&**变成**const std::string**。
|
||||
又或者你想给一个类型加上**const**或左值引用,比如把**Widget**变成**const Widget**或**Widget&**。
|
||||
(如果你没有用过玩过模板元编程,太遗憾了,因为如果你真的想成为一个高效C++程序员_[1]_,至少你需要熟悉C++的基础。你可以看看我在Item23,27提到的类型转换)。
|
||||
(如果你没有用过玩过模板元编程,太遗憾了,因为如果你真的想成为一个高效C++程序员 _[1]_,至少你需要熟悉C++的基础。你可以看看我在Item23,27提到的类型转换)。
|
||||
C++11在<em>type traits</em>中给了你一系列工具去实现类型转换,如果要使用这些模板请包含头文件<b><type_traits></b>。
|
||||
里面不全是类型转换的工具,也包含一些`predictable`接口的工具。给一个类型**T**,你想将它应用于转换中,结果类型就是**std::transformation\<T\>::type**,比如:
|
||||
````cpp
|
||||
@ -104,7 +104,7 @@ std::add_lvalue_reference<T>::type // 从T中产出T&
|
||||
尽管写了一些,但我这里不是想给你一个关于type traits使用的教程。注意类型转换尾部的<b>::type</b>。
|
||||
如果你在一个模板内部使用类型参数,你也需要在它们前面加上**typename**。
|
||||
至于为什么要这么做是因为这些type traits是通过在**struct**内嵌套**typedef**来实现的。
|
||||
是的,它们使用类型别名_[2]_技术实现,而正如我之前所说这比别名声明要差。
|
||||
是的,它们使用类型别名 _[2]_ 技术实现,而正如我之前所说这比别名声明要差。
|
||||
|
||||
关于为什么这么实现是有历史原因的,但是我们跳过它(我认为太无聊了),因为标准委员会没有及时认识到别名声明是更好的选择,所以直到C++14它们才提供了使用别名声明的版本。
|
||||
这些别名声明有一个通用形式:对于C++11的类型转换**std::transformation<T>::type**在C++14中变成了**std::transformation_t.**。
|
||||
|
@ -126,7 +126,7 @@ void logAndProcess(T&& param)
|
||||
{
|
||||
auto now = //获取现在时间
|
||||
std::chrono::system_clock::now();
|
||||
makeLogEntry("calling 'process',now);
|
||||
makeLogEntry("calling 'process'",now);
|
||||
process(std::forward<T>(param));
|
||||
}
|
||||
```
|
||||
|
Loading…
Reference in New Issue
Block a user