C++学习:Effective Modern C++条款

Posted chaos-god

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++学习:Effective Modern C++条款相关的知识,希望对你有一定的参考价值。

条款1:理解模板类型推导

  • 推导模版类型时,引用的值视为非引用,即忽略引用。
  • 推导通用引用类型参数时,左值特殊处理。
  • 推导传值参数时,忽略const和volatile。
    +推导模版类型时,参数是数组或函数名则退化为指针,除非用来初始化引用。

条款2:理解auto类型推导

  • 推导auto类型一般和推导模版类型是一致的,但auto类型推导对于大括号初始化{}会推导为std::initializer_list,模版类型推导则不会。
  • 做为函数返回类型或lambda参数中的auto类型推导意味着模版类型推导,而不做为auto类型推导。

条款3:理解decltype.

  • decltype几乎总是推导出变量或表达式的类型,不做任何修改。
  • 类型为T的左值表达式而不是名称,decltype总是推导成引用T&。
  • C++14支持decltype(auto),和auto一样,通过初始值推导,但使用decltype规则。

条款4:知道如何查看推导出来的类型.

  • 推导出来的类型通常可以使用IDE编辑器,编译错误信息或Boost TypeIndex库查看到。
  • 有些工具查看到的结果可能没有任何帮助或者就是错误的,所以理解类型推导规则还是有用的。

条款5:优先使用auto,而不是显式的类型声明

  • auto变量必须初始化,基本上不会有类型不匹配导致的可移值问题或效率问题的影响,也可以简化重构过程,一般也比显式指定类型需要更少的键盘输入。
  • auto类型的变量请查看条款2和条款6中描述的陷阱。

条款6:当auto推导出的类型不是想要的类型时,使用显示类型初始化的惯用语法。

  • “不可见的”代理类型会导致auto对初始化表达式推导出“错误的”类型。
  • 显示类型初始化的惯用语法迫使auto推导出想要的类型。

条款7:创建对象时区分()和{}。

  • 花括号初始化方法是最为广泛使用的初始化语法,可以防止类型转换变窄,也不受C++烦人的解析问题影响。
  • 构造函数的重载解析中,花括号初始化方法只要可能就会匹配std::initializer_list参数,即使有其它的构造函数提供了看上去更好的匹配参数。
  • std::vector<数字类型>在使用圆括号和花括号初始化时意义完全不同。
  • 在模板中的对象创建时选择圆括号还是花括号是一个挑战。

条款8:优先使用nullptr,而不是0和NULL。

  • 优先使用nullptr,而不是0和NULL。
  • 避免整形或指针类型的重载。

条款9:优先使用alias声明,而不是typedef。

  • typedefs不支持模板化,alias声明支持。
  • alias模板避免了“::type”后缀,以及在模板中的用于typedefs的“typename”前缀。
  • 所有C++11中类型traits转换,C++14都提供了alias模板。

条款10:优先使用范围枚举,而不是非范围枚举。

  • C++98类型的枚举就是非范围枚举。
  • 范围枚举的枚举器只在枚举内部可见,只能通过cast转换为其它类型。
  • 范围枚举和非范围枚举都支持指定底层类型。范围枚举的默认底层类型是int, 而非范围没有默认底层类型。
  • 范围枚举总是可以前向声明,而非范围枚举只有指定底层类型时才可以前向声明。

条款11:优先使用deleted函数,而不是私有未定义函数。

  • 优先使用deleted函数,而不是私有未定义函数。
  • 任何函数都可以deleted, 包括非成员函数和模板实例函数。

条款12:把重写函数声明为override

  • 把重写函数声明为override。
  • 成员函数引用限定符可以区别对待左值和右值对象。

条款13:优先使用const_iterator, 而不是其它的iterator。

  • 优先使用const_iterator, 而不是其它的iterator。
  • 大多数泛型代码中,优先使用非成员函数版本的begin, end, rbegin等等,而不是相应的成员函数。

条款14:如果函数不抛出异常则声明为noexcept

  • noexcept是函数接口的一部分,意味着调用者必须依赖它。
  • noexcept函数更容易优化。
  • noexcept在移动操作,交换,内存释放函数,析构函数中特别有价值。
  • 大多数函数是异常中立的,而不是noexcept。

条款15:只要有可能就使用constexpr。

  • constexpr对象是常量并且是在编译期初始化的。
  • constexpr函数在给定的参数是编译期已知的情况下可以生成编译期结果。
  • constexpr对象和函数比起非constexpr对象和函数,可以使用在更宽泛的上下文范围。
  • constexpr是对象接口或函数接口的一部分。

条款16:使const成员成为线程安全的。

  • 除非确定不会在并发环境中使用,否则const成员一定要是线程安全的。
  • std::atomic变量比mutex性能更好,但只能用于操作单一变量或单一内存位置。

条款17:理解特殊成员函数的生成。

  • 特殊成员函数就是编译器自动生成的:默认构造函数,析构函数,拷贝操作,移动操作。
  • 移动操作函数只有在没有显式声明移动操作,拷贝操作,析构函数时才会生成。
  • 拷贝构造函数只有在没有显式声明拷贝构造函数时才会生成,而且如果声明了移动操作就会被删除。拷贝赋值操作只有在没有显式声明拷贝赋值操作符才会生成,而且声明了移动操作就会删除。如果显式声明了析构函数,则拷贝操作就会过时。
  • 成员函数模板永远不会阻止特殊成员函数的生成。

条款18:使用std::unique_ptr管理独占资源。

  • std::unique_ptr小巧,快速,只能移动,用于管理独占资源。
  • 默认情况下资源析构使用delete, 但可以指定自定义的删除器。有状态的删除器或函数指针会增加std::unique_ptr对象的大小。
  • std::unique_ptr很容易转换为std::shared_ptr。

条款19: 使用std::shared_ptr管理共享资源。

  • std::shared_ptrs对于随意的资源的共享生命周期管理提供方便的垃圾回收处理方法。
  • 相对于std::unique_ptr来说,std::shared_ptr对象通常大一倍,主要是控制块,原子引用计数操作导致。
  • 默认资源的析构是通过delete, 但也支持自定义删除器。删除器的类型对std::shared_ptr的类型不起作用。
  • 避免从原始指针类型的变量生成std::shared_ptrs。

条款20:使用std::weak_ptr代替可能会发生悬空指针的std::shared_ptr。

  • 使用std::weak_ptr代替可能会发生悬空指针的std::shared_ptr。
  • 潜在的使用std::weak_ptr的情景有缓存,观察者列表,避免std::shared_ptr循环引用。

条款21:优先使用std::make_unique和std::make_shared,而不是直接使用new。

  • 相对于直接使用new来说,make函数消除了源代码重复,提升了异常安全,而且std::make_shared和std::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::move和std::forward。

  • std::move无条件转换为右值,就其本身而言,它不移动任何东西。
  • std::forward只有在绑定的参数是右值时才会将参数转换为右值。
  • std::move和std::forward在运行时不做任何事情。

条款24:区分通用引用和右值信引用。

  • 如果函数模板参数有类型T&&并且需要推导T,或者对象声明为auto&&, 则参数或对象是通用引用。
  • 如果类型声明的格式不是精确的type&&, 或不需要类型推导,type&&就是右值引用。
  • 通用引用如果使用右值初始化的话,则和右值引用是一致的。如果是用左值初始化的话,则与左值引用是一致的。

条款25:对右值引用使用std::move, 对通用引用使用std::forward。

  • 最后一次使用时,对右值引用使用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的函数有时应该比插入函数效率更高,而且效率永远不会降低。
  • 在实践中,以下情形(emplace的函数)可能更快:(1)要添加的值是构造到容器中时,而不是赋值到容器中(2)传递的参数类型与容器中的类型不同(3)容器不会排斥要添加的值,因为它是重复的值。
  • Emplace的函数有可能执行类型转换,而insert的函数会排斥。

以上是关于C++学习:Effective Modern C++条款的主要内容,如果未能解决你的问题,请参考以下文章

C++学习:Effective Modern C++条款

《Effective Modern C++》读书笔记

Effective Modern C++ 条款28 理解引用折叠

我的C/C++语言学习进阶之旅收集关于MODERN C++ 11/14/17/20/23 的一些资料

我的C/C++语言学习进阶之旅收集关于MODERN C++ 11/14/17/20/23 的一些资料

Effective C++学习笔记