《Effective Modern C++》概览

第一章 类型推导

1. 理解模板类型推导

  • 在模板类型推导时,有引用的实参会被视为无引用,他们的引用会被忽略
  • 对于通用引用的推导,左值实参会被特殊对待
  • 对于传值类型推导,const和/或volatile实参会被认为是non-const的和non-volatile
  • 在模板类型推导时,数组名或者函数名实参会退化为指针,除非它们被用于初始化引用

2. **理解auto类型推导

  • auto类型推导通常和模板类型推导相同,但是auto类型推导假定花括号初始化代表std::initializer_list,而模板类型推导不这样做
  • 在C++14中auto允许出现在函数返回值或者lambda函数形参中,但是它的工作机制是模板类型推导那一套方案,而不是auto类型推导

3. **理解decltype

  • decltype总是不加修改的产生变量或者表达式的类型。
  • 对于T类型的不是单纯的变量名的左值表达式,decltype总是产出T的引用即T&
  • C++14支持decltype(auto),就像auto一样,推导出类型,但是它使用decltype的规则进行推导。

4. 学会查看类型推导结果

  • 类型推断可以从IDE看出,从编译器报错看出,从Boost TypeIndex库的使用看出
  • 这些工具可能既不准确也无帮助,所以理解C++类型推导规则才是最重要的

第二章 Auto

5. **优先考虑auto而非显式类型声明

  • auto变量必须初始化,通常它可以避免一些移植性和效率性的问题,也使得重构更方便,还能让你少打几个字。
  • 正如Item26讨论的,auto类型的变量可能会踩到一些陷阱。

6. **auto推导若非己愿,使用显式类型初始化惯用法

  • 不可见的代理类可能会使auto从表达式中推导出“错误的”类型
  • 显式类型初始器惯用法强制auto推导出你想要的结果

第三章 移步现代C++

7. **区别使用(){}创建对象

  • 括号初始化是最广泛使用的初始化语法,它防止变窄转换,并且对于C++最令人头疼的解析有天生的免疫性
  • 在构造函数重载决议中,括号初始化尽最大可能与std::initializer_list参数匹配,即便其他构造函数看起来是更好的选择
  • 对于数值类型的std::vector来说使用花括号初始化和小括号初始化会造成巨大的不同
  • 在模板类选择使用小括号初始化或使用花括号初始化创建对象是一个挑战。

8. **优先考虑nullptr而非0NULL

  • 优先考虑nullptr而非0NULL
  • 避免重载指针和整型

9. 优先考虑别名声明而非 typedefs

  • typedef不支持模板化,但是别名声明支持(using ...)。
  • 别名模板避免了使用“::type”后缀,而且在模板中使用typedef还需要在前面加上typename
  • C++14提供了C++11所有type traits转换的别名声明版本

10. **优先考虑限域enum而非未限域enum

  • C++98的enum即非限域enum
  • 限域enum的枚举名仅在enum内可见。要转换为其它类型只能使用cast
  • 非限域/限域enum都支持底层类型说明语法,限域enum底层类型默认是int。非限域enum没有默认底层类型。
  • 限域enum总是可以前置声明。非限域enum仅当指定它们的底层类型时才能前置。

11. 优先考虑使用deleted函数而非使用未定义的私有声明

  • 比起声明函数为private但不定义,使用deleted函数更好
  • 任何函数都能被删除(be deleted),包括非成员函数和模板实例(译注:实例化的函数)

12. **使用override声明重写函数

  • 为重写函数加上override
  • 成员函数引用限定让我们可以区别对待左值对象和右值对象(即this)

13. **优先考虑const_iterator而非iterator

  • 优先考虑const_iterator而非iterator
  • 在最大程度通用的代码中,优先考虑非成员函数版本的beginendrbegin等,而非同名成员函数

14. **如果函数不抛出异常请使用noexcept

  • noexcept是函数接口的一部分,这意味着调用者可能会依赖它
  • noexcept函数较之于non-noexcept函数更容易优化
  • noexcept对于移动语义,swap,内存释放函数和析构函数非常有用
  • 大多数函数是异常中立的(译注:可能抛也可能不抛异常)而不是noexcept

15. **尽可能的使用constexpr

  • constexpr对象是const,它被在编译期可知的值初始化
  • 当传递编译期可知的值时,constexpr函数可以产出编译期可知的结果
  • constexpr对象和函数可以使用的范围比non-constexpr对象和函数要大
  • constexpr是对象和函数接口的一部分

16. **const成员函数线程安全

  • 确保const成员函数线程安全,除非你确定它们永远不会在并发上下文(concurrent context)中使用。
  • 使用std::atomic变量可能比互斥量提供更好的性能,但是它只适合操作单个变量或内存位置。

17. 理解特殊成员函数的生成

  • 特殊成员函数是编译器可能自动生成的函数:默认构造函数,析构函数,拷贝操作,移动操作。
  • 移动操作仅当类没有显式声明移动操作,拷贝操作,析构函数时才自动生成。
  • 拷贝构造函数仅当类没有显式声明拷贝构造函数时才自动生成,并且如果用户声明了移动操作,拷贝构造就是delete。拷贝赋值运算符仅当类没有显式声明拷贝赋值运算符时才自动生成,并且如果用户声明了移动操作,拷贝赋值运算符就是delete。当用户声明了析构函数,拷贝操作的自动生成已被废弃。
  • 成员函数模板不抑制特殊成员函数的生成。

18. **对于独占资源使用std::unique_ptr

  • std::unique_ptr是轻量级、快速的、只可移动(move-only)的管理专有所有权语义资源的智能指针
  • 默认情况,资源销毁通过delete实现,但是支持自定义删除器。有状态的删除器和函数指针会增加std::unique_ptr对象的大小
  • std::unique_ptr转化为std::shared_ptr非常简单

19. **对于共享资源使用std::shared_ptr

  • std::shared_ptr为有共享所有权的任意资源提供一种自动垃圾回收的便捷方式。
  • 较之于std::unique_ptrstd::shared_ptr对象通常大两倍,控制块会产生开销,需要原子性的引用计数修改操作。
  • 默认资源销毁是通过delete,但是也支持自定义删除器。删除器的类型是什么对于std::shared_ptr的类型没有影响。
  • 避免从原始指针变量上创建std::shared_ptr

20. **std::shard_ptr可能悬空时使用std::weak_ptr

  • std::weak_ptr替代可能会悬空的std::shared_ptr
  • std::weak_ptr的潜在使用场景包括:缓存、观察者列表、打破std::shared_ptr环状结构。

21. **优先考虑使用std::make_uniquestd::make_shared而非new

  • 和直接使用new相比,make函数消除了代码重复,提高了异常安全性。对于std::make_sharedstd::allocate_shared,生成的代码更小更快。
  • 不适合使用make函数的情况包括需要指定自定义删除器和希望用花括号初始化。
  • 对于std::shared_ptrs,其他不建议使用make函数的情况包括(1)有自定义内存管理的类;(2)特别关注内存的系统,非常大的对象,以及std::weak_ptrs比对应的std::shared_ptrs活得更久。

22. 当使用Pimpl惯用法,请在实现文件中定义特殊成员函数

  • Pimpl惯用法通过减少在类实现和类使用者之间的编译依赖来减少编译时间。
  • 对于std::unique_ptr类型的pImpl指针,需要在头文件的类里声明特殊的成员函数,但是在实现文件里面来实现他们。即使是编译器自动生成的代码可以工作,也要这么做。
  • 以上的建议只适用于std::unique_ptr,不适用于std::shared_ptr

23. **理解std::movestd::forward

  • std::move执行到右值的无条件的转换,但就自身而言,它不移动任何东西。
  • std::forward只有当它的参数被绑定到一个右值时,才将参数转换为右值。
  • std::movestd::forward在运行期什么也不做。

24. 区分通用引用与右值引用

  • 如果一个函数模板形参的类型为T&&,并且T需要被推导得知,或者如果一个对象被声明为auto&&,这个形参或者对象就是一个通用引用。
  • 如果类型声明的形式不是标准的type&&,或者如果类型推导没有发生,那么type&&代表一个右值引用。
  • 通用引用,如果它被右值初始化,就会对应地成为右值引用;如果它被左值初始化,就会成为左值引用。

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

  • 最后一次使用时,在右值引用上使用std::move,在通用引用上使用std::forward
  • 对按值返回的函数要返回的右值引用和通用引用,执行相同的操作。
  • 如果局部对象可以被返回值优化消除,就绝不使用std::move或者std::forward

26. 避免在通用引用上重载

  • 对通用引用形参的函数进行重载,通用引用函数的调用机会几乎总会比你期望的多得多。
  • 完美转发构造函数是糟糕的实现,因为对于non-const左值,它们比拷贝构造函数而更匹配,而且会劫持派生类对于基类的拷贝和移动构造函数的调用。

27. 熟悉通用引用重载的替代方法

  • 通用引用和重载的组合替代方案包括使用不同的函数名,通过lvalue-reference-to-const传递形参,按值传递形参,使用tag dispatch
  • 通过std::enable_if约束模板,允许组合通用引用和重载使用,但它也控制了编译器在哪种条件下才使用通用引用重载。
  • 通用引用参数通常具有高效率的优势,但是可用性就值得斟酌。

28. 理解引用折叠

  • 引用折叠发生在四种情况下:模板实例化,auto类型推导,typedef与别名声明的创建和使用,decltype
  • 当编译器在引用折叠环境中生成了引用的引用时,结果就是单个引用。有左值引用折叠结果就是左值引用,否则就是右值引用。
  • 通用引用就是在特定上下文的右值引用,上下文是通过类型推导区分左值还是右值,并且发生引用折叠的那些地方。

29. 假定移动操作不存在,成本高,未被使用

  • 假定移动操作不存在,成本高,未被使用。
  • 在已知的类型或者支持移动语义的代码中,就不需要上面的假设。

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

在大多数情况下,完美转发工作的很好。你基本不用考虑其他问题。但是当其不工作时——当看起来合理的代码无法编译,或者更糟的是,虽能编译但无法按照预期运行时——了解完美转发的缺陷就很重要了。同样重要的是如何解决它们。在大多数情况下,都很简单。

  • 当模板类型推导失败或者推导出错误类型,完美转发会失败。
  • 导致完美转发失败的实参种类有花括号初始化,作为空指针的0或者NULL,仅有声明的整型static const数据成员,模板和重载函数的名字,位域。

第四章 ***lambda*表达式

31. 避免使用默认捕获模式

  • 默认的按引用捕获可能会导致悬空引用。
  • 默认的按值捕获对于悬空指针很敏感(尤其是this指针),并且它会误导人产生lambda是独立的想法。

32. 使用初始化捕获来移动对象到闭包中

  • 使用C++14的初始化捕获将对象移动到闭包中。
  • 在C++11中,通过手写类或std::bind的方式来模拟初始化捕获。

33. **auto&&形参使用decltypestd::forward它们

  • auto&&形参使用decltypestd::forward它们。

34. 考虑lambda而非std::bind

  • 与使用std::bind相比,lambda更易读,更具表达力并且可能更高效。
  • 只有在C++11中,std::bind可能对实现移动捕获或绑定带有模板化函数调用运算符的对象时会很有用。

第五章 并发API

35. 优先考虑基于任务的编程而非基于线程的编程

  • std::thread API不能直接访问异步执行的结果,如果执行函数有异常抛出,代码会终止执行。
  • 基于线程的编程方式需要手动的线程耗尽、资源超额、负责均衡、平台适配性管理。
  • 通过带有默认启动策略的std::async进行基于任务的编程方式会解决大部分问题。

36. **如果有异步的必要请指定std::launch::async

  • std::async的默认启动策略是异步和同步执行兼有的。
  • 这个灵活性导致访问thread_locals的不确定性,隐含了任务可能不会被执行的意思,会影响调用基于超时的wait的程序逻辑。
  • 如果异步执行任务非常关键,则指定std::launch::async

37. **使std::thread在所有路径最后都不可结合

  • 在所有路径上保证thread最终是不可结合的。
  • 析构时join会导致难以调试的表现异常问题。
  • 析构时detach会导致难以调试的未定义行为。
  • 声明类数据成员时,最后声明std::thread对象。

38. 关注不同线程句柄的析构行为

  • future的正常析构行为就是销毁future本身的数据成员。
  • 引用了共享状态——使用std::async启动的未延迟任务建立的那个——的最后一个future的析构函数会阻塞住,直到任务完成。

39. 对于一次性事件通信考虑使用voidfutures

  • 对于简单的事件通信,基于条件变量的设计需要一个多余的互斥锁,对检测和反应任务的相对进度有约束,并且需要反应任务来验证事件是否已发生。
  • 基于flag的设计避免的上一条的问题,但是是基于轮询,而不是阻塞。
  • 条件变量和flag可以组合使用,但是产生的通信机制很不自然。
  • 使用std::promisefuture的方案避开了这些问题,但是这个方法使用了堆内存存储共享状态,同时有只能使用一次通信的限制。

40. **对于并发使用std::atomic,对于特殊内存使用volatile

  • std::atomic用于在不使用互斥锁情况下,来使变量被多个线程访问的情况。是用来编写并发程序的一个工具。
  • volatile用在读取和写入不应被优化掉的内存上。是用来处理特殊内存的一个工具。

第六章 微调

41. 对于移动成本低且总是被拷贝的可拷贝形参,考虑按值传递

  • 对于可拷贝,移动开销低,而且无条件被拷贝的形参,按值传递效率基本与按引用传递效率一致,而且易于实现,还生成更少的目标代码。
  • 通过构造拷贝形参可能比通过赋值拷贝形参开销大的多。
  • 按值传递会引起切片问题,所说不适合基类形参类型。

42. 考虑使用置入代替插入

  • 原则上,置入函数有时会比插入函数高效,并且不会更差。
  • 实际上,当以下条件满足时,置入函数更快:(1)值被构造到容器中,而不是直接赋值;(2)传入的类型与容器的元素类型不一致;(3)容器不拒绝已经存在的重复值。
  • 置入函数可能执行插入函数拒绝的类型转换。

闲聊

囫囵吞枣式的看完了Modern C++,前面看的比较仔细,对应类型推导、智能指针、右值引用、通用引用以及一些好记的技巧等有了认识,对于Lambda、并发Api等有了点印象。
清楚了自己的不足,开发时不能停留在表明,每一步都有可能是坑。对自己不足的地方,后面会再系统的学习一次。并总结输出。特别是一些常用的知识。

posted @ 2022-04-18 00:14  吹不散的流云  阅读(387)  评论(0编辑  收藏  举报