《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直接在容器内构造元素- 避免临时对象的创建和移动
- 性能更好
与其他知识的关联
- 《Effective C++》-3版:本书的前作,涵盖C++98/03的最佳实践
- 《Effective STL》-中文版:STL使用的50条指南
- 《C++ Concurrency in Action》-中文版:C++并发编程的权威著作
- 《A Tour of C++, 2nd Edition》:C++17全面速览
- 《The C++ Programming Language, 4th Edition》:C++语言权威参考
核心要点总结
- 类型推导是关键:理解auto、decltype、模板推导的差异
- 智能指针优先:
unique_ptr和shared_ptr是首选,避免裸指针 - 移动语义深入理解:区分通用引用和右值引用,正确使用move和forward
- 并发编程新范式:任务优先于线程,原子操作优先于volatile
- Lambda是利器:替代bind,注意捕获行为
本书是C++11/14开发的必备参考,建议配合《C++ Concurrency in Action》-中文版一起阅读,以获得完整的现代C++知识体系。