Effeciteve Modern C++笔记

简单记录下阅读《Effective Modern C++》的一些笔记,列出本书的一些提纲,但不给出解释,希望后续复习时直接看提纲就能回忆起相关内容。

条款1:理解模板型别推导

针对以下代码,分为三种情况讨论。

1
2
3
4
template<typename T>
void f(ParamType param);

f(expr);
  1. ParamType是个指针或引用,但不是个万能引用;
  2. ParamType是个万能引用,又分为实参是左值还是右值;
  3. ParamType不是指针也不是引用,又会分为实参具有引用或者指针特性或者什么都没有;

除此之外还有一些参数会退化成指针的情况出现,比如数组实参、函数实参。

条款2:理解auto型别推导

对于条款1中的代码,auto=T,变量的型别修饰则是ParamType,其余规则除了一种特殊情况外则和条款1一致。

特殊情况:当用于auto声明变量的初始化表达式是用大括号括起来的时候,推导的型别就属于std::initializer_list

根本原因在于,auto会假定用大括号括起来的初始化表达式代表一个std::initializer_list,但模板型别推导却不会。

注意:在C++14中auto可能用于函数返回值或lambda表达式的形参声明中,在这两种情况下auto进行的模板型别推导而不是正常的auto型别推导。

条款3:理解decltype

C++11中,decltype的主要作用大概就在于声明那些返回值型别依赖于形参型别的函数模板。

绝大多数情况下,decltype会得出变量或表达式的型别而不作任何修改。

C++14会有些特殊。

条款4:掌握查看型别推导结果的方法

IDE、编译器等工具会有帮助,但理解条款1~3更重要。

条款5:优先选用auto,而非显式型别声明

条款6:当auto推导的型别不符合要求时,使用带显式型别的初始化物习惯用法

隐形代理类和auto无法和平共处。

解决方案不应该放弃auto,而是强制进行另一次性别转换,即带显式型别的初始化物习惯用法。

还有一个好处是有时这样做可以强调程序员意在创建一个型别有异于初始化表达式型别的变量的场合。

条款7:在创建对象时注意区分{}和()

大括号初始化可以应用的语境最为宽泛,可以阻止隐式窄化型别转换,还对最令人苦恼之解析与法免疫。

在构造函数重载决议期间,只要有任何可能,大括号初始化物就会与带有std::initializer_list型别的形参相匹配,即使其他重载版本更合适。

条款8:优先选用nullptr,而非0或NULL

可以避免在整型和指针型别之间重载。

条款9:优先选用别名声明,而非typedef

typedef不支持模板化,但别名声明支持。

别名模板可以让人免写“::type”后缀,并且在模板内,对于内嵌typedef的引用经常要加typename前缀。

条款10:优先选用限定作用域的枚举型别,而非不限作用域的枚举型别

enum class来定义枚举类,即使限定作用域的枚举型别。

条款11:优先选用删除函数,而非private未定义函数

任何函数都可以删除,包括非成员函数和模板具现。

条款12:为意在改写的函数添加override声明

条款13:优先选用const_iterator,而非iterator

条款14:只要函数不会发射异常,就为其加上noexcept声明

条款15:只要有可能使用constexpr,就使用它

constexpr对象都具备const属性,并由编译期已知的值完成初始化。

constexpr函数在调用时若传入的实参值是编译期已知的,则会产出编译期结果。

条款16:保证const成员函数的线程安全性

利用std::mutexstd::atomic

条款17:理解特种成员函数的生成机制

特种成员函数是指C++会自行生成的成员函数:默认构造函数、析构函数、复制操作以及移动操作。

自行生成的移动操作仅当类中未包含用户显式声明的复制操作、移动操作和析构函数时才会生成。

条款18:使用std::unique_ptr管理具备专属所有权的资源

std::unique_ptr是小巧、高速的、具备只移型别的智能指针,对托管资源实施专属所有权语义。

条款19:使用std::shared_ptr管理具备共享所有权的资源

std::shared_ptr是提供方便的手段,实现了任意资源在共享所有权语义下进行生命周期管理的垃圾回收。

条款20:对于类似std::shared_ptr但有可能空悬的指针使用std::weak_ptr

条款21:优先选用std::make_unique和std::make_shared,而非直接使用new

相比于直接私用new表达式,make系列函数消除了重复代码、改进了异常安全性,并提升性能。

条款22:使用Pimpl习惯用法时,将特殊成员函数的定义放到实现文件中

条款23:理解std::move和std::forward

std::move只做一件事,就是把实参强制转换成右值,并不做移动。

std::forward是有条件的强制型别转换:仅当其实参是使用右值完成初始化时,它才会执行向右值型别的强制型别转换。

在运行期,std::movestd::forward都不会做任何操作。

条款24:区分万能引用和右值引用

如果函数模板形参具备T&&型别,并且T的型别系推导而来,或如果对象使用auto&&声明其型别,则该形参或对象就是个万能引用。

条款25:针对右值引用实施std::move,针对万能引用实施std::forward

编译器若要在一个按值返回的函数里省略对局部对象的复制(或者移动),则需要满足两个前提条件:1局部对象型别和函数返回值型别相同;2返回的就是局部对象本身。

若局部对象可能适用于返回值优化,则请勿针对其实施std::move或std::forward。

条款26:避免依万能指针型别进行重载

条款27:熟悉依万能引用型别进行重载的替代方案

SFINAE:substitution failure is not an error,替换失败不是错误。利用std::enable_if

万能引用形参通常在性能方面具备优势,但在易用性方面一般会有劣势。

条款28:理解引用折叠

引用折叠:如果任一引用为左值引用,则结果就是左值引用,否则结果是右值引用。

引用折叠会在四中语境中发生:模板实例化、auto型别生成、创建和运用typedef和别名声明,以及decltype。

条款29:假定移动操作不存在、成本高、未使用

对于那些型别或对于移动语义的支持情况已知的代码,则无需做以上假定。

条款30:熟悉完美转发的失败情形

论及一般意义上的转发时,都是在处理形参为引用型别的情形。

  1. 大括号初始化物
  2. 0和NULL用作空指针
  3. 仅有生命的整型static const成员变量
  4. 重载的函数名字和模板名字
  5. 位域:非const引用不得绑定到位域。

条款31:避免默认捕获模式

按引用的默认捕获会导致空悬指针问题。

按值的默认捕获极易受空悬指针影响,并会误导人们认为lambda式是自恰的。

条款32:使用初始化捕获将对象移入闭包

初始化捕获又称广义lambda捕获。

使用C++14的初始化捕获将对象移入闭包。

在C++11中,经由手工实现的类或std::bind去模拟初始化捕获。

条款33:对auto&&型别的形参使用decltype,以std::forward之

条款34:优先使用lambda式,而非std::bind

C++14已经不需要用std::bind了

C++11中,std::bind仅在实现移动捕获、或是绑定到具备模板化的函数调用运算符的对象的场合中可能尚有余热可以发挥。

条款35:优先选用基于任务而非基于线程的程序设计

std::thread 基于线程

std::async 基于任务

条款36:如果异步是必要的,则指定std::launch::async

std::async的默认启动策略既允许任务以异步方式执行,也允许任务以同步方式执行。

正因为这种弹性,所以当以默认启动策略使用std::async的时候要考虑一些不确定性。

条款37:使std::thread型别对象在所有路径皆不可联结

在成员列表的最后声明std::thread型别对象。

条款38:对变化多端的线程句柄析构函数行为保持关注

期值的析构函数在常规情况下,仅仅会析构期值的成员变量。

指涉到经由std::aysnc启动的未推迟任务的共享状态的最后一个期值会保持阻塞,直至该任务结束。

条款39:考虑针对一次性事件通信使用以void为模板型别实参的期值

条款40:对并发使用std::atomic,对特种内存使用volatile

注:一般地,编译器可以将不相关的赋值语句重新排序。

volatile的用处是告诉编译器,正在处理的内存是特种内存,不具备常规行为,即读写操作不可以被优化。

最常见的特种内存就是用于内存映射I/O的内存。

条款41:针对可复制的形参,在移动成本低并且一定会被复制的前提下,考虑将其按值传递

经由构造复制形参的成本可能比经由赋值复制形参高出很多。

按值传递肯定会导致切片问题,所以基类型别特别不适用于按值传递。

条款42:考虑置入而非插入

从原理上说,置入函数(emplace)应该有时比对应的插入函数(insert、push)高效,而且不应该有更低效的可能。

但从实践上说,置入函数在以下几个前提成立时,极有可能会运行的更快:

  1. 待添加的值是以构造而非赋值方式加入容器;
  2. 传递的实参型别与容器持有之物的型别不同;
  3. 容器不会由于存在重复值而拒绝待添加的值。

置入函数可能会执行在插入函数中会被拒绝的型别转换,如隐式型别转换构造函数。

置入函数使用的是直接初始化,而插入函数使用的复制初始化。

【END】