mirror of
https://github.com/CnTransGroup/EffectiveModernCppChinese.git
synced 2024-12-28 05:40:43 +08:00
Merge pull request #57 from AndyJMR/master
Bold all "Things to Remember"
This commit is contained in:
commit
d203a2f5cf
@ -223,7 +223,7 @@ f2(someFunc); //param被推导为指向函数的引用,类型为void(&)(int,
|
||||
|
||||
这里你需要知道:**auto**依赖于模板类型推导,正如我在开始谈论的,在大多数情况下它们的行为很直接。在通用引用中对于左值的特殊处理使得本来很直接的行为变得有些污点,然而,数组和函数退化为指针把这团水搅得更浑浊。有时你只需要编译器告诉你推导出的类型是什么。这种情况下,翻到**item4**,它会告诉你如何让编译器这么做。
|
||||
|
||||
记住:
|
||||
**记住**:
|
||||
|
||||
+ 在模板类型推导时,有引用的实参会被视为无引用,他们的引用会被忽略
|
||||
+ 对于通用引用的推导,左值实参会被特殊对待
|
||||
|
@ -138,7 +138,7 @@ auto resetV = [&v](const auto & newValue){v=newValue;}; //C++14
|
||||
reset({1,2,3}); //错误!推导失败
|
||||
````
|
||||
|
||||
记住:
|
||||
**记住**:
|
||||
|
||||
+ auto类型推导通常和模板类型推导相同,但是auto类型推导假定花括号初始化代表**std::initializer_list**而模板类型推导不这样做
|
||||
+ 在C++14中auto允许出现在函数返回值或者lambda函数形参中,但是它的工作机制是模板类型推导那一套方案。
|
@ -167,7 +167,7 @@ decltype(auto) f2()
|
||||
|
||||
同时你也不应该忽略decltype这块大蛋糕。没错,decltype可能会偶尔产生一些令人惊讶的结果,但那毕竟是少数情况。通常,decltype都会产生你想要的结果,尤其是当你对一个名字使用decltype时,因为在这种情况下,decltype只是做一件本分之事:它产出名字的声明类型。
|
||||
|
||||
记住
|
||||
**记住**
|
||||
|
||||
+ **decltype**总是不加修改的产生变量或者表达式的类型。
|
||||
+ 对于T类型的左值表达式,**decltype**总是产出T的引用即**T&**。
|
||||
|
@ -149,7 +149,7 @@ param= class Widget const * const&
|
||||
````
|
||||
这样近乎一致的结果是很不错的,但是请记住IDE,编译器错误诊断或者Boost.TypeIndex只是用来帮助你理解编译器推导的类型是什么。它们是有用的,但是作为本章结束语我想说它们根本不能让你不用理解Item1-3提到的。
|
||||
|
||||
记住
|
||||
**记住**
|
||||
|
||||
+ 类型推断可以从IDE看出,从编译器报错看出,从一些库的使用看出
|
||||
+ 这些工具可能既不准确也无帮助,所以理解C++类型推导规则才是最重要的
|
@ -133,7 +133,7 @@ for(const auto & p : m)
|
||||
|
||||
真正的问题是显式指定类型可以避免一些微妙的错误,以及更具效率和正确性,而且,如果初始化表达式改变变量的类型也会改变,这意味着使用auto可以帮助我们完成一些重构工作。举个例子,如果一个函数返回类型被声明为int,但是后来你认为将它声明为long会更好,调用它作为初始化表达式的变量会自动改变类型,但是如果你不使用auto你就不得不在源代码中挨个找到调用地点然后修改它们。
|
||||
|
||||
记住
|
||||
**记住**
|
||||
|
||||
+ auto变量必须初始化,通常它可以避免一些移植性和效率性的问题,也使得重构更方便,还能让你少打几个字。
|
||||
+ 正如Item2和6讨论的,auto类型的变量可能会踩到一些陷阱。
|
||||
|
@ -105,7 +105,7 @@ int index = d * c.size();
|
||||
auto index = static_cast<int>(d * size());
|
||||
````
|
||||
|
||||
记住
|
||||
**记住**
|
||||
|
||||
+ 不可见的代理类可能会使auto从表达式中推导出“错误的”类型
|
||||
+ 显式类型初始器惯用法强制auto推导出你想要的结果
|
||||
|
@ -214,7 +214,7 @@ may decide that typing a few extra characters is a reasonable price to pay for t
|
||||
to avoid the pitfalls of an enum technology that dates to a time when the state of
|
||||
the art in digital telecommunications was the 2400-baud modem.
|
||||
|
||||
记住
|
||||
**记住**
|
||||
+ C++98的枚举即非限域枚举
|
||||
+ 限域枚举的枚举名仅在enum内可见。要转换为其它类型只能使用cast。
|
||||
+ 非限域/限域枚举都支持基础类型说明语法,限域枚举基础类型默认是`int`。非限域枚举没有默认基础类型。
|
||||
|
@ -120,7 +120,7 @@ void Widget::processPointer<void>(void*) = delete; // 还是public,但是已
|
||||
事实上C++98的最佳实践即声明函数为*private*但不定义是在做C++11 delete函数要做的事情。作为模仿者,C++98的方法不是十全十美。它不能在类外正常工作,不能总是在类中正常工作,它的罢工可能直到链接时才会表现出来。所以请坚定不移的使用`delete`函数。
|
||||
|
||||
|
||||
记住:
|
||||
**记住**:
|
||||
+ 比起声明函数为private但不定义,使用delete函数更好
|
||||
+ 任何函数都能`delete`,包括非成员函数和模板实例
|
||||
|
||||
|
@ -181,6 +181,7 @@ auto vals2 = makeWidget().data(); //调用右值重载版本的Widget::data,
|
||||
```
|
||||
这真的很nice,但别被这结尾的暖光照耀分心以致忘记了该条款的中心。这个条款的中心是只要你在派生类声明想要重写基类虚函数的函数,就加上`override`。
|
||||
|
||||
记住:
|
||||
**记住**:
|
||||
|
||||
+ 为重载函数加上`override`
|
||||
+ 成员函数限定让我们可以区别对待左值对象和右值对象(即`*this`)
|
@ -81,7 +81,7 @@ auto cbegin(const C& container)->decltype(std::begin(container))
|
||||
|
||||
回到最开始,本条款的中心是鼓励你只要能就使用**const_iterator**。最原始的动机是——只要它有意义就加上const——C++98就有的思想。但是在C++98,它(译注:const_iterator)只是一般有用,到了C++11,它就是极其有用了,C++14在其基础上做了些修补工作。
|
||||
|
||||
记住
|
||||
**记住**
|
||||
|
||||
+ 优先考虑const_iterator而非iterator
|
||||
+ 在最大程度通用的代码中,优先考虑非成员函数版本的**begin**,**end**,**rbegin**等,而非同名成员函数
|
||||
|
@ -114,7 +114,7 @@ C标准库移动到了**std**命名空间,也可能缺少异常规范,**std:
|
||||
因为有很多合理原因解释为什么**noexcept**依赖于缺少**noexcept**保证的函数,所以C++允许这些代码,编译器
|
||||
一般也不会给出warnigns。
|
||||
|
||||
记住:
|
||||
**记住**:
|
||||
+ **noexcept**是函数接口的一部分,这意味着调用者会依赖它、
|
||||
+ **noexcept**函数较之于非**noexcept**函数更容易优化
|
||||
+ **noexcept**对于移动语义,**swap**,内存释放函数和析构函数非常有用
|
||||
|
@ -134,6 +134,6 @@ constexpr auto reflectedMid = // reflectedMid的值
|
||||
|
||||
还有个重要的需要注意的是**constexpr**是对象和函数接口的一部分。加上**constexpr**相当于宣称“我能在C++要求常量表达式的地方使用它”。如果你声明一个对象或者函数是constexpr,客户端程序员就会在那些场景中使用它。如果你后面认为使用constexpr是一个错误并想移除它,你可能造成大量客户端代码不能编译。**尽可能**的使用**constexpr**表示你需要长期坚持对某个对象或者函数施加这种限制。
|
||||
|
||||
记住
|
||||
**记住**
|
||||
+ **constexpr**对象是**cosnt**,它的值在编译期可知
|
||||
+ 当传递编译期可知的值时,**cosntexpr**函数可以产出编译期可知的结果
|
@ -186,7 +186,7 @@ private:
|
||||
|
||||
现在,这个条款是基于,多个线程可以同时在一个对象上执行一个const成员函数这个假设的。如果你不是在这种情况下编写一个const成员函数。也就是你可以保证在对象上永远不会有多个线程执行该成员函数。再换句话说,该函数的线程安全是无关紧要的。比如,为单线程使用而设计类的成员函数的线程安全是不重要的。在这种情况下你可以避免,因使用 `mutex` 和 `std::atomics`所消耗的资源,以及包含它们的类只能使用移动语义带来的副作用。然而,这种单线程的场景越来越少见,而且很可能会越来越少。可以肯定的是,const成员函数应支持并发执行,这就是为什么你应该确保const成员函数是线程安全的。
|
||||
|
||||
> 应该注意的事情
|
||||
> **应该注意的事情**
|
||||
>
|
||||
> + 确保const成员函数线程安全,除非你确定它们永远不会在临界区(concurrent context)中使用。
|
||||
> + `std::atomic`可能比互斥锁提供更好的性能,但是它只适合操作单个变量或内存位置。
|
||||
|
@ -114,7 +114,7 @@ class Widget {
|
||||
```
|
||||
编译器仍会生成移动和拷贝操作(假设正常生成它们的条件满足),即使可以模板实例化产出拷贝构造和拷贝赋值运算符的函数签名。(当T为Widget时)。很可能你会决定这是一个不值得承认的边缘情况,但是我提到它是有道理的,Item16将会详细讨论它可能带来的后果。
|
||||
|
||||
记住:
|
||||
**记住**:
|
||||
+ 特殊成员函数是编译器可能自动生成的函数:默认构造,析构,拷贝操作,移动操作。
|
||||
+ 移动操作仅当类没有显式声明移动操作,拷贝操作,析构时才自动生成。
|
||||
+ 拷贝构造仅当类没有显式声明拷贝构造时才自动生成,并且如果用户声明了移动操作,拷贝构造就是delete。拷贝赋值运算符仅当类没有显式声明拷贝赋值运算符时才自动生成,并且如果用户声明了移动操作,拷贝赋值运算符就是delete。当用户声明了析构函数,拷贝操作不再自动生成。
|
||||
|
@ -266,7 +266,7 @@ std::vector<int> v;
|
||||
这正是标准库函数std::make_unique和std::make_shared(参见Item21)面对的问题。
|
||||
它们的解决方案是使用小括号,并被记录在文档中作为接口的一部分。
|
||||
|
||||
记住
|
||||
**记住**
|
||||
+ 括号初始化是最广泛使用的初始化语法,它防止变窄转换,并且对于C++最令人头疼的解析有天生的免疫性
|
||||
+ 在构造函数重载决议中,括号初始化尽最大可能与std::initializer_list参数匹配,即便其他构造函数看起来是更好的选择
|
||||
+ 对于数值类型的std::vector来说使用花括号初始化和小括号初始化会造成巨大的不同
|
||||
|
@ -138,7 +138,7 @@ auto result3 = lockAndCall(f3, f3m, nullptr); // 没问题
|
||||
另外,使用**nullptr**不会让你受到同重载决议特殊对待**0**和**NULL**一样的待遇。
|
||||
当你想用一个空指针,使用**nullptr**,不用**0**或者**NULL**。
|
||||
|
||||
记住
|
||||
**记住**
|
||||
+ 优先考虑nullptr而非0和NULL
|
||||
+ 避免重载指针和整型
|
||||
|
||||
|
@ -135,7 +135,7 @@ using add_lvalue_reference_t = typename add_lvalue_reference<T>::type;
|
||||
````
|
||||
看见了吧?不能再简单了。
|
||||
|
||||
记住
|
||||
**记住**
|
||||
+ typedef不支持模板化,但是别名声明支持。
|
||||
+ 别名模板避免了使用"<b>::type</b>"后缀,而且在模板中使用**typedef**还需要在前面加上**typename**
|
||||
+ C++14提供了C++11所有类型转换的别名声明版本
|
||||
|
@ -149,7 +149,7 @@ private:
|
||||
|
||||
`std::shared_ptr`不能处理的另一个东西是数组。和`std::unique_ptr`不同的是,`std::shared_ptr`的API设计之初就是针对单个对象的,没有办法`std::shared_ptr<T[]>`。一次又一次,“聪明”的程序员踌躇于是否该使用`std::shared_ptr<T>`指向数组,然后传入自定义数组销毁器。(即`delete []`)。这可以通过编译,但是是一个糟糕的注意。一方面,`std::shared_ptr`没有提供`operator[]`重载,所以数组索引操作需要借助怪异的指针算术。另一方面,`std::shared_ptr`支持转换为指向基类的指针,这对于单个对象来说有效,但是当用于数组类型时相当于在类型系统上开洞。(出于这个原因,`std::unique_ptr`禁止这种转换。)。更重要的是,C++11已经提供了很多内置数组的候选方案(比如`std::array`,`std::vector`,`std::string`)。声明一个指向傻瓜数组的智能指针几乎总是标识着糟糕的设计。
|
||||
|
||||
记住:
|
||||
**记住**:
|
||||
+ `std::shared_ptr`为任意共享所有权的资源一种自动垃圾回收的便捷方式。
|
||||
+ 较之于`std::unique_ptr`,`std::shared_ptr`对象通常大两倍,控制块会产生开销,需要原子引用计数修改操作。
|
||||
+ 默认资源销毁是通过**delete**,但是也支持自定义销毁器。销毁器的类型是什么对于`std::shared_ptr`的类型没有影响。
|
||||
|
@ -204,7 +204,7 @@ processWidget(std::move(spw), computePriority());
|
||||
```
|
||||
这很有趣,也值得了解,但通常是无关紧要的,因为您很少有理由不使用make函数。除非你有令人信服的理由这样做,否则你应该使用make函数。
|
||||
|
||||
记住:
|
||||
**记住**:
|
||||
- 和直接使用new相比,make函数消除了代码重复,提高了异常安全性。对于`std::make_shared`和`std::allocate_shared`,生成的代码更小更快。
|
||||
- 不适合使用make函数的情况包括需要指定自定义删除器和希望用大括号初始化
|
||||
- 对于`std::shared_ptr`s, make函数可能不被建议的其他情况包括
|
||||
|
@ -301,7 +301,7 @@ w1 = std::move(w2); //移动赋值w1
|
||||
|
||||
对于`pImpl`惯用法而言,在`std::unique_ptr`和`std::shared_ptr`的特性之间,没有一个比较好的折中。 因为对于类`Widget`以及`Widget::Impl`而言,他们是独享占有权关系,这让`std::unique_ptr`使用起来很合适。 然而,有必要知道,在其他情况中,当共享所有权(shared ownership)存在时,`std::shared_ptr`是很适用的选择的时候,没有必要使用`std::unique_ptr`所必需的**声明——定义**(function-definition)这样的麻烦事了。
|
||||
|
||||
记住
|
||||
**记住**
|
||||
|
||||
- `pImpl`惯用法通过减少在类实现和类使用者之间的编译依赖来减少编译时间。
|
||||
- 对于`std::unique_ptr`类型的`pImpl`指针,需要在头文件的类里声明特殊的成员函数,但是在实现文件里面来实现他们。即使是编译器自动生成的代码可以工作,也要这么做。
|
||||
|
@ -182,7 +182,7 @@ public:
|
||||
|
||||
更重要的是,`std::move`的使用代表着无条件向右值的转换,而使用`std::forward`只对绑定了右值的引用进行到右值转换。这是两种完全不同的动作。前者是典型地为了移动操作,而后者只是传递(亦作转发)一个对象到另外一个函数,保留它原有的左值属性或右值属性。因为这些动作实在是差异太大,所以我们拥有两个不同的函数(以及函数名)来区分这些动作。
|
||||
|
||||
记住:
|
||||
**记住**:
|
||||
|
||||
+ `std::move`执行到右值的无条件的转换,但就自身而言,它不移动任何东西。
|
||||
+ `std::forward`只有当它的参数被绑定到一个右值时,才将参数转换为右值。
|
||||
|
@ -149,7 +149,7 @@ void someFunc(MyTemplateType&& param);
|
||||
牢记整个本小节——通用引用的基础——是一个谎言,uhh,一个“抽象”。隐藏在其底下的真相被称为"**引用折叠(reference collapsing)**",小节Item 28致力于讨论它。但是这个真相并不降低该抽象的有用程度。区分右值引用和通用引用将会帮助你更准确地阅读代码("究竟我眼前的这个`T&&`是只绑定到右值还是可以绑定任意对象呢?"),并且,当你在和你的合作者交流时,它会帮助你避免歧义("在这里我在用一个通用引用,而非右值引用")。它也可以帮助你弄懂Item 25和26,它们依赖于右值引用和通用引用的区别。所以,拥抱这份抽象,陶醉于它吧。就像牛顿的力学定律(本质上不正确),比起爱因斯坦的相对论(这是真相)而言,往往更简单,更易用。所以这份通用引用的概念,相较于穷究引用折叠的细节而言,是更合意之选。
|
||||
|
||||
|
||||
记住:
|
||||
**记住**:
|
||||
|
||||
- 如果一个函数模板参数的类型为`T&&`,并且`T`需要被推导得知,或者如果一个对象被声明为`auto&&`,这个参数或者对象就是一个通用引用。
|
||||
- 如果类型声明的形式不是标准的`type&&`,或者如果类型推导没有发生,那么`type&&`代表一个右值引用。
|
||||
|
@ -140,7 +140,7 @@ auto func = std::bind(
|
||||
|
||||
但是,该条目解释的是在C++11中有些情况下`std::bind`可能有用,这就是其中一种。 (在C++14中,初始化捕获和自动参数等功能使得这些情况不再存在。)
|
||||
|
||||
要谨记的是:
|
||||
**要谨记的是**:
|
||||
|
||||
* 使用C ++14的初始化捕获将对象移动到闭包中。
|
||||
* 在C ++11中,通过手写类或`std::bind`的方式来模拟初始化捕获。
|
@ -95,7 +95,7 @@ auto f =
|
||||
};
|
||||
```
|
||||
|
||||
要谨记的是:
|
||||
**要谨记的是**:
|
||||
|
||||
* 对`auto&&`参数使用`decltype`来(`std::forward`)转发参数;
|
||||
|
||||
|
@ -270,7 +270,7 @@ auto boundPW = [pw](const auto& param) // C++14
|
||||
当然,这些是特殊情况,并且是暂时的特殊情况,因为支持C++14 lambda的编译器越来越普遍了。
|
||||
当`bind`在2005年被非正式地添加到C ++中时,与1998年的前身相比有了很大的改进。 在C ++11中增加了lambda支持,这使得`std::bind`几乎已经过时了,从C ++ 14开始,更是没有很好的用例了。
|
||||
|
||||
要谨记的是:
|
||||
**要谨记的是**:
|
||||
|
||||
* 与使用`std::bind`相比,Lambda更易读,更具表达力并且可能更高效。
|
||||
* 只有在C++11中,`std::bind`可能对实现移动捕获或使用模板化函数调用运算符来绑定对象时会很有用。
|
||||
|
Loading…
Reference in New Issue
Block a user