Skip to content
ClickHouse Docs
ClickHouse DocsClickHouse Docs

C++ 风格指南

一般建议

以下是一些建议,而非硬性要求。 如果你在编辑代码,遵循现有代码的格式通常是合理的。 代码风格对于保持一致性很有必要。一致性能让代码更易于阅读,也更便于检索。 许多规则并没有严格的逻辑依据;它们更多是由约定俗成的实践决定的。

格式化

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 * pos

25. 声明指针或引用时,* 和 & 符号的左右两侧都应留空格。

//正确
const char * pos
//错误
const char* pos
const char *pos

26. 使用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 IStorage

4. using 的命名方式与类名相同。

5. Template类型参数的命名:简单情况下,使用 T、T、U、T1、T2。

更复杂的情况下,要么遵循类名的命名规则,要么添加前缀 T。

template <typename TKey, typename TValue>
struct AggregatedStatElement

6. Template 常量参数的名称:要么遵循变量命名规则,要么在简单情况下使用 N。

template <bool without_www>
struct ExtractDomain

7. 对于抽象类 (接口) ,可以添加 I 前缀。

class IProcessor

8. 如果变量只在局部使用,可以使用短名称。

其他情况下,请使用能体现其含义的名称。

bool info_successfully_loaded = false;

9. define 和全局常量的名称使用带下划线的全大写形式。

#define MAX_SRC_TABLE_NAMES_TO_STORE 1000

10. 文件名应与其内容采用相同的命名风格。

如果文件只包含一个类,文件名应与类名一致 (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() -> void

25. 变量的声明和初始化。

//正确方式
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)
Navigation