C++ 代码风格
这是 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而非抛出异常。 - 优先使用编译期检查而非运行时检查。
- 封装复杂的构造函数,而不是将其分散到代码各处。
- 添加必要的注释。注释并非越多越好,也非越少越好。
- 好的注释能让晦涩的代码易于理解。对于相当明显的代码,添加注释是没有必要的。
参考资料
- CppCoreGuidelines
- Velox CODING_STYLE
- 感谢 Gluten 开发者们明智的建议与帮助。
评论
登录后参与评论
KnowForge