代码规范

一些个人习惯而罢了。

头文件

  • 分四部分依次排列
    1. cpp 文件对应的头文件
    2. 解决方案内源码可见的头文件
    3. 第三方库的头文件
    4. 标准库头文件
  • 第三方库用 <> 自己的头文件用 “”
  • 按字典序排序
  • 清理不必要的头文件
  • C++ 标准库头文件不带扩展名,C 头文件带 .h,C++ 程序中尽量用 C++ 头文件

命名

  • 函数大写驼峰
  • 变量小写驼峰
  • 指针和引用右对齐
  • 成员变量 - m_
  • 指针 - p_
  • local 静态变量 - s_
  • 静态成员变量不加前缀,因为 T::abc 的用法足以说明该变量的类型
  • 编译期常量 - 大写驼峰,或者全大写,下划线连接
  • 缩写 - 全大写
  • 累计值 - count
  • 字节数 - size
  • 字符串长度 - length

注释

  • 单行注释
1
2
// ...
int a; // ...
  • 多行注释
1
2
3
4
5
/*
* ...
* ...
*/
int a;
  • // TODO: Do something
  • 首字母大写
  • 除非是长篇的描述,否则不加句号
  • 使用 does not 而非 doesn't
  • 公式之类非英文的部分用 `` 括起来
  • 对函数、类型的引用以 # 开头
  • 命名空间右大括号后跟注释
  • 较长的 #else#endif 后跟注释
  • 单行死代码用 //,多行用 #if 0
  • 复数:内容物是否XXX,并且相互独立
    • Utils/, Defines.h
  • 单数:内容物是否属于XXX的一部分,并且相互耦合
    • Renderer/, Hash.hpp

类型

  • 可以使用 intchar
  • 使用 int16_t 而非 short,使用 uint64_t 而非 unsigned long long
  • 使用 truefalse 而非 01 来表示 bool
  • 涉及位运算时使用无符号整数
  • 避免对无符号整数进行可能令其下溢的计算
  • 与第三方库交互时优先遵循该库的参数 / 返回类型

  • 考虑到内存对齐,成员变量应按大小降序排序,不需要严格遵守,重点是不要让类的中间出现填充
  • 按成员变量的声明顺序排列初始化列表,尽量不要用一个成员变量初始化另一个成员变量
  • 成员函数的默认值写在构造函数初始化列表里,没有构造函数则写在声明处
  • final
  • 成员以 static, public, protected, private 的顺序排列
  • 三五法则
  • 基类析构函数必须为虚函数
  • 重写基类虚函数必须加 override

大括号

  • 不要省略单行代码的大括号
  • switch 每个 case 都加大括号,大括号内 break / return

其他

  • 4 空格缩进

  • 命名空间内容不缩进

  • template<typename> 为非 template<class>

  • 前向声明

  • 如何传参

    • 对于 sizeof(T) <= 8 的栈类型
      • 直接 copy
    • 对于 sizeof(T) > 8,的栈类型
      • const &T,禁止对其移动
    • 对于会产生动态内存分配的类型
      • 提供普通参数,内部必须 move,由函数调用者决定 copy 进函数还是 move 进函数
      • 禁止用 std::string_view 参数构造 std::string
      • 禁止用 std::span / std::initializer_list 参数构造 std::vector
    • 如果参数在函数内不存在直接的构造 / 赋值行为
      • const &T
      • 很多时候我们只需要 void * 和 size,优先用 std::span
      • 如果只需要一个 char *,优先用 const char *
      • 如果同时需要 char * 和 size,优先用 std::string_view
    • C-style string, std::stringstd::string_view 会让情况变得复杂
      • copy 一个 char * 比 copy 一个 std::string_view 快一丢丢,但是缺失了长度信息
      • 比较糟糕的情况就是 std::string.data() 传给 char * 参数,然后在内部以某种方式重新计算长度,产生的开销可能得不偿失
        • 更糟糕的是我们不一定能简单地判断这件事,举个例子,C++20 MSVC STL:
        • std::fstream 就算用 std::string 构造也只会将参数的 .data() 转发给接受 char * 的重载
        • std::filesystem::pathchar * 构造则会在内部用 for 循环计算长度并构造 std::string_view
        • 换个编译器实现说不定就不一样了,换个 C++ 版本说不定也不一样了
      • Anyway:
        • if 调用处一定只存在 char *
          • const char *,真要计算长度早晚逃不掉,不如少 copy 点东西
        • else
          • if 函数内不需要长度
            • const char *
          • else
            • std::string_view
        • 当然也可以拉几把倒全用 std::string_view,扣这么细很麻烦而且不一定有意义
        • STL 只要提供一堆重载就行,我们调库的要考虑的可就多了(
  • 尽量用 assert 代替 if-log-return,C++ 最好的错误检查就是崩在一个合理的地方

    • 用户错误则用 if-log-return 没有问题
  • std::tuple 和结构化绑定代替一些过分临时的结构体

  • 怎么处理编译期可确定的映射关系

    • constexpr array(键为或者能够转换为非重复的整数下标)
    • constexpr / consteval 函数 / Lambda 里的 switch-case
    1
    2
    3
    4
    auto Lambda = []() constexpr
    {
    //...
    };