《Effective Modern C++》- 42条改善C++11/14使用的指南

基本信息

  • 作者: Scott Meyers
  • 原版: Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14
  • 页数: 334页
  • 出版时间: 2014年11月

核心概述

《Effective Modern C++》是Scott Meyers”Effective C++“系列的最新作品,专注于C++11和C++14新特性的正确使用。全书42条指南分为8章,覆盖了从类型推导到并发编程的完整现代C++开发流程。


第1章:类型推导 (Deducing Types)

Item 1: 理解模板类型推导

  • 模板类型推导遵循严格规则:
    • 实参类型中的引用和const被忽略
    • 指针和引用实参传递给引用形参时有特殊规则
    • 数组和函数名会退化为指针
  • 关键区别:auto类型推导与模板类型推导不同

Item 2: 理解auto类型推导

  • auto类型推导时,引用和const被保留
  • 花括号初始化对auto有特殊限制:不能用花括号初始化auto变量(除非是列表初始化)
  • auto推导遵循”去引用”规则

Item 3: 理解decltype

  • decltype保留类型的cv限定符和引用
  • decltype(var)返回变量的实际类型(含引用)
  • decltype((var))返回引用类型(因为(var)是表达式)
  • 与auto配合使用时,auto&&是最安全的通用引用形式

Item 4: 了解如何查看推导的类型

  • 使用编译错误信息查看类型推导结果
  • 工具如typeid().name()(但输出难以解读)
  • 推荐:故意触发类型不匹配的错误来查看推导结果

第2章:auto

Item 5: 优先使用auto而非显式类型声明

  • 减少重复代码,避免类型声明错误
  • 提高可读性(特别是复杂类型)
  • 确保迭代器遍历的正确性
  • 注意事项:避免隐式类型转换

Item 6: 当auto推导出不想要的类型时使用显式初始化器习语

  • 语法:auto varName {expr}; 或 auto varName = Type{expr};
  • 强制类型转换,防止隐式 narrowing conversion
  • 推荐用于确保类型正确性

第3章:迈向现代C++

Item 7: 区分()和{}

  • 花括号初始化(统一初始化)是最安全的初始化方式
  • 防止 narrowing conversion
  • 可以初始化所有类型,包括STL容器
  • 构造函数调用时行为不同,需注意

Item 8: 优先使用nullptr而非0和NULL

  • nullptr是nullptr_t类型,不会隐式转换为整数
  • 解决重载歧义问题
  • 更清晰的语义表达

Item 9: 优先使用别名声明而非typedef

  • using别名更直观,支持模板别名
  • 模板别名可以简化复杂类型的声明
  • 推荐使用:using newName = oldType;

Item 10: 优先使用作用域枚举而非非作用域枚举

  • 作用域枚举(enum class)更安全
  • 不隐式转换为整数
  • 枚举值需要作用域限定访问
  • 可以指定底层类型

Item 11: 优先使用删除的函数而非私有未定义的函数

  • = delete明确禁止函数调用
  • 编译时错误,而非链接时错误
  • 可以用于禁用拷贝构造/赋值等操作

Item 12: 声明覆盖函数时使用override

  • override明确标识虚函数覆盖
  • 编译时检查是否正确覆盖
  • 提高代码可读性

Item 13: 优先使用const_iterator而非iterator

  • const迭代器更安全
  • 现代C++中,auto配合范围for循环是最简洁的方式
  • 避免不必要的修改

Item 14: 如果函数不会抛出异常则声明noexcept

  • noexcept提供性能优化机会
  • 允许STL容器在移动操作时使用更高效算法
  • 声明为noexcept的函数可以被移动到异常安全的容器中

Item 15: 尽可能使用constexpr

  • constexpr变量在编译时求值
  • 可以用于模板参数、数组大小等
  • constexpr函数在输入为常量时可以编译时求值

Item 16: 使const成员函数线程安全

  • mutable成员可以在const成员函数中修改
  • 常用于缓存、引用计数等场景
  • 线程安全需注意同步机制

Item 17: 理解特殊成员函数的生成

  • 编译器生成的特殊成员函数行为
  • 移动操作默认删除的情况
  • 自定义析构函数、拷贝操作会影响移动操作

第4章:智能指针

Item 18: 对独占所有权资源管理使用std::unique_ptr

  • unique_ptr是首选的独占所有权智能指针
  • 不可拷贝,可移动
  • 可以自定义删除器
  • 空指针开销为零

Item 19: 对共享所有权资源管理使用std::shared_ptr

  • shared_ptr通过引用计数实现共享所有权
  • 拷贝时引用计数增加
  • 线程安全的引用计数操作
  • 注意循环引用问题

Item 20: 对可能悬空引用的std::shared_ptr类指针使用std::weak_ptr

  • weak_ptr不增加引用计数
  • 用于打破循环引用
  • 使用时需要升级为shared_ptr
  • 检查是否过期:expired()

Item 21: 优先使用std::make_unique和std::make_shared

  • 异常安全:避免内存泄漏
  • 代码更简洁
  • 性能更好(make_shared可以一次性分配)
  • C++14才有make_unique,C++11需要自己实现

Item 22: 使用Pimpl习语时,在实现文件中定义特殊成员函数

  • Pimpl(Pointer to Implementation)习语隐藏实现细节
  • 特殊成员函数需要在实现文件中定义
  • 避免在头文件中包含完整类型

第5章:右值引用、移动语义和完美转发

Item 23: 理解std::move和std::forward

  • std::move:无条件转换为右值引用
  • std::forward:条件转发,保留值类别
  • std::move不仅仅是移动,它是强制转换
  • 正确使用两者是关键

Item 24: 区分通用引用和右值引用

  • 通用引用:T&&在模板推导中
  • 右值引用:类型确定时的Type&&
  • 通用引用可以绑定到左值和右值
  • 判断方法:是否有模板类型推导

Item 25: 对右值引用使用std::move,对通用引用使用std::forward

  • 右值引用已知是右值,用std::move
  • 通用引用可能是左值或右值,用std::forward
  • 这是标准做法

Item 26: 避免对通用引用进行重载

  • 通用引用重载可能导致意外匹配
  • 调用者可能获得意想不到的行为
  • 考虑使用SFINAE或其他技术

Item 27: 熟悉替代对通用引用重载的方案

  • 使用重载函数模板
  • 使用SFINAE(std::enable_if)
  • 使用概念(C++20)

Item 28: 理解引用折叠

  • T& & → T&
  • T& && → T&
  • T&& & → T&
  • T&& && → T&&
  • 引用折叠只发生在模板推导中

Item 29: 假设移动操作不存在、不廉价且未被使用

  • 移动操作可能被删除
  • 移动操作可能不便宜
  • 移动操作可能不被调用
  • 设计类时应考虑这些情况

Item 30: 熟悉完美转发的失败情况

  • 数组类型无法转发
  • 函数类型无法转发
  • cv限定符丢失
  • 引用丢失

第6章:Lambda表达式

Item 31: 避免默认捕获模式

  • [=]和[&]可能捕获不必要的内容
  • 明确指定捕获列表更安全
  • 避免隐式依赖外部变量

Item 32: 使用init capture将对象移动到闭包中

  • C++14引入的泛型lambda捕获
  • 可以移动对象进入闭包
  • 语法:[var = std::move(obj)]

Item 33: 对auto&&参数使用decltype以std::forward它们

  • 通用lambda参数需要完美转发
  • 使用decltype(param)获取原始类型
  • 配合std::forward使用

Item 34: 优先使用lambda而非std::bind

  • lambda更直观、更易读
  • lambda支持泛型(C++14)
  • std::bind的行为有时令人困惑

第7章:并发API

Item 35: 优先使用基于任务的编程而非基于线程的编程

  • std::async和std::future简化并发
  • 任务可以自动分配线程
  • 更高层次的抽象

Item 36: 如果需要异步性则指定std::launch::async

  • 默认行为可能延迟执行
  • 指定std::launch::async确保异步执行
  • 避免意外的同步行为

Item 37: 确保std::threads在所有路径上都是unjoinable的

  • 线程析构前必须join或detach
  • 使用RAII包装器
  • 避免线程泄漏

Item 38: 注意不同的线程句柄析构行为

  • std::thread析构时会调用std::terminate如果未join/detach
  • 使用自定义删除器的智能指针更安全
  • 考虑使用线程池

Item 39: 考虑void futures为一一次性事件通信

  • std::promise和std::future可以用于事件通知
  • 不传输数据,只 signaling
  • 简单的线程同步机制

Item 40: 对并发性使用std::atomic,对特殊内存使用volatile

  • std::atomic提供原子操作和内存序
  • volatile不保证原子性,只防止优化
  • 并发编程应使用std::atomic

第8章:技巧

Item 41: 对于可复制但移动廉价且总是被复制的参数考虑按值传递

  • 按值传递结合std::move内部使用
  • 统一拷贝和移动语义
  • 代码更简洁

Item 42: 考虑使用emplace而非insert

  • emplace直接在容器内构造元素
  • 避免临时对象的创建和移动
  • 性能更好

与其他知识的关联


核心要点总结

  1. 类型推导是关键:理解auto、decltype、模板推导的差异
  2. 智能指针优先:unique_ptr和shared_ptr是首选,避免裸指针
  3. 移动语义深入理解:区分通用引用和右值引用,正确使用move和forward
  4. 并发编程新范式:任务优先于线程,原子操作优先于volatile
  5. Lambda是利器:替代bind,注意捕获行为

本书是C++11/14开发的必备参考,建议配合《C++ Concurrency in Action》-中文版一起阅读,以获得完整的现代C++知识体系。