Effective Modern C++ 完全解读笔记汇总
Effective Modern C++ 学习和解读笔记汇总于此。
Chapter 1. Deducing Types
Item 1: Understand template type deduction.
- 在模板类型推导中,引用类型参数将被视为非引用类型处理,也就是说其引用性被忽略。
- 在万能引用参数类型推导时,左值参数被特殊处理。
- 值传递形参的类型推导时,其 const 和 volatile 被忽略。
- 在模板类型推导时,数组或者函数类型被转换为指针类型,除非它们用来初始化引用。
Item 2: Understand auto type deduction.
- auto 类型推导除了大括号初始化列表方式外,和模板类型推导方法一致。模板类型推导不支持 std::initializer_list。
- 函数返回值为 auto 时,实际是使用模板推导,不是 auto 类型推导。
- decltype几乎总是能够得出变量或者表达式的类型。
- 对于类型为 T 的左值表达式,而不是名字,decltype 基本上总是输出 T&。
- C++14支持 delctype(auto),像是 auto,能够自动从初始化列表中推断出类型,但它使用的是decltype 的推断规则。
Item 4: Know how to view deduced types.
- 通常可以使用 IDE 编辑器、编译器报错信息和 Boost TypeIndex 库来查看已推断的类型。
- 一些工具的结果可能没有帮助或者不准确,还是要理解透彻 C++ 的类型推断规则。
Chapter 2. auto
Item 5: Prefer auto to explicit type declarations.
- auto 变量必须初始化,不受类型不匹配导致移植和效率问题。
- auto 类型也受 Item2 和 Item6 中介绍的陷阱困扰。
Item 6: Use the explicitly typed initializer idiom when auto deduces undesired types.
- 隐式的代理类型可能导致 auto 类型推导结果不符合预期。
- 对于这种代理类型一般使用显式类型申明或者显示类型初始化。
Chapter 3. Moving to Modern C++
Item 7: Distinguish between () and {} when creating objects.
- 括号初始化是最广泛使用的初始化语法,它防止窄化转换,并且对于 C++ 最令人头疼的解析有天生的免疫性。
- 在构造函数重载决议中,括号初始化尽最大可能与 std::initializer_list 参数匹配,即便其他构造函数看起来是更好的选择。
- 对于数值类型的std::vector来说使用花括号初始化和圆括号初始化会产生巨大的不同。
- 在模板类选择使用小括号初始化或使用花括号初始化创建对象是一个挑战。
Item 8: Prefer nullptr to 0 and NULL.
- 相较于 0 和 NULL,优先使用 nullptr 。
- 避免对整数类型和指针类型的重载。
Item 9: Prefer alias declarations to typedefs.
- typedef 不支持模板化,但是别名声明支持。
- 模板别名没有 ::type 后缀,在模板中,typedef 还经常要求使用 typename 前缀。
- C++14 为 C++11 中的类型特征转换提供了模板别名。
Item 10: Prefer scoped enums to unscoped enums.
- C++98 风格的 enum 是 unscoped enum 。
- scoped enums 的枚举成员仅仅对枚举体内部可见。只能通过类型转换( cast )转换为其他类型。
- scopded enums 和 unscoped enum 都支持指定潜在类型。scoped enum 默认潜在类型是 int 。unscoped enum 没有默认的潜在类型。
- scoped enum 总是可以前置声明的。unscoped enum 只有当指定潜在类型时才可以前置声明。
Item 11: Prefer deleted functions to private undefined ones.
- 比起声明函数为 private 但不定义,使用 delete 函数更好。
- 任何函数都能 delete ,包括非成员函数和模板实例。
Item 12: Declare overriding functions override.
- 为重载函数加上override。
- 成员函数引用限定让我们可以区别对待左值对象和右值对象(即*this)。
Item 13: Prefer const_iterators to iterators.
- 优先考虑 const_iterator 而非 iterator。
- 在最大程度通用的代码中,优先考虑非成员函数版本的 begin、end、rbegin 等,而非同名成员函数。
Item14: Declare functions noexcept if they won‘t emit exception.
- noexcept 是函数接口的一部分,并且调用者可能会依赖这个接口。
- 相较于 non-noexcept 函数,noexcept 函数有被更好优化的机会。
- noexcept 对于 move 操作、swap、内存释放函数和析构函数是非常有价值的。
- 大部分函数是异常中立的而不是 noexcept。
Item 15: Use constexpr whenever possible.
- constexpr 对象是 const 的,给它初始化的值需要在编译时知道。
- 如果使用在编译时就知道的参数来调用 constexpr 函数,它就能产生编译时的结果。
- 相较于 non-constexpr 对象和函数,constexpr 对象很函数能被用在更广泛的上下文中。
- constexpr 是对象接口或函数接口的一部分。
Item 16: Make const member functions thread safe.
- 让 const 成员函数做到线程安全,除非你确保它们永远不会用在多线程的环境下。
- 相比于 std::mutex,使用 std::atomic 变量能提供更好的性能,但是它只适合处理单一的变量或内存单元。
- std::mutex 和 std::atomic 都是 move-only 的。
Item 17: Understand special member function generation.
- 特殊成员函数是那些编译器可能自己帮我们生成的函数:默认构造函数,析构函数,copy 操作,move 操作。
- 只有在类中没有显式声明的 move 操作,copy 操作和析构函数时,move 操作才被自动生成。
- 只有在类中没有显式声明的拷贝构造函数的时候,拷贝构造函数才被自动生成。只要存在 move 操作的声明,拷贝构造函数就会被删除(delete)。拷贝 operator= 和拷贝构造函数的情况类似。在有显式声明的 copy 操作或析构函数时,另一个 copy 操作能被生成,但是这种生成方法是被弃用的。
- 成员函数模板永远不会抑制特殊成员函数的生成。
Chapter 4. Smart Pointers
Item 18: Use std::unique_ptr for exclusive-ownership resource management.
- std::unique_ptr 是一个小的、快的、move-only 的智能指针,它能用来管理资源,并且独占资源的所有权。
- 默认情况下,std::unique_ptr 资源的销毁是用 delete 进行的,但也可以用户自定义 deleter。用带状态的 deleter 和函数指针作为 deleter 会增加 std::unique_ptr 对象的大小。
- 很容易将 std::unique_ptr 转换为 std::shared_ptr。
Item 19: Use std::shared_ptr for shared-ownership resource management.
- std::shared_ptr 为任意共享所有权的资源提供一种自动垃圾回收的便捷方式。
- 较之于 std::unique_ptr,std::shared_ptr 对象占用的内存通常大两倍,控制块会产生开销,需要原子引用计数修改操作。
- 默认资源销毁是通过 delete,但是也支持自定义 deleter。自定义 deleter 的类型对 std::shared_ptr 的类型没有影响。
- 避免从原始指针变量上创建 std::shared_ptr。
Item 20: Use std::weak_ptr for std::shared_ptr like pointers that can dangle.
- 对类似 std::shared_ptr 可能悬空的指针,使用 std::weak_ptr。
- std::weak_ptr 的潜在使用场景包括:caching、observer lists、避免 std::shared_ptr 的循环引用。
Item 21: Prefer std::make_unique and std::make_shared to direct use of new.
- 和直接使用 new 相比,make 函数消除了代码重复、提高了异常安全性。对于 std::make_shared和 std::allocate_shared,生成的代码更小更快。
- 不适合使用 make 函数的情况包括需要指定自定义删除器和希望用大括号初始化。
- 对于std::shared_ptrs, make函数可能不被建议的其他情况包括 (1)有自定义内存管理的类和 (2)特别关注内存的系统、非常大的对象,以及 std::weak_ptrs 比对应的 std::shared_ptrs 存在的时间更长。
Item 22: When using the Pimpl Idiom, define special member functions in the implementation file.
- pImpl 惯用法通过减少类实现和类使用者之间的编译依赖来减少编译时间。
- 对于std::unique_ptr 类型的 pImpl 指针,需要在头文件的类里声明特殊的成员函数,但是在实现文件里面来实现他们。即使是编译器自动生成的代码可以工作,也要这么做。
- 以上的建议只适用于 std::unique_ptr,不适用于 std::shared_ptr。
Chapter 5. Rvalue References, Move Semantics, and Perfect Forwarding
Item 23: Understand std::move and std::forward.
- std::move 无条件将输入转化为右值。它本身并不移动任何东西。
- std::forward 把其参数转换为右值,仅仅在参数被绑定到一个右值时。
- std::move 和 std::forward 只是做类型转换,在运行时(runtime)不做任何事。
Item 24: Distinguish universal references from rvalue references.
- 如果一个函数模板参数有 T&& 格式,并且发生类型推导,或者一个对象使用 auto&& 来声明,那么参数或对象就是一个万能引用。
- 如果类型推导的格式不是准确的 T&&(type&&),或者如果类型推导没有发生,T&&(type&&)就是一个右值引用。
- 如果用右值来初始化,万能引用相当于右值引用。如果用左值来初始化,则相当于左值引用。
Item 25: Use std::move on rvalue references, std::forward on universal references.
- 对右值引用使用 std::move,对通用引用使用 std::forward。
- 对按值返回的函数返回值,无论返回右值引用还是通用引用,执行相同的操作。
- 当局部变量就是返回值是,不要使用s td::move 或者 std::forward。
Item 26: Avoid overloading on universal references.
- 对万能引用参数的函数进行重载,调用机会将比你期望的多得多。
- 完美转发构造函数是糟糕的实现,因为对于 non-const 左值不会调用拷贝构造而是完美转发构造,而且会劫持派生类对于基类的拷贝和移动构造的调用。
Item 27: Familiarize yourself with alternatives to overloading on universal references.
- 万能引用和重载的组合替代方案包括使用不同的函数名、通过 const 左值引用传参、按值传递参数,使用 tag 分发。
- 通过 std::enable_if 约束模板来允许万能引用和重载组合使用,std::enable_if 可以控制编译器什么条件才使用万能引用的实例。
- 万能引用参数通常具有高效率的优势,但通常可用性较差。
Item 28: Understand reference collapsing.
- 引用折叠发生在四种情况:模板实例化,auto 类型的生成,创建和使用 typedef、别名声明和decltype。
- 当编译器生成了引用的引用时,通过引用折叠就是单个引用。其中之一为左值引用就是左值引用,否则就是右值引用。
- 在类型推导区分左值和右值以及引用折叠发生的上下文中,万能引用是右值引用。
Item 29: Assume that move operations are not present, not cheap, and not used.
- 假设移动操作不可用、不廉价。
- 在已知类型或支持移动语义的代码中,不需要进行此假设。
Item 30: Familiarize yourself with perfect forwarding failure cases.
- 当模板类型推导失败或者推导类型是错误的时候完美转发会失败。
- 导致完美转发失败的类型有花括号初始化、空指针的 0 或者 NULL、只声明的整型 static const 数据成、,模板和重载的函数名和位域。
Chapter 6. Lambda Expressions
Item 31: Avoid default capture modes.
- 默认的按引用捕获可能会导致引用悬挂。
- 默认的按值引用对于悬挂指针很敏感(尤其是this指针),并且它会误导人认为 lambda 是独立的。
Item 32: Use init capture to move objects into closures.
- C++14 使用初始化捕获模式实现移动捕获。
- C++11 使用 std::bind 间接实现移动捕获。
Item 33: Use decltype on auto&& parameters to std::forward them.
- 对 auto&& 参数使用 decltype来转发(std::forward)。
Item 34: Prefer lambdas to std::bind.
- 相较于 std::bind,lambda 代码可读性更强、更容易理解、性能可能更好。
- C++11 的 std::bind 在实现移动捕获、模板函数对象方面可以弥补 lambda 的不足。
Chapter 7. The Concurrency API
Item 35: Prefer task-based programming to thread-based.
- std::thread API 不能直接访问异步函数执行的结果,如果执行函数有异常抛出,代码终止执行。
- 基于线程的编程方式存在资源耗尽、认购超额、负载均衡的方案移植性不佳。
- 通过 std::async 的基于任务的编程方式会默认解决上面的问题。
Item 36: Specify std::launch::async if asynchronicity is essential.
- std::async 的默认启动策略允许是异和者同步。
- 灵活性导致访问 thread_locals 的不确定性,隐含了任务可能不会被执行的含义,会影响程序基于超时的 wait 调用。
- 只有确定是异步时才指定为 std::launch::async。
Item 37: Make std::threads unjoinable on all paths.
- 在所有路径上保证 thread 是 unjoinable 的。
- 析构时 join 会导致难以调试的性能异常问题。
- 析构时 detach 会导致难以调试的未定义行为。
- 在成员列表的最后声明 std::thread 类型成员。
Item 38: Be aware of varying thread handle destructor behavior.
- future 的正常析构行为只是销毁 future 本身的成员数据。
- 最后一个引用通过 std::async 创建的 non-deferred 任务的共享状态的 future 会阻塞到任务结束。
Item 39: Consider void futures for one-shot event communication.
- 对于简单的事件通信,基于条件变量的方法需要一个多余的互斥锁、对检测和反应任务的相对进度有约束,并且需要反应任务来确认事件是否已发生。
- 基于 flag 的方法可以避免的上一条的问题,但是不是真正的阻塞任务。
- 组合条件变量和 flag 使用,上面的问题都解决了,但是逻辑让人多少有点感觉有点生硬。
- 使用 std::promise 和 future 的方案可以避免这些问题,但为共享状态使用了堆内存,并且仅限于一次性通信。
Item 40: Use std::atomic for concurrency, volatile for special memory.
- std::atomic 用于不使用锁的多线程数据访问,用于编写并发程序。
- volatile 阻止内存的读写优化。用于特殊内存的场景。
Chapter 8. Tweaks
Item 41: Consider pass by value for copyable parameters that are cheap to move and always copied.
- 对于可复制、移动开销低、且无条件复制的参数,按值传递效率基本与按引用传递效率一致,而且易于实现,生成更少的目标代码。
- 通过构造函数拷贝参数可能比通过赋值拷贝开销大的多。
- 按值传递会引起切片问题,不适合基类类型的参数。
Item 42: Consider emplacement instead of insertion.
- 原则上,emplacement 函数会比传统插入函数更高效。
- 实际上,当执行如下操作时,emplacement 函数更快:(1)值被构造到容器中,而不是直接赋值;(2)传入参数的类型与容器类型不一致;(3)容器不拒绝已经存在的重复值。
- emplacement 函数可能执行类型转化,而传统插入函数会拒绝。