一般建议
以下是一些建议,而非硬性要求。 如果你在编辑代码,遵循现有代码的格式通常是合理的。 代码风格对于保持一致性很有必要。一致性能让代码更易于阅读,也更便于检索。 许多规则并没有严格的逻辑依据;它们更多是由约定俗成的实践决定的。
格式化
1. 大部分格式化工作由 clang-format 自动完成。
2. 缩进为 4 个空格。请配置您的开发环境,使 Tab 键输入四个空格。
3. 左右花括号必须各占一行。
inline void readBoolText(bool & x, ReadBuffer & buf)
{
char tmp = '0';
readChar(tmp, buf);
x = tmp != '0';
}4. 如果整个函数体只有一条 statement,可以将其写在同一行。在花括号两侧添加空格 (行尾空格除外) 。
inline size_t mask() const { return buf_size() - 1; }
inline size_t place(HashValue x) const { return x & mask(); }5. 对于函数,括号两侧不要加空格。
void reinsert(const Value & x)memcpy(&buf[place_value], &x, sizeof(x));6. 在 if、for、while 及其他表达式中,开括号前须加一个空格 (与函数调用不同) 。
for (size_t i = 0; i < rows; i += storage.index_granularity)7. 在二元运算符 (+、-、*、/、% 等) 和三元运算符 ?: 两侧添加空格。
UInt16 year = (s[0] - '0') * 1000 + (s[1] - '0') * 100 + (s[2] - '0') * 10 + (s[3] - '0');
UInt8 month = (s[5] - '0') * 10 + (s[6] - '0');
UInt8 day = (s[8] - '0') * 10 + (s[9] - '0');8. 如果输入了换行符,请将运算符另起一行,并增加其前面的缩进。
if (elapsed_ns)
message << " ("
<< rows_read_on_server * 1000000000 / elapsed_ns << " rows/s., "
<< bytes_read_on_server * 1000.0 / elapsed_ns << " MB/s.) ";9. 如有需要,可以在行内使用空格来对齐内容。
dst.ClickLogID = click.LogID;
dst.ClickEventID = click.EventID;
dst.ClickGoodEvent = click.GoodEvent;10. 不要在运算符 .、-> 两侧使用空格。
如有必要,operator 可以折行至下一行。此时,其前面的缩进量会相应增加。
11. 不要在一元运算符 (--、++、*、& 等) 与参数之间添加空格。
12. 逗号后加空格,逗号前不加空格。for 表达式中的分号同理。
13. 不要在 [] 运算符两侧使用空格。
14. 在 template <...> 表达式中,template 与 < 之间需加空格;< 之后和 > 之前不加空格。
template <typename TKey, typename TValue>
struct AggregatedStatElement
{}15. 在类和结构体中,public、private 和 protected 与 class/struct 保持相同的缩进级别,其余代码向内缩进。
template <typename T>
class MultiVersion
{
public:
/// 供使用的对象版本。shared_ptr 负责管理版本的生命周期。
using Version = std::shared_ptr<const T>;
...
}16. 如果整个文件使用相同的 namespace,且没有其他重要内容,则 namespace 内部无需缩进。
17. 如果 if、for、while 或其他表达式的块只包含单个 statement,则花括号是可选的。此时应将该 statement 单独置于一行。此规则同样适用于嵌套的 if、for、while ……
但如果内部 statement 包含花括号或 else,则外部块需要用花括号括起来。
/// 完成写入。
for (auto & stream : streams)
stream.second->finalize();18. 行尾不应有任何空格。
19. 源文件采用 UTF-8 编码。
20. 字符串字面量中可以使用非 ASCII 字符。
<< ", " << (timer.elapsed() / chunks_stats.hits) << " μsec/hit.";21. 不要在同一行中写多个表达式。
22. 将函数内的代码按段分组,各段之间最多只留一个空行。
23. 函数、类等内容之间用一到两个空行分隔。
24. A const (与某个值相关) 必须写在类型名称之前。
//正确
const char * pos
const std::string & s
//错误
char const * pos25. 声明指针或引用时,* 和 & 符号的左右两侧都应留空格。
//正确
const char * pos
//错误
const char* pos
const char *pos26. 使用Template类型时,请用 using 关键字为其定义别名 (最简单的情况除外) 。
换句话说,Template参数只在 using 中指定,不会在代码里重复。
using 可以在局部声明,例如在函数内部。
//正确
using FileStreams = std::map<std::string, std::shared_ptr<Stream>>;
FileStreams streams;
//错误
std::map<std::string, std::shared_ptr<Stream>> streams;27. 不要在同一条语句中声明多个不同类型的变量。
//错误
int x, *y;28. 不要使用 C 风格转换。
//错误
std::cerr << (int)c <<; std::endl;
//正确
std::cerr << static_cast<int>(c) << std::endl;29. 在类和结构体中,应在每个可见性作用域内分别对成员和函数进行分组。
30. 对于较小的类和结构体,没有必要将方法声明与实现分开。
任何类或结构体中的较小方法也是如此。
对于Template类和结构体,不要将方法声明与实现分开 (否则它们必须定义在同一个翻译单元中) 。
31. 代码行宽可以放宽到 140 个字符,而不是 80 个字符。
32. 如果不需要后缀形式,始终使用前缀自增/自减运算符。
for (Names::const_iterator it = column_names.begin(); it != column_names.end(); ++it)1. 务必为代码中所有不那么简单的部分添加注释。
这一点非常重要。编写注释的过程可能会帮助你意识到,这段代码其实没有必要,或者它的设计有问题。
/** Part of piece of memory, that can be used.
* For example, if internal_buffer is 1MB, and there was only 10 bytes loaded to buffer from file for reading,
* then working_buffer will have size of only 10 bytes
* (working_buffer.end() will point to position right after those 10 bytes available for read).
*/2. 注释可以根据需要写得详细一些。
3. 将注释放在其所说明的代码之前。少数情况下,也可以把注释放在同一行代码之后。
/** 解析并执行查询。
*/
void executeQuery(
ReadBuffer & istr, /// 从何处读取查询(以及 INSERT 的数据,如适用)
WriteBuffer & ostr, /// 将结果写入何处
Context & context, /// 数据库、表、数据类型、引擎、函数、聚合函数……
BlockInputStreamPtr & query_plan, /// 此处可记录查询执行方式的描述
QueryProcessingStage::Enum stage = QueryProcessingStage::Complete /// SELECT 查询处理到哪个阶段为止
)4. 注释只能使用英文书写。
5. 如果你在编写库,请在主头文件中添加详细注释加以说明。
6. 不要添加不能提供额外信息的注释。尤其不要留下像下面这样的空注释:
/*
* Procedure Name:
* Original procedure name:
* Author:
* Date of creation:
* Dates of modification:
* Modification authors:
* Original file name:
* Purpose:
* Intent:
* Designation:
* Classes used:
* Constants:
* Local variables:
* Parameters:
* Date of creation:
* Purpose:
*/该示例引自 http://home.tamk.fi/~jaalto/course/coding-style/doc/unmaintainable-code/。
7. 不要在每个文件开头写无意义的注释 (如 author、创建日期等) 。
8. 单行注释以三个斜杠开头:///,多行注释以 /** 开头。这些注释被视为“文档注释”。
注意:你可以使用 Doxygen 根据这些注释生成文档。但通常并不使用 Doxygen,因为在 IDE 中浏览代码更方便。
9. 多行注释的开头和结尾不能有空行 (多行注释的结束行除外) 。
10. 如果要注释掉代码,请使用普通注释,不要使用“文档注释”。
11. 提交前,删除那些被注释掉的代码片段。
12. 不要在注释或代码中使用脏话。
13. 不要使用大写字母。不要使用过多的标点符号。
/// WHAT THE FAIL???14. 不要使用注释作为分隔符。
///******************************************************15. 不要在注释中展开讨论。
/// Why did you do this stuff?16. 无需在代码块末尾添加说明其用途的注释。
/// for名称
1. 变量和类成员的名称应使用带下划线的小写字母。
size_t max_block_size;2. 对于函数 (方法) 名称,请使用首字母小写的驼峰命名法。
std::string getName() const override { return "Memory"; }3. 类名 (结构体) 应使用首字母大写的 CamelCase。interface 不使用 I 以外的前缀。
class StorageMemory : public IStorage4. using 的命名方式与类名相同。
5. Template类型参数的命名:简单情况下,使用 T、T、U、T1、T2。
更复杂的情况下,要么遵循类名的命名规则,要么添加前缀 T。
template <typename TKey, typename TValue>
struct AggregatedStatElement6. Template 常量参数的名称:要么遵循变量命名规则,要么在简单情况下使用 N。
template <bool without_www>
struct ExtractDomain7. 对于抽象类 (接口) ,可以添加 I 前缀。
class IProcessor8. 如果变量只在局部使用,可以使用短名称。
其他情况下,请使用能体现其含义的名称。
bool info_successfully_loaded = false;9. define 和全局常量的名称使用带下划线的全大写形式。
#define MAX_SRC_TABLE_NAMES_TO_STORE 100010. 文件名应与其内容采用相同的命名风格。
如果文件只包含一个类,文件名应与类名一致 (CamelCase) 。
如果文件只包含一个函数,文件名应与函数名一致 (camelCase) 。
11. 如果名称中包含缩写,则:
- 对于变量名,缩写应使用小写字母,如
mysql_connection(不要写成mySQL_connection) 。 - 对于类名和函数名,应保留缩写中的大写字母,如
MySQLConnection(不要写成MySqlConnection) 。
12. 如果构造函数参数仅用于初始化类成员,其命名应与类成员相同,但末尾要加下划线。
FileQueueProcessor(
const std::string & path_,
const std::string & prefix_,
std::shared_ptr<FileHandler> handler_)
: path(path_),
prefix(prefix_),
handler(handler_),
log(&Logger::get("FileQueueProcessor"))
{
}如果该参数在构造函数体中未被使用,则可以省略下划线后缀。
13. 局部变量和类成员的命名没有区别 (无需前缀) 。
timer (not m_timer)14. 对于 enum 中的常量,使用首字母大写的驼峰命名法。ALL_CAPS 也是可以接受的。如果 enum 不是局部的,请使用 enum class。
enum class CompressionMethod
{
QuickLZ = 0,
LZ4 = 1,
};15. 所有名称都必须使用英文。不允许将希伯来语单词音译。
不要使用 T_PAAMAYIM_NEKUDOTAYIM
16. 如果缩写广为人知 (也就是说,你可以很容易在 Wikipedia 或搜索引擎中查到其含义) ,则可以接受。
AST、SQL。
不要使用 NVDH (一些随意拼凑的字母)
如果截短后的形式已被广泛使用,也可以接受不完整的单词。
如果在注释中同时给出了全称,也可以使用缩写。
17. 包含 C++ 源代码的文件名必须使用 .cpp 扩展名。头文件必须使用 .h 扩展名。
18. 产品名称应写作 ClickHouse——一个单词,C 和 H 均大写。clickhouse_spelling 风格检查也接受惯用的标记拼写 clickhouse 和 CLICKHOUSE;其他所有变体均为拼写错误。它会检查代码、注释、消息、文档和文件名。对于软件包、二进制文件和主机名等仅使用小写字母的标记,请使用 clickhouse;在 CamelCase 标识符中使用 ClickHouse;对于宏和环境变量,则使用 CLICKHOUSE。
ClickHouse, clickhouse-client, CLICKHOUSE_DATABASE
不要使用 Clickhouse, clickHouse, click_house, CLICK_HOUSE, click-house, Click House
如何编写代码
1. 内存管理。
手动释放内存 (delete) 只能在库代码中使用。
在库代码中,delete 运算符只能在析构函数中使用。
在应用程序代码中,内存必须由其所属对象负责释放。
示例:
- 最简单的方法是将对象分配在栈上,或作为另一个类的成员。
- 对于大量小对象,请使用容器。
- 对于少量位于堆上的对象,如需自动释放,请使用
shared_ptr/unique_ptr。
2. 资源管理。
使用 RAII,并参见上文。
3. 错误处理。
使用异常。在大多数情况下,你只需要抛出异常,不需要捕获它 (因为有 RAII) 。
在离线数据处理应用程序中,通常可以不捕获异常。
在处理用户请求的服务器中,通常只需在连接处理程序的顶层捕获异常即可。
在线程函数中,你应当捕获并保存所有异常,以便在 join 之后在主线程中重新抛出。
/// 如果尚未开始任何计算,则同步计算第一个块
if (!started)
{
calculate();
started = true;
}
else /// 如果计算已在进行中,则等待结果
pool.wait();
if (exception)
exception->rethrow();绝不要在未处理异常的情况下将其隐藏。也绝不要不加甄别地把所有异常都记入日志。
//不正确
catch (...) {}如果必须忽略某些异常,也只应忽略特定的异常,其余异常都应重新抛出。
catch (const DB::Exception & e)
{
if (e.code() == ErrorCodes::UNKNOWN_AGGREGATE_FUNCTION)
return nullptr;
else
throw;
}使用带有响应代码或 errno 的函数时,务必检查返回结果,并在出错时抛出异常。
if (0 != close(fd))
throw ErrnoException(ErrorCodes::CANNOT_CLOSE_FILE, "Cannot close file {}", file_name);你可以使用 assert 来检查代码中的不变量。
4. 异常类型。
应用程序代码中没有必要使用复杂的异常层次结构。异常信息应当让系统管理员能够理解。
5. 从析构函数中抛出异常。
不建议这样做,但这是允许的。
可采用以下做法:
- 创建一个函数 (
done()或finalize()) ,提前完成所有可能导致异常的工作。如果该函数已经被调用,之后析构函数中就不应再出现异常。 - 过于复杂的任务 (例如通过网络发送消息) 可以放到单独的方法中,由类的使用者在对象销毁前调用。
- 如果析构函数中发生异常,最好将其记入日志,而不是悄悄忽略 (如果 logger 可用) 。
- 在简单的应用程序中,可以接受依赖
std::terminate(针对 C++11 中默认noexcept的情况) 来处理异常。
6. 匿名代码块。
你可以在单个函数内部创建单独的代码块,以将某些变量限制在局部作用域内,这样在退出该代码块时就会调用析构函数。
Block block = data.in->read();
{
std::lock_guard<std::mutex> lock(mutex);
data.ready = true;
data.block = block;
}
ready_any.set();7. 多线程。
在离线数据处理程序中:
- 尽量先把单个 CPU 核心上的性能做到最好。然后在有必要时再对代码进行并行化。
在服务器应用中:
- 使用线程池处理请求。到目前为止,我们还没有遇到任何需要 userspace 上下文切换的任务。
不要使用 fork 来做并行化。
8. 线程同步。
通常可以让不同线程使用不同的内存单元 (更好的是,不同的缓存行) ,从而避免使用任何线程同步机制 (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。
在少数情况下 (例如在循环中更新某个值) ,你可能需要通过参数返回该值。这种情况下,该参数应为引用。
using AggregateFunctionPtr = std::shared_ptr<IAggregateFunction>;
/** 允许通过名称创建聚合函数。
*/
class AggregateFunctionFactory
{
public:
AggregateFunctionFactory();
AggregateFunctionPtr get(const String & name, const DataTypes & argument_types) const;15. namespace。
没有必要为应用代码使用单独的 namespace。
小型库同样不需要这么做。
对于中大型库,应将所有内容都放在一个 namespace 中。
在库的 .h 文件中,可以使用 namespace detail 来隐藏应用代码不需要的实现细节。
在 .cpp 文件中,可以使用 static 或匿名 namespace 来隐藏符号。
此外,namespace 也可用于 enum,以防止相应名称进入外部 namespace (但更好的做法是使用 enum class) 。
16. 延迟初始化。
如果初始化需要参数,通常就不应该编写默认构造函数。
如果后续需要延迟初始化,可以添加一个默认构造函数来创建无效对象。或者,对于数量较少的对象,也可以使用 shared_ptr/unique_ptr。
Loader(DB::Connection * connection_, const std::string & query, size_t max_block_size_);
/// 用于延迟初始化
Loader() {}17. 虚函数。
如果某个类不打算用于多态,就不需要将函数声明为 virtual。析构函数也一样。
18. 编码。
一律使用 UTF-8。使用 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。
auto f() -> void25. 变量的声明和初始化。
//正确方式
std::string s = "Hello";
std::string s{"Hello"};
//错误方式
auto s = std::string{"Hello"};26. 对于虚函数,在基类中使用 virtual,但在派生类中应使用 override,而不是 virtual。
C++ 中未使用的功能特性
1. 不使用虚继承。
2. 现代 C++ 中那些提供了便捷语法糖的构造,例如
// 不使用语法糖的传统方式
template <typename G, typename = std::enable_if_t<std::is_same<G, F>::value, void>> // 通过 std::enable_if 实现 SFINAE,使用 ::value
std::pair<int, int> func(const E<G> & e) // 显式指定返回类型
{
if (elements.count(e)) // .count() 成员检测
{
// ...
}
elements.erase(
std::remove_if(
elements.begin(), elements.end(),
[&](const auto x){
return x == 1;
}),
elements.end()); // remove-erase 惯用法
return std::make_pair(1, 2); // 通过 make_pair() 创建 pair
}
// 使用语法糖(C++14/17/20)
template <typename G>
requires std::same_v<G, F> // 通过 C++20 概念实现 SFINAE,使用 C++14 Template别名
auto func(const E<G> & e) // auto 返回类型(C++14)
{
if (elements.contains(e)) // C++20 .contains 成员检测
{
// ...
}
elements.erase_if(
elements,
[&](const auto x){
return x == 1;
}); // C++20 std::erase_if
return {1, 2}; // 或:return std::pair(1, 2); // 通过初始化列表或值初始化创建 pair(C++17)
}平台
1. 我们为特定平台编写代码。
但在其他条件相同的情况下,优先选择跨平台或可移植的代码。
2. 语言:C++20 (参见可用的 C++20 特性列表) 。
3. 编译器: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 配置下进行开发和调试。
工具
1. KDevelop 是一个不错的 IDE。
2. 调试时,使用 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. 未使用的代码会从代码仓库中移除。
库
1. 使用 C++20 标准库 (允许使用实验性扩展) ,以及 boost 和 Poco 框架。
2. 不允许使用操作系统软件包中的库,也不允许使用预装库。所有库都应以源代码的形式放在 contrib 目录中,并随 ClickHouse 一起构建。详见添加新的第三方库指南。
3. 始终优先选择已经在使用的库。
一般建议
1. 尽量少写代码。
2. 优先尝试最简单的解决方案。
3. 在弄清楚代码将如何工作以及内部循环如何运作之前,不要动手写代码。
4. 在最简单的情况下,优先使用 using,而不是类或结构体。
5. 如果可以,尽量不要编写拷贝构造函数、赋值运算符、析构函数 (如果类中至少包含一个虚函数,则虚析构函数除外) 、移动构造函数或移动赋值运算符。换句话说,编译器生成的这些函数应当能够正常工作。你也可以使用 default。
6. 鼓励简化代码。在可能的情况下,尽量减少代码量。
其他建议
1. 对 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. 多行函数参数。
以下任一种换行风格都可以:
function(
T1 x1,
T2 x2)function(
size_t left, size_t right,
const & RangesInDataParts ranges,
size_t limit)function(size_t left, size_t right,
const & RangesInDataParts ranges,
size_t limit)function(size_t left, size_t right,
const & RangesInDataParts ranges,
size_t limit)function(
size_t left,
size_t right,
const & RangesInDataParts ranges,
size_t limit)
注释