开发者概览

C++ 代码风格

qianmoQqianmoQ· 更新于 2026-10-02· 阅读 8 分钟· 0 次阅读

登录后可跨设备保存划线和私人笔记登录

这是 Gluten 的一套 C++ 核心准则。其目的是通过提倡一致性和遵循最佳实践,使代码库更简洁、更高效、更易于维护。

哲学理念

哲学层面的准则通常难以量化衡量,但它们非常有价值。针对 Gluten 的 C++ 编码,有以下几条哲学准则。

  • 使用 ISO 标准 C++ 编写代码。
  • 优先使用标准 API,C++ 编程 API 优先于系统调用。
  • 保持代码风格一致,有助于理解与维护。
  • 保持简单,使代码清晰易读。
  • 为阅读者而非编写者优化代码,因为人们阅读代码的时间远多于编写代码的时间。
  • 先让它跑起来,再让它变得更好或更快。
  • 尽量不引入任何复杂性,基于最小共识进行协作。

代码格式化

C++ 编码风格的许多方面都将由 clang-format 覆盖,例如空格、行宽、缩进以及排序(包括 include、using 指令等)。

  • 始终确保你的代码与 Velox 后端所使用的 clang-format-15 兼容。
  • dev/formatcppcode.sh 已提供用于格式化 Velox C++ 代码。

要格式化 CMake 文件(如 CMakeLists.txt 和 *.cmake),需要安装 cmake-format。以下是一个示例。

apt install python3-pip -y
pip3 install --user cmake-format
cmake-format --first-comment-is-literal True --in-place cpp/velox/CMakeLists.txt

命名规范

  • 类型(class、struct、enum、类型别名、类型模板参数)和文件名使用 PascalCase。
  • 函数、成员变量、局部变量和非类型模板参数使用 camelCase。
  • 私有和保护成员变量使用 camelCase_。
  • 命名空间名称和构建目标使用 snake_case。
  • 宏使用 UPPER_SNAKE_CASE。
  • 静态常量和枚举值使用 kPascalCase。

设计原则

  • 不做过度设计。
  • 避免双重否定,isValid 优于 isNotInvalid。
  • 避免边界情况,优先处理常见情况。
  • 直接表达意图,不要让人去猜。
  • 在模块间的接口处对参数进行检查,内部实现中不要做检查,私有实现中应使用 assert,而不是编写过多的安全性检查。
  • 所有头文件必须使用 #pragma once 包含单一包含保护(include guard)

  • 始终使用 .h 作为头文件后缀,而不是 .hpp。

  • 始终使用 .cc 作为源文件后缀,既不用 .cpp 也不用 .cxx。

  • 一个文件应只包含一个主要类,且文件名应与主要类名保持一致。

    • 明显的例外:用于定义各种杂项函数的文件。
  • 如果一个头文件有对应的源文件,它们应具有相同的文件名,仅后缀不同,例如 a.h vs a.cc。

  • 如果函数在文件 a.h 中声明,应确保它在对应的源文件 a.cc 中定义,不要在其他文件中定义。

  • CPP 文件不应有深层的源码目录结构,不要像 JAVA 那样组织。

  • 包含头文件应满足以下规则。

    • 包含必要的头文件,即仅包含一行 #include "test.h" 的源文件(.cc)应能在不包含任何其他头文件的情况下成功编译。
    • 不要包含任何不必要的头文件,包含越多,编译越慢。
    • 总之,不多不少,按需包含。

类

  • 基类名称不要以 Base 结尾,使用 Backend 而非 BackendBase。

  • 确保一个类只做一件事,遵循单一职责原则。

  • 不要写大类、巨型类,也不要暴露过多接口。

  • 区分接口与实现,将实现设为私有。

  • 设计类层次结构时,要区分接口继承与实现继承。

    • 确保公有继承表示 is-a 的关系。
    • 确保私有继承表示 implements-with 的关系。
  • 没有理由不要将函数设为 virtual。

  • 确保多态基类具有 virtual 析构函数。

  • 使用 override 使重写显式化,并让编译器发挥作用。

  • 尽可能使用 const 将成员函数标记为只读。

  • 当你尝试为类定义 copy constructor 或 operator= 时,请记住 Rule of three/five/zero。

函数

  • 使函数简短且简单。

  • 调用一个有意义的函数比原地编写过多语句更具可读性,但性能敏感的代码路径除外。

  • 给函数起一个好名字,如何判断函数名是否好。

    • 大声读出来时感觉通顺。
    • 能够通过参数表示的信息不应编码进函数名中。例如,使用 get(size_t index) 而不是 getByIndex。
  • 一个函数应专注于单一的逻辑操作。

  • 函数应做到与名称含义相符。

    • 完成函数名所涵盖的所有事情
    • 不做函数名未涵盖的任何事情

变量

  • 使变量名简单且有意义。
  • 不要把所有变量都集中在作用域顶部,这是一种过时的习惯。
  • 尽量在使用点附近声明变量。

常量

  • 优先使用 const 变量,而不是使用预处理器(#define)来定义常量值。
  • 如果需要表示空指针的常量(某个 T 的 T*),始终使用 nullptr;其他零值则使用 0。

宏

  • 宏会降低可读性、扰乱思维,并影响调试。
  • 宏具有副作用。
  • 谨慎、小心地使用宏。
  • 考虑使用 const 变量或 inline 函数来替代宏。
  • 考虑使用 do {...} while (0) 包裹来定义宏。
  • 避免直接使用第三方库的宏。

命名空间

  • 不要在头文件中 using namespace xxx。你可以在源文件中这样做,但这仍然不被鼓励。
  • 将所有 Gluten CPP 代码放置在 namespace gluten 下,因为一层命名空间已经足够。不要使用嵌套命名空间,嵌套命名空间会导致混乱。
  • 推荐使用匿名命名空间来定义文件级别的类、函数和变量,它用于放置具有文件作用域的静态函数和变量。

资源管理

  • 使用句柄和 RAII 自动管理资源。

  • 立即将显式资源分配的结果交给管理对象。

  • 优先使用作用域对象和栈对象。

  • 使用裸指针表示单个对象。

  • 如果不想使用容器,请使用 pointer + size_t 表示数组对象。

  • 裸指针(即 T*)是非拥有性的。

  • 裸引用(即 T&)是非拥有性的。

  • 理解 unique_ptr、shared_ptr、weak_ptr 之间的区别。

    • unique_ptr 表示所有权,但不表示共享所有权。unique_ptr 等价于 RAII,即在对象析构时释放资源。
    • shared_ptr 表示基于使用计数的共享所有权,其开销比 unique_ptr 更大。
    • weak_ptr 建模临时所有权,有助于打破由 shared_ptr 管理的对象所形成的引用循环。
  • 使用 unique_ptr 或 shared_ptr 表示所有权。

  • 除非需要共享所有权,否则优先使用 unique_ptr 而非 shared_ptr。

  • 使用 make_unique 创建 unique_ptr。

  • 使用 make_shared 创建 shared_ptr。

  • 仅在需要显式表达生命周期语义时,才将智能指针作为参数。

  • 对于一般用途,应使用 T* 或 T& 参数,而不是智能指针。

异常

  • 异常规范一直在变化,不同 CPP 标准之间的差异很大,因此在 Gluten 中应谨慎使用异常。
  • 优先使用 return code 而非抛出异常。
  • 优先使用编译期检查而非运行时检查。
  • 封装复杂的构造函数,而不是将其分散到代码各处。
  • 添加必要的注释。注释并非越多越好,也非越少越好。
  • 好的注释能让晦涩的代码易于理解。对于相当明显的代码,添加注释是没有必要的。

参考资料

评论

登录后参与评论

正在加载评论…