Non-Profit, International

Spirit unsterblich.

C++ 代码编写建议

字数统计:2459 blog

  这是一份 C++ 的代码编写建议书,本建议书类似于 Google C++ Style Guide,但更倾向于推广现代 C++。不同于 C++ Core Guidelines,本建议更为细致。本建议书具有强烈的个人倾向,也会参考其他建议。

  目前处于初期阶段。

  1. 避免使用具有静态生命周期的可变量,例如命名空间内变量,非常量静态成员,非常量函数内静态变量

    理由:

    1. 静态生命周期变量的生命周期不可控
    2. 静态声明周期变量打破了函数的局部性,使得函数的运行结果依赖于函数参数之外的对象,更容易产生 bug 以及更难以调试
  2. 避免虚假的数组声明,使用指针,例如 void func(int *c) 而不是 void func(int c[])

    理由:

    1. 使用这种虚假的数组声明往往会带来一种误解,使得数组长度被忽视,并且曲解了 c 的真正类型
  3. 避免在正式交付的软件中做不必要的边界检查用于 debug,例如 std::vector<T, U>::at,应使用提前检查,例如判断 size,或在正式发布时禁用检查,例如使用 assert

    理由:

    1. 完全可以使用提前检查 size 来实现相同效果
    2. 抛出该类型异常会增加软件复杂度
    3. at 不是原子的,不具有线程安全性
  4. 避免使用 define 宏定义,使用 constexpr 表示常量

    理由:

    1. 宏没有作用域约束
    2. 宏展开的结果无法预测
    3. 宏没有类型
  5. 类成员函数仅在类内声明而不实现

    理由:

    1. 存在循环依赖的情况下不得不声明和实现分类
    2. 类声明的行数过长不利于阅读
    3. 友元函数通常需要在类外声明(除了隐藏友元)
  6. 使用 const 变量传递参数

    理由:

    1. 对象状态清晰,方便维护
    2. 避免误操作导致产生野指针或者悬垂引用
  7. 使用 inline 修饰静态成员变量(C++ 17)

    理由:

    1. 直观,减少冗余
    2. 不违反 odr
  8. 使用 using 和函数类型构造函数指针

    理由:

    1. 清晰

    例如:

    
     using f_type = void(int, int);
     using f_pointer = f_type*;
     void f_impl(int a, int b);
     f_pointer a = f_impl;
    
    
  9. 避免内存别名使用

    理由:

    1. 别名使用通常存在 bug,错误的使用会导致错误的结果
    2. 别名使用影响编译器优化

    例如:

    
     template<typename T>
     void accumulate(T* source, size_t len, T* target) {
         for (size_t i = 0; i < len; ++i) {
             *target += source[i];
         }
     }
    
     template<typename T>
     void accumulate1(T* source, size_t len, T* target) {
         T acc = *target;
         for (size_t i = 0; i < len; ++i) {
             acc += source[i];
         }
         *target = acc;
     }
    
    

    注意这两个 accumulate 函数的区别:accumulate 每次做 += 操作都会通过 target 指针进行间接访问,造成效率降低,由于 source 和 target 的类型都是 T*,所以编译器不能贸然将 accumulate 优化为 accumulate1,因为两个函数语义上不等价:当 target 是 source 的一个元素,两个函数的结果不一样

    这种现象叫做内存别名使用:编译器无法判断指针是否重叠。一个好的方案是引入一个中间变量,这种方式首先能避免别名使用,还能提高可读性:字符数少的代码有可能性能差又难读

  10. 使用引用限定修饰赋值意义的运算符重载函数,限制 *this 的值类别(参考C++ 非静态成员函数的引用限定修饰

    理由:

    1. 对一个右值进行赋值通常是无意义的
    2. 允许对一个右值进行赋值可能会使形如 if(foo() = 8) 的语句合法
  11. 避免抛出含有状态的异常,使用无状态异常(参考C++ 异常问题的简单分析

    理由:

    1. 带状态异常会存在堆内存分配,存在逻辑矛盾
    2. 带状态异常性能差
  12. 使用 std::string_view 代替字符串常量(C++ 17)

    理由:

    1. std::string_view 编译器计算长度,避免不必要的 strlen
    2. std::string_view 自带长度,方便使用和传递
  13. 使用 std::string_view 和 std::span 作为数组视图(C++ 17)

    理由:

    1. 避免不必要的堆内存分配
    2. 自带长度方便使用和传递
  14. 使用到 std::string_view 的推导指引代替 char*(C++ 17)

    理由:

    1. 编译期即可计算长度,避免不必要的 strlen
    2. 方便使用和传递
  15. 同类型比较使用成员函数,非同类比较使用友元函数

    理由:

    1. 同类比较实现为成员可以防止函数扩散到其他作用域
    2. 非同类比较实现为友元便于维持对称性
    3. 非同类比较实现为友元可以避免循环引用
  16. 使用三路比较代替谓词比较(C++ 20)

    理由:

    1. 三路比较能提高有序容器的性能
    2. 三路比较能简化比较函数实现
    3. 三路比较具有对称性
  17. 使用无符号数(例如 size_t)代替有符号数

    理由:

    1. 有符号数溢出通常未定义
    2. 无符号数能表示更大范围
  18. 使用 std::unique_ptr 代替多次 new

    理由:

    1. 保证动态内存分配的异常安全(参考C++ 异常安全 - 智能指针
    2. 可以使用移动概念转移所有权
  19. 使用 const_cast,static_cast 和 reinterpret_cast 代替 C 风格的转换

    理由:

    1. static_cast 可以检查溢出和非常规转换
    2. 由于错误转换而存在 bug 时利于发现
  20. 使用枚举类代替枚举

    理由:

    1. 枚举类的成员不会泄漏
  21. 使用匿名命名空间代替 static

    理由:

    1. 更易于维护
  22. 使用 inline 函数和 inline 变量代替 extern(C++ 17)

    理由:

    1. 防止定义冲突
    2. 便于维护
    3. 不违反 odr
  23. 使用 copy-and-swap idiom 实现移动构造和移动赋值

    理由:

    1. 异常安全
    2. 方便编写
  24. 默认构造,移动构造,移动赋值,析构和无异常函数使用 noexcept 修饰

    理由:

    1. 默认构造应该只进行 0 初始化,不进行资源分配(参考C++ 异常 - 资源管理),不会产生异常
    2. 移动构造和移动赋值应该使用 copy-and-swap idiom,天生异常安全
    3. 析构函数的异常无法正确处理(参考C++ 异常 - 类和异常
  25. 避免使用 using 指令

    理由:

    1. using 指令只影响查找规则,不引入名字到当前作用域,所以可能会发生遮蔽,造成错误的函数调用(参考C++ 命名空间和 using
  26. 避免使用无限定调用,例如避免依赖 ADL 或者 ADL 两步法,改为使用限定调用

    理由:

    1. 无限定调用会增加查找范围,增加编译时间
    2. 无限定调用会启动 ADL,依赖 ADL 不利于维护,如果 ADL 查找到的函数在将来被错误修改,会使用错误的函数
    3. ADL 两步法会造成代码无法维护,在使用 ADL 两步法后,不能贸然删除 using std::swap; 以及将无限定调用改为限定调用
    4. 无限定调用有可能会因为函数遮蔽问题找不到正确实现,或者使用错误实现
  27. 优先支持 STL 工具,而不是对 STL 进行重载或特化,例如增加成员函数 swap 以及实现移动以支持 std::swap

    理由:

    1. 给 STL 工具添加特化不利于维护
    2. 对 STL 函数增加重载通常是错误的
  28. 使用 global namespace 内的名字时加上 global 限定

    理由:

    1. 消除 std 和 global 同名名字的歧义
    2. 限定查找缩小了查找范围,加快编译速度

若无特殊声明,本文以 CC BY-SA 3.0 许可协议 提供。