《Effective Modern C++》
22:如果使用Pimpl惯用法,则要在实现文件中定义特殊成员函数
- Pimpl惯用法通过减少类的实现和类的客户端的编译依赖关系缩减了编译时间。
- 如果std::unique_ptr用于pImpl指针,则在类的头文件中声明特殊成员函数,在实现文件中实现。即使默认的函数实现可以接收的话也可以这么做。
- 以上建议适用于std::unique_ptr,但不适用于是std::shared_ptr。
23:理解std::move和std::forword
- std::move无条件转换为右值,就是本省而言,它不移动任何东西。
- std::forward只有在绑定的参数是右值时才会将参数转换为右值。
- std::move和std::forward在运行时不做任何事情。
24:区分通用引用和右值引用
- 如果函数模板参数有类信托T&&并且需要推导T,或者对象声明为auto&&,则参数或对象是通用引用。
- 如果类型声明的格式不是精确的type&&,或不需要类型推导,type&&就是右值引用。
- 通用引用如果使用右值初始化的话,则和右值引用是一致的。如果是用左值初始化的话,则与左值引用是一致的。
25:对右值引用使用std::move,对通用引用使用std::froward.
- 最后一次使用时,对右值引用使用std:::move,对通用引用使用std::forward。
- 返回值是传值时,同上面一样。
- 如果本地对象有可能做返回值优化的话,永远也不要对本地对象使用std::move或std::forward。
26:避免对通用引用进行重载
- 重载通用引用几乎总是超预期地频繁调用了通用引用的重载。
- 完美转发构造函数特别有问题,因为比non-const左值的拷贝构造函数有更好的匹配,这样就会派生类调用基类的拷贝构造函数和移动构造函数。
27:熟悉重载通用引用函数的其他方法
- 通用引用和重载的组合替代方法有使用不同的函数名,通过常量的左值引用传递参数,通过值传递参数,以及使用标记调度。
- 通过std::enable_if约束模板可以允许通用引用和重载一起使用,但会控制编译器使用通用引用重载的条件。
- 通用引用参数经常会有提升效率的优点,但是通常也会有使用上的缺点。
28:理解引用折叠
- 引用折叠发生在四种情况:模板实例化、自动类型生成、typedef和alias声明的创建和使用,和decltype
- 当编译器在引用折叠环境中生成引用的引用时,结果就会成为单引用。如果原始的引用有一个是左值引用,则结果就是左值引用,否则就是右值引用。
- 通用引用是右值引用的情况有,类型推导可以区分左值和右值时,以及发生引用折叠。
29:假定移动操作存在,不是廉价的,也不是可用的
- 假定移动操作存在,不是廉价的,也不是可用的。
- 已知类型或支持移动语义类型的代码中,不需要有待定。
30:熟悉完美转发失败案例
- 如果模板类型推导失败或推导出错误的类型时,完美转发就会失败。
- 导致完美转发失败的参数类型有括号初始化器,使用0或NULL的指针,整型变量静态数据成员的声明,模板和重载函数名称,位成员。
31:避免默认的捕捉模式
- 默认的传引用操作捕捉会导致悬空引用。
- 默认的传值操作捕捉容易受悬空指针影响,并且会误导成lambdas是自包含的。
32:使用init捕捉来移动对象到闭包
- 使用C++14的init捕捉来移动对象到闭包。
- C++11中,通过手写的类或std::bind来模仿init捕捉。
33:使用decltype调用std::forward移动auto&&参数
- 使用decltype调用std::forward移动auto&&参数。
34:优先使用lambdas,而不是std::bind
- Lambdas更容易阅读,更快捷,并且比std::bind更高效。
- 只有在 C++11中,std::bind在实现移动捕捉或绑定对象到模板化的函数调用操作符上可能会有用。
35:优先使用基于task的编程方法,而不是基于thread(相关的类)
- std::thread API从异步运行函数中得到非直接的结果,如果函数抛出异常,则程序终止。
- 基于Thread的编程方法调用需要手工管理线程耗尽、过度订阅、负载均衡,以及适应新平台等问题。
- 基于Task的编程方法通过std::async使用默认加载策略来处理大多数的问题。
36:如果需要异步处理,请指定std::launch::async
- std::async的默认加载策略既允许异步执行,也允许同步的执行。
- 灵活性也会导致访问thread_local时产生不确定性,线程也许永远不会执行,也会影响基于超时等待调用的程序逻辑。
- 如果需要异步处理,请指定std::launch::async。
37:使std::threads在任何路径下都是不能join的
- 使std::threads在任何路径下都是不能join的。
- 在析构函数上join会导致难以调试的性能问题。
- 在析构函数上detach会导致难以调试的未定义行为。
- 将std::thread对象作为数据成员列表项中的最后一个。
38:要小心不同的线程句柄析构行为
- Future的析构函数通常只会销毁future的数据成员。
- 涉及到非延期task的共享状态的final future是通过std::async,在task完成后加载的。
39:考虑对于一次性事件通信中使用void future
- 对于简单的事件通信,基于condvar的设计需要一个多余的互斥量,对检测和响应任务的相应进度施加限制,并且需要任务验证事件是否已发生。
- 设计使用一个标记来避免这些问题,但这是基于轮询的,而不是基于阻塞的。
- condvar和标记可以一起使用,但结果通信机制会有点不自然。
- 使用std::promises和futures来回避这些问题,但是这种方法使用堆内存来处理共享状态,并且只能用于一次性通信。
40:使用std::atomic处理并发,使用volatile处理特殊内存
- std::atomic用来在不使用mutex的多线程情形下访问数据。它是编写并发软件的一个工具。
- volatile用来在读写操作不是优化的方式下处理内存。它是编写处理特殊内存的一个工具。
41:考虑使用传值来处理移动操作很廉价而且总是被拷贝的参数
- 对于能够拷贝,移动操作很廉价而且总是被拷贝的参数,传值跟传引用基本上同样高效,而且更容易实现,产生更少的代码。
- 通过构造函数拷贝参数可能比通过复制操作符拷贝参数更加高昂。
- 传值操作会有切片问题,所以一般对于基类参数类型不适合。
42:考虑使用emplace的函数,而不是insert函数
- 原则上,emplace的函数有时应该比插入函数效率更高,而且效率永远不会降低。
- 在实践中,以下情形可能更快:(1)要添加的值是构造到容器中时,而不是赋值到容器中(2)传递的参数类型与容器中的类型不同(3)容器不会排斥要添加的值,因为它是重要的值。
- Emplace的函数有可能执行类型转换,而insert的函数会排斥。