如何编写 C++ 代码

一般建议

1. 以下是建议,而不是要求。

2. 如果你在修改代码,遵守已有的风格是有意义的。

3. 代码的风格需保持一致。一致的风格有利于阅读代码,并且方便检索代码。

4. 许多规则没有逻辑原因; 它们是由既定的做法决定的。

格式化

1. 大多数格式化可以用 clang-format 自动完成。

2. 缩进是4个空格。 配置开发环境,使得 TAB 代表添加四个空格。

3. 左右花括号需在单独的行。

  1. inline void readBoolText(bool & x, ReadBuffer & buf)
  2. {
  3. char tmp = '0';
  4. readChar(tmp, buf);
  5. x = tmp != '0';
  6. }

4. 若整个方法体仅有一行 描述, 则可以放到单独的行上。 在花括号周围放置空格(除了行尾的空格)。

  1. inline size_t mask() const { return buf_size() - 1; }
  2. inline size_t place(HashValue x) const { return x & mask(); }

5. 对于函数。 不要在括号周围放置空格。

  1. void reinsert(const Value & x)
  1. memcpy(&buf[place_value], &x, sizeof(x));

6.ifforwhile和其他表达式中,在开括号前面插入一个空格(与函数声明相反)。

  1. for (size_t i = 0; i < rows; i += storage.index_granularity)

7. 在二元运算符(+-*/,…)和三元运算符 ?: 周围添加空格。

  1. UInt16 year = (s[0] - '0') * 1000 + (s[1] - '0') * 100 + (s[2] - '0') * 10 + (s[3] - '0');
  2. UInt8 month = (s[5] - '0') * 10 + (s[6] - '0');
  3. UInt8 day = (s[8] - '0') * 10 + (s[9] - '0');

8. 若有换行,新行应该以运算符开头,并且增加对应的缩进。

  1. if (elapsed_ns)
  2. message << " ("
  3. << rows_read_on_server * 1000000000 / elapsed_ns << " rows/s., "
  4. << bytes_read_on_server * 1000.0 / elapsed_ns << " MB/s.) ";

9. 如果需要,可以在一行内使用空格来对齐。

  1. dst.ClickLogID = click.LogID;
  2. dst.ClickEventID = click.EventID;
  3. dst.ClickGoodEvent = click.GoodEvent;

10. 不要在 .-> 周围加入空格

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

11. 不要使用空格来分开一元运算符 (--, ++, *, &, …) 和参数。

12. 在逗号后面加一个空格,而不是在之前。同样的规则也适合 for 循环中的分号。

13. 不要用空格分开 [] 运算符。

14.template <...> 表达式中,在 template< 中加入一个空格,在 < 后面或在 > 前面都不要有空格。

  1. template <typename TKey, typename TValue>
  2. struct AggregatedStatElement
  3. {}

15. 在类和结构体中, publicprivate 以及 protectedclass/struct 无需缩进,其他代码须缩进。

  1. template <typename T>
  2. class MultiVersion
  3. {
  4. public:
  5. /// Version of object for usage. shared_ptr manage lifetime of version.
  6. using Version = std::shared_ptr<const T>;
  7. ...
  8. }

16. 如果对整个文件使用相同的 namespace,并且没有其他重要的东西,则 namespace 中不需要偏移量。

17.if, for, while 中包裹的代码块中,若代码是一个单行的 statement,那么大括号是可选的。 可以将 statement 放到一行中。这个规则同样适用于嵌套的 ifforwhile, …

但是如果内部 statement 包含大括号或 else,则外部块应该用大括号括起来。

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

18. 行的末尾不应该包含空格。

19. 源文件应该用 UTF-8 编码。

20. 非ASCII字符可用于字符串文字。

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

21 不要在一行中写入多个表达式。

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

23. 将 函数,类用一个或两个空行分开。

24. const 必须写在类型名称之前。

  1. //correct
  2. const char * pos
  3. const std::string & s
  4. //incorrect
  5. char const * pos

25. 声明指针或引用时,* 符号两边应该都用空格分隔。

  1. //correct
  2. const char * pos
  3. //incorrect
  4. const char* pos
  5. const char *pos

26. 使用模板类型时,使用 using 关键字对它们进行别名(最简单的情况除外)。

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

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

  1. //correct
  2. using FileStreams = std::map<std::string, std::shared_ptr<Stream>>;
  3. FileStreams streams;
  4. //incorrect
  5. std::map<std::string, std::shared_ptr<Stream>> streams;

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

  1. //incorrect
  2. int x, *y;

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

  1. //incorrect
  2. std::cerr << (int)c <<; std::endl;
  3. //correct
  4. std::cerr << static_cast<int>(c) << std::endl;

29. 在类和结构中,组成员和函数分别在每个可见范围内。

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

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

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

31. 您可以将换行规则定在140个字符,而不是80个字符。

32. 如果不需要 postfix,请始终使用前缀增量/减量运算符。

  1. for (Names::const_iterator it = column_names.begin(); it != column_names.end(); ++it)

评论

1. 请务必为所有非常重要的代码部分添加注释。

这是非常重要的。 编写注释可能会帮助您意识到代码不是必需的,或者设计错误。

  1. /** Part of piece of memory, that can be used.
  2. * For example, if internal_buffer is 1MB, and there was only 10 bytes loaded to buffer from file for reading,
  3. * then working_buffer will have size of only 10 bytes
  4. * (working_buffer.end() will point to position right after those 10 bytes available for read).
  5. */

2. 注释可以尽可能详细。

3. 在他们描述的代码之前放置注释。 在极少数情况下,注释可以在代码之后,在同一行上。

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

4. 注释应该只用英文撰写。

5. 如果您正在编写库,请在主头文件中包含解释它的详细注释。

6. 请勿添加无效的注释。 特别是,不要留下像这样的空注释:

  1. /*
  2. * Procedure Name:
  3. * Original procedure name:
  4. * Author:
  5. * Date of creation:
  6. * Dates of modification:
  7. * Modification authors:
  8. * Original file name:
  9. * Purpose:
  10. * Intent:
  11. * Designation:
  12. * Classes used:
  13. * Constants:
  14. * Local variables:
  15. * Parameters:
  16. * Date of creation:
  17. * Purpose:
  18. */

这个示例来源于 http://home.tamk.fi/~jaalto/course/coding-style/doc/unmaintainable-code/

7. 不要在每个文件的开头写入垃圾注释(作者,创建日期…)。

8. 单行注释用三个斜杆: /// ,多行注释以 /**开始。 这些注释会当做文档。

注意:您可以使用 Doxygen 从这些注释中生成文档。 但是通常不使用 Doxygen,因为在 IDE 中导航代码更方便。

9. 多行注释的开头和结尾不得有空行(关闭多行注释的行除外)。

10. 要注释掉代码,请使用基本注释,而不是“文档”注释。

11. 在提交之前删除代码的无效注释部分。

12. 不要在注释或代码中使用亵渎语言。

13. 不要使用大写字母。 不要使用过多的标点符号。

  1. /// WHAT THE FAIL???

14. 不要使用注释来制作分隔符。

  1. ///******************************************************

15. 不要在注释中开始讨论。

  1. /// Why did you do this stuff?

16. 没有必要在块的末尾写一条注释来描述它的含义。

  1. /// for

姓名

1. 在变量和类成员的名称中使用带下划线的小写字母。

  1. size_t max_block_size;

2. 对于函数(方法)的名称,请使用以小写字母开头的驼峰标识。

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

3. 对于类(结构)的名称,使用以大写字母开头的驼峰标识。接口名称用I前缀。

  1. class StorageMemory : public IStorage

4. using 的命名方式与类相同,或者以__t`命名。

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

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

  1. template <typename TKey, typename TValue>
  2. struct AggregatedStatElement

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

  1. template <bool without_www>
  2. struct ExtractDomain

7. 对于抽象类(接口),用 I 前缀。

  1. class IBlockInputStream

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

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

  1. bool info_successfully_loaded = false;

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

  1. #define MAX_SRC_TABLE_NAMES_TO_STORE 1000

10. 文件名应使用与其内容相同的样式。

如果文件包含单个类,则以与该类名称相同的方式命名该文件(CamelCase)。

如果文件包含单个函数,则以与函数名称相同的方式命名文件(camelCase)。

11. 如果名称包含缩写,则:

  • 对于变量名,缩写应使用小写字母 mysql_connection(不是 mySQL_connection )。
  • 对于类和函数的名称,请将大写字母保留在缩写 MySQLConnection(不是 MySqlConnection)。

12. 仅用于初始化类成员的构造方法参数的命名方式应与类成员相同,但最后使用下划线。

  1. FileQueueProcessor(
  2. const std::string & path_,
  3. const std::string & prefix_,
  4. std::shared_ptr<FileHandler> handler_)
  5. : path(path_),
  6. prefix(prefix_),
  7. handler(handler_),
  8. log(&Logger::get("FileQueueProcessor"))
  9. {
  10. }

如果构造函数体中未使用该参数,则可以省略下划线后缀。

13. 局部变量和类成员的名称没有区别(不需要前缀)。

  1. timer (not m_timer)

14. 对于 enum 中的常量,请使用带大写字母的驼峰标识。ALL_CAPS 也可以接受。如果 enum 是非本地的,请使用 enum class

  1. enum class CompressionMethod
  2. {
  3. QuickLZ = 0,
  4. LZ4 = 1,
  5. };

15. 所有名字必须是英文。不允许音译俄语单词。

  1. not Stroka

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

  1. `AST`, `SQL`.
  2. Not `NVDH` (some random letters)

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

如果旁边有注释包含全名,您也可以使用缩写。

17. C++ 源码文件名称必须为 .cpp 拓展名。 头文件必须为 .h 拓展名。

如何编写代码

1. 内存管理。

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

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

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

示例:

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

2. 资源管理。

使用 RAII 以及查看以上说明。

3. 错误处理。

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

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

在处理用户请求的服务器中,捕获连接处理程序顶层的异常通常就足够了。

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

  1. /// If there weren't any calculations yet, calculate the first block synchronously
  2. if (!started)
  3. {
  4. calculate();
  5. started = true;
  6. }
  7. else /// If calculations are already in progress, wait for the result
  8. pool.wait();
  9. if (exception)
  10. exception->rethrow();

不处理就不要隐藏异常。 永远不要盲目地把所有异常都记录到日志中。

  1. //Not correct
  2. catch (...) {}

如果您需要忽略某些异常,请仅针对特定异常执行此操作并重新抛出其余异常。

  1. catch (const DB::Exception & e)
  2. {
  3. if (e.code() == ErrorCodes::UNKNOWN_AGGREGATE_FUNCTION)
  4. return nullptr;
  5. else
  6. throw;
  7. }

当使用具有返回码或 errno 的函数时,请始终检查结果并在出现错误时抛出异常。

  1. if (0 != close(fd))
  2. throwFromErrno("Cannot close file " + file_name, ErrorCodes::CANNOT_CLOSE_FILE);

不要使用断言

4. 异常类型。

不需要在应用程序代码中使用复杂的异常层次结构。 系统管理员应该可以理解异常文本。

5. 从析构函数中抛出异常。

不建议这样做,但允许这样做。

按照以下选项:

  • 创建一个函数( done()finalize() ),它将提前完成所有可能导致异常的工作。 如果调用了该函数,则稍后在析构函数中应该没有异常。
  • 过于复杂的任务(例如通过网络发送消息)可以放在单独的方法中,类用户必须在销毁之前调用它们。
  • 如果析构函数中存在异常,则最好记录它而不是隐藏它(如果 logger 可用)。
  • 在简单的应用程序中,依赖于std::terminate(对于C++ 11中默认情况下为 noexcept 的情况)来处理异常是可以接受的。

6. 匿名代码块。

您可以在单个函数内创建单独的代码块,以使某些变量成为局部变量,以便在退出块时调用析构函数。

  1. Block block = data.in->read();
  2. {
  3. std::lock_guard<std::mutex> lock(mutex);
  4. data.ready = true;
  5. data.block = block;
  6. }
  7. ready_any.set();

7. 多线程。

在离线数据处理程序中:

  • 尝试在单个CPU核心上获得最佳性能。 然后,您可以根据需要并行化代码。

在服务端应用中:

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

Fork不用于并行化。

8. 同步线程。

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

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

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

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

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

9. 指针和引用。

大部分情况下,请用引用。

10. 常量。

使用 const 引用、指针,指向常量、const_iterator和 const 方法。

const 视为默认值,仅在必要时使用非 const

当按值传递变量时,使用 const 通常没有意义。

11. 无符号。

必要时使用unsigned

12. 数值类型。

使用 UInt8UInt16UInt32UInt64Int8Int16Int32Int64,同样还有 size_tssize_tptrdiff_t

不要使用这些类型:signed / unsigned longlong longshortsigned / unsigned charchar

13. 参数传递。

通过引用传递复杂类型 (包括 std::string)。

如果函数中传递堆中创建的对象,则使参数类型为 shared_ptr 或者 unique_ptr.

14. 返回值

大部分情况下使用 return。不要使用 return std::move(res)

如果函数在堆上分配对象并返回它,请使用 shared_ptrunique_ptr

在极少数情况下,您可能需要通过参数返回值。 在这种情况下,参数应该是引用传递的。

  1. using AggregateFunctionPtr = std::shared_ptr<IAggregateFunction>;
  2. /** Allows creating an aggregate function by its name.
  3. */
  4. class AggregateFunctionFactory
  5. {
  6. public:
  7. AggregateFunctionFactory();
  8. AggregateFunctionPtr get(const String & name, const DataTypes & argument_types) const;

15. 命名空间。

没有必要为应用程序代码使用单独的 namespace

小型库也不需要这个。

对于中大型库,须将所有代码放在 namespace 中。

在库的 .h 文件中,您可以使用 namespace detail 来隐藏应用程序代码不需要的实现细节。

.cpp 文件中,您可以使用 static 或匿名命名空间来隐藏符号。

同样 namespace 可用于 enum 以防止相应的名称落入外部 namespace(但最好使用enum class)。

16. 延迟初始化。

如果初始化需要参数,那么通常不应该编写默认构造函数。

如果稍后您需要延迟初始化,则可以添加将创建无效对象的默认构造函数。 或者,对于少量对象,您可以使用 shared_ptr / unique_ptr

  1. Loader(DB::Connection * connection_, const std::string & query, size_t max_block_size_);
  2. /// For deferred initialization
  3. Loader() {}

17. 虚函数。

如果该类不是用于多态使用,则不需要将函数设置为虚拟。这也适用于析构函数。

18. 编码。

在所有情况下使用 UTF-8 编码。使用 std::stringchar *。不要使用 std::wstringwchar_t

19. 日志。

请参阅代码中的示例。

在提交之前,删除所有无意义和调试日志记录,以及任何其他类型的调试输出。

应该避免循环记录日志,即使在 Trace 级别也是如此。

日志必须在任何日志记录级别都可读。

在大多数情况下,只应在应用程序代码中使用日志记录。

日志消息必须用英文写成。

对于系统管理员来说,日志最好是可以理解的。

不要在日志中使用亵渎语言。

在日志中使用UTF-8编码。 在极少数情况下,您可以在日志中使用非ASCII字符。

20. 输入-输出。

不要使用 iostreams 在对应用程序性能至关重要的内部循环中(并且永远不要使用 stringstream )。

使用 DB/IO 库替代。

21. 日期和时间。

参考 DateLUT 库。

22. 引入头文件。

一直用 #pragma once 而不是其他宏。

23. using 语法

using namespace 不会被使用。 您可以使用特定的 using。 但是在类或函数中使它成为局部的。

24. 不要使用 trailing return type 为必要的功能。

  1. auto f() -> void

25. 声明和初始化变量。

  1. //right way
  2. std::string s = "Hello";
  3. std::string s{"Hello"};
  4. //wrong way
  5. auto s = std::string{"Hello"};

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

没有用到的 C++ 特性。

1. 不使用虚拟继承。

2. 不使用 C++03 中的异常标准。

平台

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

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

2. 语言: C++20.

3. 编译器: clang。 此时(2021年03月),代码使用11版编译。(它也可以使用gcc 编译 but it is not suitable for production)

使用标准库 (libc++)。

4. 操作系统:Linux Ubuntu,不比 Precise 早。

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

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

6. 使用 -Wall -Wextra -Werror 编译参数。

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. 未使用的代码将从 repo 中删除。

1. The C++20 standard library is used (experimental extensions are allowed), as well as boost and Poco frameworks.

2. It is not allowed to use libraries from OS packages. It is also not allowed to use pre-installed libraries. All libraries should be placed in form of source code in contrib directory and built with ClickHouse.

3. Preference is always given to libraries that are already in use.

一般建议

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. 函数的多行参数。

允许以下任何包装样式:

  1. function(
  2. T1 x1,
  3. T2 x2)
  1. function(
  2. size_t left, size_t right,
  3. const & RangesInDataParts ranges,
  4. size_t limit)
  1. function(size_t left, size_t right,
  2. const & RangesInDataParts ranges,
  3. size_t limit)
  1. function(size_t left, size_t right,
  2. const & RangesInDataParts ranges,
  3. size_t limit)
  1. function(
  2. size_t left,
  3. size_t right,
  4. const & RangesInDataParts ranges,
  5. size_t limit)