跳转到主要内容

一般建议

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

格式化

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.ifforwhile 及其他表达式中,开括号前须加一个空格 (与函数调用不同) 。
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. 在类和结构体中,publicprivateprotectedclass/struct 保持相同的缩进级别,其余代码向内缩进。
template <typename T>
class MultiVersion
{
public:
    /// 供使用的对象版本。shared_ptr 负责管理版本的生命周期。
    using Version = std::shared_ptr<const T>;
    ...
}
16. 如果整个文件使用相同的 namespace,且没有其他重要内容,则 namespace 内部无需缩进。 17. 如果 ifforwhile 或其他表达式的块只包含单个 statement,则花括号是可选的。此时应将该 statement 单独置于一行。此规则同样适用于嵌套的 ifforwhile …… 但如果内部 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. 类名 (struct) 应使用首字母大写的 CamelCase。interface 不使用 I 以外的前缀。
class StorageMemory : public IStorage
4. using 的命名方式与类名相同。 5. Template类型参数的命名:简单情况下,使用 TTUT1T2 更复杂的情况下,要么遵循类名的命名规则,要么添加前缀 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 或搜索引擎中查到其含义) ,则可以接受。 ASTSQL 不要使用 NVDH (一些随意拼凑的字母) 如果截短后的形式已被广泛使用,也可以接受不完整的单词。 如果在注释中同时给出了全称,也可以使用缩写。 17. 包含 C++ 源代码的文件名必须使用 .cpp 扩展名。头文件必须使用 .h 扩展名。

如何编写代码

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_iteratorconst 方法。 const 视为默认选择,只有在必要时才使用非 const 按值传递变量时,使用 const 通常没有意义。 11. unsigned。 必要时使用 unsigned 12. 数值类型。 使用类型 UInt8UInt16UInt32UInt64Int8Int16Int32Int64,以及 size_tssize_tptrdiff_t 不要对数值使用这些类型:signed/unsigned longlong longshortsigned/unsigned charchar 13. 传递参数。 如果复杂值后续需要被移动,则按值传递并使用 std::move;如果需要在循环中更新某个值,则按引用传递。 如果函数会接管在堆上创建的对象的所有权,应将参数类型设为 shared_ptrunique_ptr 14. 返回值。 大多数情况下,直接使用 return 即可。不要写 return std::move(res) 如果函数在堆上分配对象并返回它,应使用 shared_ptrunique_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::stringchar *。不要使用 std::wstringwchar_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. 调试时,使用 gdbvalgrind (memcheck) 、strace-fsanitize=...tcmalloc_minimal_debug 3. 做性能分析时,使用 Linux Perfvalgrind (callgrind) 或 strace -cf 4. 源代码存放在 Git 中。 5. 构建使用 CMake 6. 程序通过 deb 包发布。 7. 提交到 master 的代码不得导致构建失败。 不过,只有选定的修订版本才被认为是可用的。 8. 尽可能频繁地提交,即使代码还只完成了一部分。 为此请使用分支。 如果你在 master 分支中的代码还无法构建,请在 push 之前将其从构建中排除。你需要在几天内完成它或将其删除。 9. 对于较复杂的更改,请使用分支并将其发布到服务器上。 10. 未使用的代码会从代码仓库中移除。

1. 使用 C++20 标准库 (允许使用实验性扩展) ,以及 boostPoco 框架。 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)
最后修改于 2026年6月10日