一般建议
格式化
clang-format 自动完成。
2. 缩进为 4 个空格。请配置您的开发环境,使 Tab 键输入四个空格。
3. 左右花括号必须各占一行。
statement,可以将其写在同一行。在花括号两侧添加空格 (行尾空格除外) 。
if、for、while 及其他表达式中,开括号前须加一个空格 (与函数调用不同) 。
+、-、*、/、% 等) 和三元运算符 ?: 两侧添加空格。
.、-> 两侧使用空格。
如有必要,operator 可以折行至下一行。此时,其前面的缩进量会相应增加。
11. 不要在一元运算符 (--、++、*、& 等) 与参数之间添加空格。
12. 逗号后加空格,逗号前不加空格。for 表达式中的分号同理。
13. 不要在 [] 运算符两侧使用空格。
14. 在 template <...> 表达式中,template 与 < 之间需加空格;< 之后和 > 之前不加空格。
public、private 和 protected 与 class/struct 保持相同的缩进级别,其余代码向内缩进。
namespace,且没有其他重要内容,则 namespace 内部无需缩进。
17. 如果 if、for、while 或其他表达式的块只包含单个 statement,则花括号是可选的。此时应将该 statement 单独置于一行。此规则同样适用于嵌套的 if、for、while ……
但如果内部 statement 包含花括号或 else,则外部块需要用花括号括起来。
A const (与某个值相关) 必须写在类型名称之前。
* 和 & 符号的左右两侧都应留空格。
using 关键字为其定义别名 (最简单的情况除外) 。
换句话说,Template参数只在 using 中指定,不会在代码里重复。
using 可以在局部声明,例如在函数内部。
注释
///,多行注释以 /** 开头。这些注释被视为“文档注释”。
注意:你可以使用 Doxygen 根据这些注释生成文档。但通常并不使用 Doxygen,因为在 IDE 中浏览代码更方便。
9. 多行注释的开头和结尾不能有空行 (多行注释的结束行除外) 。
10. 如果要注释掉代码,请使用普通注释,不要使用“文档注释”。
11. 提交前,删除那些被注释掉的代码片段。
12. 不要在注释或代码中使用脏话。
13. 不要使用大写字母。不要使用过多的标点符号。
名称
using 的命名方式与类名相同。
5. Template类型参数的命名:简单情况下,使用 T、T、U、T1、T2。
更复杂的情况下,要么遵循类名的命名规则,要么添加前缀 T。
N。
I 前缀。
define 和全局常量的名称使用带下划线的全大写形式。
- 对于变量名,缩写应使用小写字母,如
mysql_connection(不要写成mySQL_connection) 。 - 对于类名和函数名,应保留缩写中的大写字母,如
MySQLConnection(不要写成MySqlConnection) 。
enum 中的常量,使用首字母大写的驼峰命名法。ALL_CAPS 也是可以接受的。如果 enum 不是局部的,请使用 enum class。
AST、SQL。
不要使用 NVDH (一些随意拼凑的字母)
如果截短后的形式已被广泛使用,也可以接受不完整的单词。
如果在注释中同时给出了全称,也可以使用缩写。
17. 包含 C++ 源代码的文件名必须使用 .cpp 扩展名。头文件必须使用 .h 扩展名。
如何编写代码
delete) 只能在库代码中使用。
在库代码中,delete 运算符只能在析构函数中使用。
在应用程序代码中,内存必须由其所属对象负责释放。
示例:
- 最简单的方法是将对象分配在栈上,或作为另一个类的成员。
- 对于大量小对象,请使用容器。
- 对于少量位于堆上的对象,如需自动释放,请使用
shared_ptr/unique_ptr。
RAII,并参见上文。
3. 错误处理。
使用异常。在大多数情况下,你只需要抛出异常,不需要捕获它 (因为有 RAII) 。
在离线数据处理应用程序中,通常可以不捕获异常。
在处理用户请求的服务器中,通常只需在连接处理程序的顶层捕获异常即可。
在线程函数中,你应当捕获并保存所有异常,以便在 join 之后在主线程中重新抛出。
errno 的函数时,务必检查返回结果,并在出错时抛出异常。
- 创建一个函数 (
done()或finalize()) ,提前完成所有可能导致异常的工作。如果该函数已经被调用,之后析构函数中就不应再出现异常。 - 过于复杂的任务 (例如通过网络发送消息) 可以放到单独的方法中,由类的使用者在对象销毁前调用。
- 如果析构函数中发生异常,最好将其记入日志,而不是悄悄忽略 (如果 logger 可用) 。
- 在简单的应用程序中,可以接受依赖
std::terminate(针对 C++11 中默认noexcept的情况) 来处理异常。
- 尽量先把单个 CPU 核心上的性能做到最好。然后在有必要时再对代码进行并行化。
- 使用线程池处理请求。到目前为止,我们还没有遇到任何需要 userspace 上下文切换的任务。
joinAll 除外) 。
如果确实需要同步,大多数情况下,使用 lock_guard 保护的 mutex 就足够了。
其他情况下,使用系统同步原语。不要使用忙等待。
只有在最简单的情况下才应使用原子操作。
除非这正是你的主要专长领域,否则不要尝试实现无锁数据结构。
9. 指针与引用。
大多数情况下,优先使用引用。
10. const。
使用常量引用、指向常量的指针、const_iterator 和 const 方法。
将 const 视为默认选择,只有在必要时才使用非 const。
按值传递变量时,使用 const 通常没有意义。
11. unsigned。
必要时使用 unsigned。
12. 数值类型。
使用类型 UInt8、UInt16、UInt32、UInt64、Int8、Int16、Int32 和 Int64,以及 size_t、ssize_t 和 ptrdiff_t。
不要对数值使用这些类型:signed/unsigned long、long long、short、signed/unsigned char、char。
13. 传递参数。
如果复杂值后续需要被移动,则按值传递并使用 std::move;如果需要在循环中更新某个值,则按引用传递。
如果函数会接管在堆上创建的对象的所有权,应将参数类型设为 shared_ptr 或 unique_ptr。
14. 返回值。
大多数情况下,直接使用 return 即可。不要写 return std::move(res)。
如果函数在堆上分配对象并返回它,应使用 shared_ptr 或 unique_ptr。
在少数情况下 (例如在循环中更新某个值) ,你可能需要通过参数返回该值。这种情况下,该参数应为引用。
namespace。
没有必要为应用代码使用单独的 namespace。
小型库同样不需要这么做。
对于中大型库,应将所有内容都放在一个 namespace 中。
在库的 .h 文件中,可以使用 namespace detail 来隐藏应用代码不需要的实现细节。
在 .cpp 文件中,可以使用 static 或匿名 namespace 来隐藏符号。
此外,namespace 也可用于 enum,以防止相应名称进入外部 namespace (但更好的做法是使用 enum class) 。
16. 延迟初始化。
如果初始化需要参数,通常就不应该编写默认构造函数。
如果后续需要延迟初始化,可以添加一个默认构造函数来创建无效对象。或者,对于数量较少的对象,也可以使用 shared_ptr/unique_ptr。
std::string 和 char *。不要使用 std::wstring 和 wchar_t。
19. 日志。
请参考代码中的各处示例。
提交前,删除所有无意义的日志、调试日志,以及其他任何类型的调试输出。
应避免在循环中记录日志,即使是 Trace 级别也不例外。
在任何日志级别下,日志都必须具有可读性。
在大多数情况下,日志只应在应用程序代码中使用。
日志消息必须用英文编写。
日志内容最好能让系统管理员看懂。
不要在日志中使用脏话。
日志应使用 UTF-8 编码。在极少数情况下,可以在日志中使用非 ASCII 字符。
20. 输入输出。
不要在对应用程序性能至关重要的内部循环中使用 iostreams (并且绝不要使用 stringstream) 。
请改用 DB/IO 库。
21. 日期和时间。
参见 DateLUT 库。
22. include。
始终使用 #pragma once,不要使用 include guard。
23. using。
不要使用 using namespace。可以对具体项使用 using,但要将其局部限制在类或函数内部。
24. 除非必要,否则不要对函数使用 trailing return type。
virtual,但在派生类中应使用 override,而不是 virtual。
C++ 中未使用的功能特性
平台
clang。在撰写本文时 (2025 年 3 月) ,代码使用 >= 19 版本的 clang 编译。
使用标准库 (libc++) 。
4. 操作系统:Ubuntu Linux,不早于 Precise。
5. 代码面向 x86_64 CPU 架构编写。
CPU 指令集采用我们各台服务器均支持的最低指令集。目前为 SSE 4.2。
6. 使用 -Wall -Wextra -Werror -Weverything 编译选项,但有少数例外。
7. 除了那些难以静态链接的库之外,其他所有库都使用静态链接 (参见 ldd 命令的输出) 。
8. 代码在 release 配置下进行开发和调试。
工具
gdb、valgrind (memcheck) 、strace、-fsanitize=... 或 tcmalloc_minimal_debug。
3. 做性能分析时,使用 Linux Perf、valgrind (callgrind) 或 strace -cf。
4. 源代码存放在 Git 中。
5. 构建使用 CMake。
6. 程序通过 deb 包发布。
7. 提交到 master 的代码不得导致构建失败。
不过,只有选定的修订版本才被认为是可用的。
8. 尽可能频繁地提交,即使代码还只完成了一部分。
为此请使用分支。
如果你在 master 分支中的代码还无法构建,请在 push 之前将其从构建中排除。你需要在几天内完成它或将其删除。
9. 对于较复杂的更改,请使用分支并将其发布到服务器上。
10. 未使用的代码会从代码仓库中移除。
库
boost 和 Poco 框架。
2. 不允许使用操作系统软件包中的库,也不允许使用预装库。所有库都应以源代码的形式放在 contrib 目录中,并随 ClickHouse 一起构建。详见添加新的第三方库指南。
3. 始终优先选择已经在使用的库。
一般建议
using,而不是类或结构体。
5. 如果可以,尽量不要编写拷贝构造函数、赋值运算符、析构函数 (如果类中至少包含一个虚函数,则虚析构函数除外) 、移动构造函数或移动赋值运算符。换句话说,编译器生成的这些函数应当能够正常工作。你也可以使用 default。
6. 鼓励简化代码。在可能的情况下,尽量减少代码量。
其他建议
stddef.h 中的类型显式加上 std::
不推荐这样做。换句话说,我们建议写 size_t,而不是 std::size_t,因为前者更简洁。
当然,加上 std:: 也是可以接受的。
2. 对标准 C 库中的函数显式加上 std::
不推荐这样做。换句话说,应该写 memcpy,而不是 std::memcpy。
原因是还存在类似的非标准函数,例如 memmem。我们确实偶尔会用到这些函数,而它们并不存在于 namespace std 中。
如果你到处都写 std::memcpy 而不是 memcpy,那么不带 std:: 的 memmem 看起来就会很奇怪。
不过,如果你更喜欢,也还是可以使用 std::。
3. 当标准 C++ 库中提供了相同函数时,使用 C 中的函数。
如果这样做效率更高,也是可以接受的。
例如,复制大块内存时,使用 memcpy 而不是 std::copy。
4. 多行函数参数。
以下任一种换行风格都可以: