Effective C++ 汇总

2022-09-25
7 min read

Effective C++ / More Effective C++ / Effective STL / Effective Modern C++ / Exceptional C++ / More Exceptional C++ /

下面文章中使用 EC 表示 Effective C++,使用 MEC 表示 More Effective,同理 ES 和 EMC;使用 EPC 表示 Exceptional C++

基础

C++ 是一门复合型语言(EC1),并不是所有技巧都需要去掌握,有些工具是专门提供给库作者的。因为历史原因,#define 过去被使用的非常频繁,我们应该减少对 #define 的使用(EC2),其他技巧性建议如下所示

  1. 尽可能使用 const 关键字(EC3
  2. 了解 Rule of 5(EC5/EC6EMC17),细节可以参考 这里
  3. 多态基类的析构函数加上 virtual,注意这里的两个关键词:多态&基类EC7
  4. 析构函数默认是不抛异常的(noexcept,抛出异常会造成程序退出),所以不能让异常从析构函数中逃离(EC8
  5. 不要在构造与析构函数中调用 virtual 函数(EC9),因为你不能确定调用的是哪个函数
  6. 类声明(头文件)中定义的函数默认 inline(EC30),尽量不要把构造和析构设计成 inline。模板函数内联需要明确标识
  7. 尽量使用 C++ 风格的编码,例如尽量使用 C++ 风格的类型转换(MEC2),当然要尽量少做类型转换(EC27
  8. new/delete 的形式应该保持一致(EC16),否则系统行为不定
  9. 尽量使用 pass-by-ref 传递拷贝开销大的对象(EC20EMC41
  10. 不要忽略编译器的警告(EC53
  11. 其他

Modern CPP

这里 Modern CPP 表示 C++11 之后 C++ 引入的新特性

  1. 理解模板类型推导的过程(EMC1);理解 auto 类型推导过程(EMC2/5/6);理解 decltype(EMC3/4

  2. 构造对象时理解 {}() 的区别(EMC7

  3. 使用 alias 替换 typedefs(EMC9);在合适的地方优先使用 nullptr 替换 0 和 NULL(EMC8);使用新的范围枚举(EMC10

    template<typename T>
    using MyAllocList = std::list<T, MyAlloc<T>>;
    
  4. 熟悉并使用新的 C++ 关键字:override(EMC12)、delete/default(EMC11)、noexcept(EMC14)、constexpr(EMC15

  5. 理解智能指针的功能和用途,熟悉智能指针在 Pimpl 技法中的使用细节(EMC18~EMC22

  6. 理解右值引用、移动语义、通用引用和完美转发(EMC23~EMC30)。此类问题更多细节可以参考《C++ Move Semantics》

  7. 优先使用 emplace_back 方法,而不是插入(inseration)方法(EMC42

  8. 熟练掌握 lambda 表达式的使用(EMC31~EMC34),优先使用 lambda 而不是 bind 方法(EMC34

设计

  1. 令 operator = 返回 reference to *this,a = b = c (EC10)。拷贝对象的时候不能遗漏任何成员(EC12

  2. 使用证同测试处理自我赋值;更好的办法是使用异常安全代码:swap。未处理自我赋值可能造成资源意外释放等问题(EC11

  3. RAII 是 C++ 中的典型技法:使用对象管理资源(EC13);资源管理类应该向外提供原始资源的访问方式(EC15);为了安全,非资源管理类应该避免返回内部成分(EC28);因为 C++ 仅仅能删除被完全构造的对象,所以要保证构造函数不泄露资源(MEC10),子成员需要有自己的 RAII,例如使用智能指针管理资源

  4. 使用独立的语句将资源置入智能指针中,避免调用异常时造成资源泄漏(EC17

    //A的创建、fun的执行、智能指针的创建,随编译器实现而不同,一旦出现异常很可能出现资源泄漏
    process(std::shared_ptr<A>(new A()), fun()); // 错误用法
    // 下面的写法比较合适
    auto sptr = std::shared_ptr<A>(new A());
    process(sptr, fun());
    
  5. 适当的引入新类型可以减少不必要的错误,比如引入 Day/Month/Year 类,可以减少参数填写的错误(EC18

  6. 封装意味着减少信息的泄漏,尽量减少成员函数的个数(EC23)优先使用 free funcs 减少 frend funcs;也应该尽量将成员变量设置为 pirvate(EC22

  7. 尽量少做转型(EC27),默认转型会造成编译器额外的工作

  8. 保证异常安全(EC29):不泄漏资源 & 不破坏数据(all or nothing

  9. 减少编译依赖(EC31),最好实现头文件中“只有声明式”,比较常见的技法是 PimplLevelDB 中大量使用了 Pimpl 技法

    1. 使用 unique_ptr 实现 Pimpl 手法时需要自行实现类的析构函数,否则编译无法通过,细节可以参考这里

面向对象

  1. 面向行为继承EC32)。public 继承意味着 is-a,这里的 is 是面向行为(函数)的。企鹅是鸟但不能飞,所以企鹅不应该继承自鸟
  2. 避免遮掩继承而来的名字(EC33);绝不重写继承而来的非虚函数(EC36);不修改继承而来的虚函数默认参数(EC37);少用多重继承(EC40
  3. 区分接口继承与实现继承(EC34
  4. 非虚函数也可以实现多态(EC35),例如 NVI(non-virtual interface)functor/bind

异常与异常安全

C++ 中异常机制的简介可以参考这篇文章。根据 MEC10,C++ 仅仅能删除被完全构造的对象,所以类内子对象需要有自己的 RAII 机制,例如使用智能指针

异常对象也是对象且有自己的析构机制,所以捕获异常时最好使用引用和指针,避免异常对象的拷贝(MEC13)。C++11 以前,使用异常对比不使用异常的二进制文件大小与执行时间有 5%~10% 的损耗;一旦抛出异常,整个异常流可能比正常流慢三个数量级。C++ 11 后,随着技术的发展,异常对系统性能的影响逐渐降低,但不使用异常处理业务依旧时一条铁律——异常机制只能用于处理异常事件

审慎使用异常规格(exception specifications) (MEC14)。C++ 异常可以捕获 double、int 等非类异常对象,使用这类方法会加重编译器的负担。好的原则是只抛出继承标准异常类的那些异常对象

泛型与STL

用好 C++ 中的泛型和 STL 有一定的难度,不过一些高阶内容平时使用的不多,也没必要过度关注,例如模板元编程(EC48)、Type Trait(EC47)等,细节可以参考 EC41~EC48。一些典型的原则:尽量调用标准库中的函数(ES43)、尽量使用成员方法替换同名算法(ES44)。为了提升程序的性能与可维护性,可以考虑如下原则:

提升性能

  1. 合理选择容器(ES1)、判断容器为空时优先使用 empty 方法(ES4
  2. 尽量使用区间类成员方法(ES5),例如尽量使用 vector::assign(beg, end) 取代频繁的 push_back 或者 std::copy
  3. 使用 vector 尽量使用 reserve 预分配内存(ES14);为减少内存使用可以考虑使用 container::swap 方法修整过剩容量(ES17
  4. 考虑用有序 vector 代替关联容器(ES23),vector 占用更少的内存与内存页,内存效率更高
  5. 当给 map 添加新元素时,insert 比 operator[] 效率高;当更新已经在 map 里的元素值时 operator[] 更好(ES24
  6. 需要一个一个字符输入时考虑使用 istreambuf_iteratorES29
  7. 合理的选择排序算法(ES31):partition / nth_element / partial_sort / stable_partition / sort / stable_sort
  8. 合理选择 count、find、binary_search、lower_bound、upper_bound、equal_rangeES45
  9. 遵循按值传递的原则来设计 functor(ES38);考虑使用函数对象而不是函数作为 STL 算法的参数(ES46),functor 会做 inline 优化,效率提升比较明显。C++11 后使用 lambda 表达式更优

其他

  1. 合理的选择容器的删除方法(ES9),序列容器可使用 erase-remove、关联容器使用 erase
  2. 避免使用 vector<bool>ES18),因为它不是传统意义上的容器
  3. 避免原地修改 set 和 multiset 的键(ES22
  4. 通过 mismatch 或 lexicographical 比较实现简单的忽略大小写字符串比较(ES36
  5. 用 accumulate 或 for_each 来统计区间(ES37
  6. 仿函数相关内容请参考 ES38~ES42

关联容器

相等≠等价

find 算法和 set 的 insert 成员函数是很多必须判断两个值是否相同的函数的代表。但它们以不同的方式完成(ES19):

  1. find 对“相同”的定义是相等,基于operator==
  2. set::insert 对“相同”的定义是等价,通常基于 operator<

等价是基于在一个有序区间中对象值的相对位置。等价一般在每种标准关联容器(比如 set、multiset、map 和multimap)的排序顺序方面有意义。两个对象 x 和 y 如果在关联容器 c 的排序顺序中没有哪个排在另一个之前,那么它们关于 c 使用的排序顺序有等价的值。因为关联容器对等价有着严格的要求,所以永远让比较函数对相等的值返回 false(ES21)。这个特性也被称为严格弱序(反对称性、传递性、反自反性和等号的传递性),例如下面 pair 的比较方法:

p1.first<p2.first || (p1.first==p2.first && p1.second<p2.second);

其他

new&delete

new 和 delete 是 C++ 中比较复杂的模块,平常用的也不多,需要时请参考 EC49~EC52。new delete 有不同的类型,例如操作符、函数、placement 等,细节可以参考 MEC8

多线程

EMC 对 C++ 多线程的介绍不多,主要集中在 EMC35~EMC40,细节可以参考 《C++ Concurrency in Action》

  1. 优先使用基于任务的编程模式(EMC35),例如 std::async;std::launch::async 标记在需要的时候主动标记,否则系统不一定会并发执行(EMC36
  2. 了解 atomic & volatile(EMC10

标准库&Boost

尽量了解标准库和 Boost 中的细节