跳到主要内容
跳到主要内容
编辑此页面

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.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. 不要在运算符 .-> 周围使用空格。

如有必要,运算符可以换行到下一行。在这种情况下,其前面的偏移量会增加。

11. 不要使用空格来分隔一元运算符(--++*&、...)与参数。

12. 在逗号后放置一个空格,但不要在其前面放置空格。对于 for 表达式内的分号,规则相同。

13. 不要使用空格来分隔 [] 运算符。

14.template <...> 表达式中,在 template< 之间使用一个空格;< 之后或 > 之前没有空格。

template <typename TKey, typename TValue>
struct AggregatedStatElement
{}

15. 在类和结构中,将 publicprivateprotectedclass/struct 放在同一级别,并缩进其余代码。

template <typename T>
class MultiVersion
{
public:
/// Version of object for usage. shared_ptr manage lifetime of version.
using Version = std::shared_ptr<const T>;
...
}

16. 如果整个文件都使用相同的 namespace,并且没有其他重要的内容,则 namespace 内部不需要偏移量。

17. 如果 ifforwhile 或其他表达式的块由单个 statement 组成,则花括号是可选的。将 statement 放在单独的行上。此规则也适用于嵌套的 ifforwhile、...

但是,如果内部 statement 包含花括号或 else,则外部块应写在花括号中。

/// Finish write.
for (auto & stream : streams)
stream.second->finalize();

18. 行尾不应有任何空格。

19. 源文件是 UTF-8 编码。

20. 非 ASCII 字符可以在字符串字面量中使用。

<< ", " << (timer.elapsed() / chunks_stats.hits) << " μsec/hit.";

21. 不要在单行中编写多个表达式。

22. 将函数内部的代码段分组,并用不超过一个空行分隔它们。

23. 用一个或两个空行分隔函数、类等。

24. const (与值相关) 必须写在类型名称之前。

//correct
const char * pos
const std::string & s
//incorrect
char const * pos

25. 当声明指针或引用时,*& 符号应在两侧用空格分隔。

//correct
const char * pos
//incorrect
const char* pos
const char *pos

26. 当使用模板类型时,使用 using 关键字为其创建别名(最简单的情况除外)。

换句话说,模板参数仅在 using 中指定,并且不会在代码中重复。

using 可以本地声明,例如在函数内部。

//correct
using FileStreams = std::map<std::string, std::shared_ptr<Stream>>;
FileStreams streams;
//incorrect
std::map<std::string, std::shared_ptr<Stream>> streams;

27. 不要在一条语句中声明多个不同类型的变量。

//incorrect
int x, *y;

28. 不要使用 C 风格的强制类型转换。

//incorrect
std::cerr << (int)c <<; std::endl;
//correct
std::cerr << static_cast<int>(c) << std::endl;

29. 在类和结构中,在每个可见性作用域内分别对成员和函数进行分组。

30. 对于小型类和结构,不必将方法声明与实现分开。

对于任何类或结构中的小型方法也是如此。

对于模板类和结构,不要将方法声明与实现分开(因为否则它们必须在同一翻译单元中定义)。

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. 将注释放在它们描述的代码之前。在极少数情况下,注释可以放在代码之后,在同一行上。

/** Parses and executes the query.
*/
void executeQuery(
ReadBuffer & istr, /// Where to read the query from (and data for INSERT, if applicable)
WriteBuffer & ostr, /// Where to write the result
Context & context, /// DB, tables, data types, engines, functions, aggregate functions...
BlockInputStreamPtr & query_plan, /// Here could be written the description on how query was executed
QueryProcessingStage::Enum stage = QueryProcessingStage::Complete /// Up to which stage process the SELECT query
)

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. 不要在每个文件的开头编写垃圾注释(作者、创建日期...)。

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. 对于函数(方法)的名称,使用以小写字母开头的 camelCase 风格。

std::string getName() const override { return "Memory"; }

3. 对于类(结构)的名称,使用以大写字母开头的 CamelCase 风格。接口不使用 I 以外的前缀。

class StorageMemory : public IStorage

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

5. 模板类型参数的名称:在简单的情况下,使用 TTUT1T2

对于更复杂的情况,请遵循类名称的规则,或添加前缀 T

template <typename TKey, typename TValue>
struct AggregatedStatElement

6. 模板常量参数的名称:遵循变量名称的规则,或者在简单的情况下使用 N

template <bool without_www>
struct ExtractDomain

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

class IProcessor

8. 如果您在本地使用变量,则可以使用短名称。

在所有其他情况下,请使用描述含义的名称。

bool info_successfully_loaded = false;

9. define 和全局常量的名称使用 ALL_CAPS 风格,并带下划线。

#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 中的常量,使用首字母大写的 CamelCase 风格。ALL_CAPS 也是可以接受的。如果 enum 是非本地的,请使用 enum class

enum class CompressionMethod
{
QuickLZ = 0,
LZ4 = 1,
};

15. 所有名称必须是英文。不允许音译希伯来语单词。

不是 T_PAAMAYIM_NEKUDOTAYIM

16. 如果缩写是众所周知的(当您可以轻松在维基百科或搜索引擎中找到缩写的含义时),则可以接受缩写。

ASTSQL

不是 NVDH(一些随机字母)

如果缩短版本是常用的,则可以接受不完整的单词。

如果注释中包含了全名,您也可以使用缩写。

17. 带有 C++ 源代码的文件名必须具有 .cpp 扩展名。头文件必须具有 .h 扩展名。

如何编写代码

1. 内存管理。

手动内存释放 (delete) 只能在库代码中使用。

在库代码中,delete 运算符只能在析构函数中使用。

在应用程序代码中,内存必须由拥有它的对象释放。

示例

  • 最简单的方法是将对象放在堆栈上,或使其成为另一个类的成员。
  • 对于大量小型对象,请使用容器。
  • 对于驻留在堆中的少量对象的自动释放,请使用 shared_ptr/unique_ptr

2. 资源管理。

使用 RAII 并参见上文。

3. 错误处理。

使用异常。在大多数情况下,您只需要抛出异常,而不需要捕获它(因为 RAII)。

在离线数据处理应用程序中,通常可以接受不捕获异常。

在处理用户请求的服务器中,通常只需在连接处理程序的顶层捕获异常即可。

在线程函数中,您应该捕获并保留所有异常,以便在 join 后在主线程中重新抛出它们。

/// If there weren't any calculations yet, calculate the first block synchronously
if (!started)
{
calculate();
started = true;
}
else /// If calculations are already in progress, wait for the result
pool.wait();

if (exception)
exception->rethrow();

永远不要隐藏异常而不处理。永远不要只是盲目地将所有异常记录到日志中。

//Not correct
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()),它将预先完成所有可能导致异常的工作。如果调用了该函数,则稍后在析构函数中不应出现异常。
  • 过于复杂的任务(例如通过网络发送消息)可以放在单独的方法中,类用户必须在销毁之前调用该方法。
  • 如果析构函数中存在异常,最好记录它而不是隐藏它(如果记录器可用)。
  • 在简单的应用程序中,可以接受依赖 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 核心上获得最佳性能。如果需要,您可以并行化您的代码。

在服务器应用程序中

  • 使用线程池来处理请求。目前,我们还没有任何需要用户空间上下文切换的任务。

Fork 不用于并行化。

8. 同步线程。

通常可以使不同的线程使用不同的内存单元(甚至更好:不同的缓存行),并且不使用任何线程同步(除了 joinAll)。

如果需要同步,在大多数情况下,使用 lock_guard 下的互斥锁就足够了。

在其他情况下,请使用系统同步原语。不要使用忙等待。

原子操作应仅在最简单的情况下使用。

除非它是您的主要专业领域,否则不要尝试实现无锁数据结构。

9. 指针 vs 引用。

在大多数情况下,首选引用。

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>;

/** Allows creating an aggregate function by its name.
*/
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_);

/// For deferred initialization
Loader() {}

17. 虚函数。

如果该类不用于多态用途,则无需将函数设为虚函数。这也适用于析构函数。

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 保护。

23. using。

不使用 using namespace。您可以将 using 与特定内容一起使用。但使其在类或函数内部是局部的。

24. 除非必要,否则不要对函数使用 trailing return type

auto f() -> void

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

//right way
std::string s = "Hello";
std::string s{"Hello"};

//wrong way
auto s = std::string{"Hello"};

26. 对于虚函数,在基类中编写 virtual,但在派生类中编写 override 而不是 virtual

C++ 的未使用特性

1. 不使用虚继承。

2. 在现代 C++ 中具有方便的语法糖的结构,例如

// Traditional way without syntactic sugar
template <typename G, typename = std::enable_if_t<std::is_same<G, F>::value, void>> // SFINAE via std::enable_if, usage of ::value
std::pair<int, int> func(const E<G> & e) // explicitly specified return type
{
if (elements.count(e)) // .count() membership test
{
// ...
}

elements.erase(
std::remove_if(
elements.begin(), elements.end(),
[&](const auto x){
return x == 1;
}),
elements.end()); // remove-erase idiom

return std::make_pair(1, 2); // create pair via make_pair()
}

// With syntactic sugar (C++14/17/20)
template <typename G>
requires std::same_v<G, F> // SFINAE via C++20 concept, usage of C++14 template alias
auto func(const E<G> & e) // auto return type (C++14)
{
if (elements.contains(e)) // C++20 .contains membership test
{
// ...
}

elements.erase_if(
elements,
[&](const auto x){
return x == 1;
}); // C++20 std::erase_if

return {1, 2}; // or: return std::pair(1, 2); // create pair via initialization list or value initialization (C++17)
}

平台

1. 我们为特定平台编写代码。

但在其他条件相同的情况下,首选跨平台或可移植代码。

2. 语言:C++20(请参阅可用的 C++20 特性列表)。

3. 编译器:clang。在编写本文时(2022 年 7 月),代码是使用 clang 版本 >= 12 编译的。(它也可以使用 gcc 编译,但这未经测试,不适用于生产用途)。

使用标准库 (libc++)。

**4.**操作系统:Linux Ubuntu,不低于 Precise 版本。

**5.**代码是为 x86_64 CPU 架构编写的。

CPU 指令集是我们服务器中支持的最低指令集。目前,它是 SSE 4.2。

6. 使用 -Wall -Wextra -Werror -Weverything 编译标志,但有一些例外。

7. 使用静态链接与除那些难以静态连接的库之外的所有库(请参阅 ldd 命令的输出)。

8. 代码是在发布设置下开发和调试的。

工具

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)