LLVM 编码规范¶
简介¶
本文档描述了 LLVM 项目中使用的编码规范。虽然任何编码规范都不应被视为在所有情况下都必须遵守的绝对要求,但对于遵循基于库的设计(如 LLVM)的大规模代码库而言,编码规范尤为重要。
虽然本文档可能为某些机械格式化问题、空白字符或其他 “微观细节” 提供指导,但这些并非固定的标准。始终遵循黄金法则
如果您正在扩展、增强或修复已实现的代码,请使用已使用的风格,以使源代码统一且易于理解。
请注意,某些代码库(例如 libc++
)有特殊原因偏离编码规范。例如,在 libc++
的情况下,这是因为命名和其他约定由 C++ 标准规定。
代码库中存在一些未统一遵循的约定(例如,命名约定)。这是因为它们相对较新,并且在它们制定之前已经编写了大量代码。我们的长期目标是使整个代码库都遵循约定,但我们明确 *不希望* 补丁对现有代码进行大规模的重新格式化。另一方面,如果您即将以某种其他方式更改类的方法,则重命名这些方法是合理的。请单独提交此类更改,以便更轻松地进行代码审查。
这些指南的最终目标是提高我们通用代码库的可读性和可维护性。
语言、库和标准¶
LLVM 和其他使用这些编码规范的 LLVM 项目中的大多数源代码都是 C++ 代码。在某些地方使用了 C 代码,这要么是由于环境限制、历史限制,要么是由于导入到树中的第三方源代码。总的来说,我们偏好符合标准、现代且可移植的 C++ 代码作为首选的实现语言。
对于自动化、构建系统和实用程序脚本,Python 是首选,并且已在 LLVM 仓库中广泛使用。
C++ 标准版本¶
除非另有说明,否则 LLVM 子项目使用标准 C++17 代码编写,并避免不必要的供应商特定扩展。
尽管如此,我们将自己限制在主要工具链中可用的功能范围内,这些工具链作为主机编译器得到支持(请参阅 LLVM 系统入门 页面,“软件” 部分)。
每个工具链都为它接受的内容提供了良好的参考
此外,在 cppreference.com 上还有支持的 C++ 功能的编译器比较表。
C++ 标准库¶
当 C++ 标准库工具或 LLVM 支持库可用于特定任务时,我们鼓励使用它们,而不是实现自定义数据结构。LLVM 和相关项目尽可能强调和依赖标准库工具和 LLVM 支持库。
LLVM 支持库(例如,ADT)实现了标准库中缺少的专用数据结构或功能。此类库通常在 llvm
命名空间中实现,并在存在标准接口时遵循预期的标准接口。
当 C++ 和 LLVM 支持库都提供类似的功能,并且没有特定理由偏爱 C++ 实现时,通常最好使用 LLVM 库。例如,llvm::DenseMap
几乎总是应该代替 std::map
或 std::unordered_map
使用,而 llvm::SmallVector
通常应代替 std::vector
使用。
我们明确避免使用某些标准工具,例如 I/O 流,而是使用 LLVM 的流库 (raw_ostream)。有关这些主题的更多详细信息,请参阅 LLVM 程序员手册。
有关 LLVM 数据结构及其权衡的更多信息,请查阅 程序员手册的那一部分。
Python 版本和源代码格式化¶
当前所需的最低 Python 版本记录在 LLVM 系统入门 部分中。LLVM 仓库中的 Python 代码应仅使用此 Python 版本中可用的语言功能。
LLVM 仓库中的 Python 代码应遵守 PEP 8 中概述的格式化指南。
为了保持一致性并限制变更,代码应使用 black 实用程序自动格式化,该实用程序符合 PEP 8 标准。使用其默认规则。例如,即使 --line-length
未默认设置为 80,也应避免指定它。默认规则可能在 black 的主要版本之间发生变化。为了避免格式化规则中不必要的变更,我们目前在 LLVM 中使用 black 版本 23.x。
当贡献与格式化无关的补丁时,您应该仅格式化补丁修改的 Python 代码。为此,请使用 darker 实用程序,该实用程序仅对修改后的 Python 代码运行默认的 black 规则。这样做应确保补丁通过 LLVM 预提交 CI 中的 Python 格式检查,该检查也使用 darker。当专门为重新格式化 Python 文件贡献补丁时,请使用 black,它目前仅支持格式化整个文件。
以下是一些快速示例,但有关详细信息,请参阅 black 和 darker 文档
$ pip install black=='23.*' darker # install black 23.x and darker
$ darker test.py # format uncommitted changes
$ darker -r HEAD^ test.py # also format changes from last commit
$ black test.py # format entire file
您可以为 darker 指定目录而不是单个文件名,它将查找已更改的文件。但是,如果目录很大(例如 LLVM 仓库的克隆),darker 可能会非常慢。在这种情况下,您可能希望使用 git 列出已更改的文件。例如
$ darker -r HEAD^ $(git diff --name-only --diff-filter=d HEAD^)
机械源代码问题¶
源代码格式化¶
注释¶
注释对于可读性和可维护性非常重要。编写注释时,请将其写成英文散文,使用正确的 capitalization、标点符号等。旨在描述代码试图做什么以及为什么这样做,而不是在微观层面描述 *如何* 做。以下是一些需要记录的重要事项
文件头¶
每个源文件都应在其上有一个标头,描述文件的基本用途。标准标头如下所示
//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.net.cn/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
///
/// \file
/// This file contains the declaration of the Instruction class, which is the
/// base class for all of the VM instructions.
///
//===----------------------------------------------------------------------===//
文件中的第一部分是一个简洁的注释,定义了文件发布的许可证。这可以清楚地表明源代码可以在哪些条款下分发,并且不应以任何方式修改。
主体是一个 Doxygen 注释(由 ///
注释标记而不是通常的 //
标识),描述文件的用途。第一句话(或以 \brief
开头的段落)用作摘要。任何其他信息都应以空行分隔。如果算法基于论文或在其他来源中描述,请提供参考。
头文件保护¶
头文件的保护应是用户将 #include
的全大写路径,使用 ‘_’ 代替路径分隔符和扩展名标记。例如,头文件 llvm/include/llvm/Analysis/Utils/Local.h
将作为 #include
-ed #include "llvm/Analysis/Utils/Local.h"
,因此其保护是 LLVM_ANALYSIS_UTILS_LOCAL_H
。
类概述¶
类是面向对象设计的基本组成部分。因此,类定义应具有注释块,解释该类的用途及其工作原理。每个非平凡的类都应具有 doxygen
注释块。
方法信息¶
方法和全局函数也应记录在案。简要说明其作用以及边缘情况的描述就足够了。读者应该能够在不阅读代码本身的情况下理解如何使用接口。
此处要讨论的好处是当发生意外情况时会发生什么,例如,该方法是否返回 null?
Doxygen 在文档注释中的使用¶
使用 \file
命令将标准文件头转换为文件级注释。
为所有公共接口(公共类、成员和非成员函数)包含描述性段落。避免重复可以从 API 名称推断出的信息。第一句话(或以 \brief
开头的段落)用作摘要。尽量使用一个句子作为 \brief
,以避免视觉混乱。将详细讨论放在单独的段落中。
要在段落中引用参数名称,请使用 \p name
命令。不要使用 \arg name
命令,因为它会启动一个新段落,其中包含参数的文档。
将非内联代码示例包装在 \code ... \endcode
中。
要记录函数参数,请使用 \param name
命令开始一个新段落。如果参数用作 out 或 in/out 参数,请分别使用 \param [out] name
或 \param [in,out] name
命令。
要描述函数返回值,请使用 \returns
命令开始一个新段落。
最简文档注释
/// Sets the xyzzy property to \p Baz.
void setXyzzy(bool Baz);
以首选方式使用所有 Doxygen 功能的文档注释
/// Does foo and bar.
///
/// Does not do foo the usual way if \p Baz is true.
///
/// Typical usage:
/// \code
/// fooBar(false, "quux", Res);
/// \endcode
///
/// \param Quux kind of foo to do.
/// \param [out] Result filled with bar sequence on foo success.
///
/// \returns true on success.
bool fooBar(bool Baz, StringRef Quux, std::vector<int> &Result);
不要在头文件和实现文件中重复文档注释。将公共 API 的文档注释放在头文件中。私有 API 的文档注释可以放在实现文件中。在任何情况下,实现文件都可以包含额外的注释(不一定使用 Doxygen 标记)以根据需要解释实现细节。
不要在注释开头重复函数或类名。对于人类来说,很明显正在记录哪个函数或类;自动文档处理工具足够智能,可以将注释绑定到正确的声明。
避免
// Example.h:
// example - Does something important.
void example();
// Example.cpp:
// example - Does something important.
void example() { ... }
首选
// Example.h:
/// Does something important.
void example();
// Example.cpp:
/// Builds a B-tree in order to do foo. See paper by...
void example() { ... }
错误和警告消息¶
清晰的诊断消息对于帮助用户识别和修复其输入中的问题非常重要。使用简洁但正确的英文散文,为用户提供理解问题所在所需的上下文。此外,为了匹配其他工具常用的错误消息风格,请以小写字母开始第一句话,并在最后一句话末尾不加句点(如果原本会以句点结尾)。以不同标点符号结尾的句子,例如 “did you forget ‘;’?”,仍应这样做。
例如,这是一个好的错误消息
error: file.o: section header 3 is corrupt. Size is 10 when it should be 20
这是一个糟糕的消息,因为它没有提供有用的信息并且使用了错误的风格
error: file.o: Corrupt section header.
与其他编码规范一样,各个项目(例如 Clang 静态分析器)可能具有不符合此规范的预先存在的风格。如果整个项目始终如一地使用不同的格式化方案,请改用该风格。否则,此标准适用于所有 LLVM 工具,包括 clang、clang-tidy 等。
如果工具或项目没有现有的函数来发出警告或错误,请使用 Support/WithColor.h
中提供的错误和警告处理程序,以确保它们以适当的风格打印,而不是直接打印到 stderr。
使用 report_fatal_error
时,请遵循与常规错误消息相同的消息标准。断言消息和 llvm_unreachable
调用不一定需要遵循相同的风格,因为它们是自动格式化的,因此这些指南可能不适用。
#include
风格¶
紧跟在 头文件注释 之后(以及如果正在处理头文件,则为包含保护),应列出文件所需的 最小 #include 列表。我们更喜欢按以下顺序排列这些 #include
主模块头文件
本地/私有头文件
LLVM 项目/子项目头文件(
clang/...
,lldb/...
,llvm/...
等)系统
#include
每个类别都应按完整路径按字典顺序排序。
主模块头文件 适用于实现由 .h
文件定义的接口的 .cpp
文件。无论此 #include
在文件系统上的哪个位置,都应始终 首先 包含它。通过在实现接口的 .cpp
文件中首先包含头文件,我们确保头文件没有任何隐藏的依赖项,这些依赖项未在头文件中显式 #include
,但应该是。它也是 .cpp
文件中的一种文档形式,用于指示其实现的接口的定义位置。
LLVM 项目和子项目头文件应从最具体到最不具体进行分组,原因与上述相同。例如,LLDB 依赖于 clang 和 LLVM,而 clang 依赖于 LLVM。因此,LLDB 源文件应首先包含 lldb
头文件,然后是 clang
头文件,然后是 llvm
头文件,以减少(例如)LLDB 头文件由于先前在主源文件或某些较早的头文件中包含该头文件而意外拾取丢失的包含的可能性。 clang 应该类似地在其包含 llvm 头文件之前包含自己的头文件。此规则适用于所有 LLVM 子项目。
源代码宽度¶
将您的代码编写为适合 80 列宽度。
代码的宽度必须有一些限制,以便开发人员可以在适度的显示器上的窗口中并排显示多个文件。如果您要选择宽度限制,则它在某种程度上是任意的,但您不妨选择一些标准的东西。例如,使用 90 列而不是 80 列不会增加任何显着价值,并且不利于打印代码。此外,许多其他项目已将 80 列标准化,因此有些人已经为其配置了编辑器(而不是其他列,例如 90 列)。
空白字符¶
在所有情况下,源文件中都首选空格而不是制表符。人们对首选的缩进级别和他们喜欢的缩进样式有不同的看法;这很好。不好的是不同的编辑器/查看器将制表符扩展为不同的制表符停止位。这可能会导致您的代码看起来完全无法阅读,并且不值得处理。
与往常一样,请遵循上面的 黄金法则:如果您正在修改和扩展现有代码,请遵循现有代码的风格。
不要添加尾随空格。某些常用编辑器会在保存文件时自动删除尾随空格,这会导致不相关的更改出现在差异和提交中。
像代码块一样格式化 Lambda 表达式¶
格式化多行 lambda 表达式时,请像格式化代码块一样格式化它。如果语句中只有一个多行 lambda 表达式,并且语句中在该表达式之后没有词法表达式,则将缩进减少到代码块的标准两个空格缩进,就好像它是语句的前一部分打开的 if 块一样
std::sort(foo.begin(), foo.end(), [&](Foo a, Foo b) -> bool {
if (a.blah < b.blah)
return true;
if (a.baz < b.baz)
return true;
return a.bam < b.bam;
});
为了充分利用此格式,如果您正在设计一个接受延续或单个可调用参数(无论是函数对象还是 std::function
)的 API,则应尽可能将其作为最后一个参数。
如果语句中有多个多行 lambda 表达式,或者在 lambda 表达式之后有其他参数,则将块从 []
的缩进处缩进两个空格
dyn_switch(V->stripPointerCasts(),
[] (PHINode *PN) {
// process phis...
},
[] (SelectInst *SI) {
// process selects...
},
[] (LoadInst *LI) {
// process loads...
},
[] (AllocaInst *AI) {
// process allocas...
});
带花括号的初始化列表¶
从 C++11 开始,有更多使用带花括号的列表来执行初始化的例子。例如,它们可以用于在表达式中构造聚合临时变量。现在,它们有一种自然的方式来最终嵌套在彼此内部和函数调用内部,以便从局部变量构建聚合(例如选项结构)。
历史上常见的聚合变量的带花括号的初始化的格式化与深度嵌套、通用表达式上下文、函数参数和 lambda 表达式不能很好地混合使用。我们建议新代码使用一个简单的规则来格式化带花括号的初始化列表:就像花括号是函数调用中的括号一样。格式化规则与已经很好理解的嵌套函数调用的格式化规则完全匹配。示例
foo({a, b, c}, {1, 2, 3});
llvm::Constant *Mask[] = {
llvm::ConstantInt::get(llvm::Type::getInt32Ty(getLLVMContext()), 0),
llvm::ConstantInt::get(llvm::Type::getInt32Ty(getLLVMContext()), 1),
llvm::ConstantInt::get(llvm::Type::getInt32Ty(getLLVMContext()), 2)};
这种格式化方案还使得使用 Clang Format 等工具获得可预测、一致和自动的格式化变得特别容易。
语言和编译器问题¶
像对待错误一样对待编译器警告¶
编译器警告通常很有用,有助于改进代码。那些无用的警告,通常可以通过小的代码更改来抑制。例如,if
条件中的赋值通常是拼写错误
if (V = getValue()) {
...
}
多个编译器将为上面的代码打印警告。可以通过添加括号来抑制它
if ((V = getValue())) {
...
}
编写可移植代码¶
在几乎所有情况下,都可以编写完全可移植的代码。当您需要依赖不可移植的代码时,请将其置于明确定义且文档完善的接口之后。
不要使用 RTTI 或异常¶
为了减少代码和可执行文件的大小,LLVM 不使用异常或 RTTI(运行时类型信息,例如,dynamic_cast<>
)。
也就是说,LLVM 确实广泛使用手写的 RTTI 形式,该形式使用诸如 isa<>、cast<> 和 dyn_cast<> 之类的模板。这种形式的 RTTI 是可选择加入的,可以 添加到任何类。
优先使用 C++ 风格的类型转换¶
进行类型转换时,请使用 static_cast
、reinterpret_cast
和 const_cast
,而不是 C 风格的类型转换。对此有两个例外
当转换为
void
以抑制关于未使用变量的警告时(作为[[maybe_unused]]
的替代方案)。在这种情况下,优先使用 C 风格的转换。当在整型类型(包括非强类型枚举)之间进行转换时,允许使用函数式风格的转换作为
static_cast
的替代方案。
不要使用静态构造函数¶
不应向代码库添加静态构造函数和析构函数(例如,类型具有构造函数或析构函数的全局变量),并且应尽可能删除。
不同源文件中的全局变量以任意顺序初始化,这使得代码更难以推理。
静态构造函数对将 LLVM 用作库的程序的启动时间有负面影响。我们非常希望将额外的 LLVM 目标或其他库链接到应用程序中没有成本,但静态构造函数破坏了这个目标。
使用 class
和 struct
关键字¶
在 C++ 中,class
和 struct
关键字几乎可以互换使用。唯一的区别是当它们用于声明类时:class
默认使所有成员为私有,而 struct
默认使所有成员为公共。
给定
class
或struct
的所有声明和定义都必须使用相同的关键字。例如
// Avoid if `Example` is defined as a struct.
class Example;
// OK.
struct Example;
struct Example { ... };
当所有成员都被声明为公共时,应使用
struct
。
// Avoid using `struct` here, use `class` instead.
struct Foo {
private:
int Data;
public:
Foo() : Data(0) { }
int getData() const { return Data; }
void setData(int D) { Data = D; }
};
// OK to use `struct`: all members are public.
struct Bar {
int Data;
Bar() : Data(0) { }
};
不要使用花括号初始化列表来调用构造函数¶
从 C++11 开始,有一种“通用初始化语法”,允许使用花括号初始化列表调用构造函数。不要使用这些来调用具有非平凡逻辑的构造函数,或者如果您关心您正在调用某个特定的构造函数。这些应该看起来像使用括号的函数调用,而不是像聚合初始化。 同样,如果您需要显式命名类型并调用其构造函数来创建临时对象,请不要使用花括号初始化列表。相反,在进行聚合初始化或概念上等效的操作时,使用花括号初始化列表(对于临时对象不带任何类型)。示例
class Foo {
public:
// Construct a Foo by reading data from the disk in the whizbang format, ...
Foo(std::string filename);
// Construct a Foo by looking up the Nth element of some global data ...
Foo(int N);
// ...
};
// The Foo constructor call is reading a file, don't use braces to call it.
std::fill(foo.begin(), foo.end(), Foo("name"));
// The pair is being constructed like an aggregate, use braces.
bar_map.insert({my_key, my_value});
如果您在使用花括号初始化列表初始化变量时,请在左花括号前使用等号
int data[] = {0, 1, 2, 3};
使用 auto
类型推导以使代码更具可读性¶
有些人提倡在 C++11 中“几乎总是 auto
”的策略,然而 LLVM 采用更温和的立场。当且仅当 auto
使代码更具可读性或更易于维护时才使用它。不要“几乎总是”使用 auto
,但在使用像 cast<Foo>(...)
这样的初始化器或类型从上下文中已经很明显的其他地方时,请使用 auto
。当类型无论如何都会被抽象出来时,auto
在这些目的中也效果很好,通常在容器的 typedef 之后,例如 std::vector<T>::iterator
。
类似地,C++14 添加了泛型 lambda 表达式,其中参数类型可以是 auto
。在您会使用模板的地方使用这些。
注意使用 auto
造成的不必要的复制¶
auto
的便利性很容易让人忘记它的默认行为是复制。特别是在基于范围的 for
循环中,不小心复制的代价很高。
对值使用 auto &
,对指针使用 auto *
,除非您需要进行复制。
// Typically there's no reason to copy.
for (const auto &Val : Container) observe(Val);
for (auto &Val : Container) Val.change();
// Remove the reference if you really want a new copy.
for (auto Val : Container) { Val.change(); saveSomewhere(Val); }
// Copy pointers, but make it clear that they're pointers.
for (const auto *Ptr : Container) observe(*Ptr);
for (auto *Ptr : Container) Ptr->change();
注意由于指针排序引起的不确定性¶
一般来说,指针之间没有相对顺序。因此,当无序容器(如集合和映射)与指针键一起使用时,迭代顺序是未定义的。因此,迭代此类容器可能会导致不确定的代码生成。虽然生成的代码可能可以正确工作,但不确定性会使重现错误和调试编译器变得更加困难。
如果期望有序结果,请记住在迭代之前对无序容器进行排序。或者,如果您想迭代指针键,请使用有序容器,如 vector
/MapVector
/SetVector
。
注意相等元素的不确定排序顺序¶
std::sort
使用非稳定排序算法,其中不保证保留相等元素的顺序。因此,对具有相等元素的容器使用 std::sort
可能会导致不确定的行为。为了揭示这种不确定性的实例,LLVM 引入了一个新的 llvm::sort 包装函数。对于 EXPENSIVE_CHECKS 构建,这将在排序之前随机打乱容器。默认情况下,使用 llvm::sort
而不是 std::sort
。
风格问题¶
高层次问题¶
自包含头文件¶
头文件应该是自包含的(可以独立编译)并且以 .h
结尾。旨在包含的非头文件应以 .inc
结尾,并应谨慎使用。
所有头文件都应该是自包含的。用户和重构工具不应为了包含头文件而遵守特殊条件。具体来说,头文件应该有头文件保护符,并包含它需要的所有其他头文件。
在极少数情况下,设计为包含的文件不是自包含的。这些文件通常旨在包含在不寻常的位置,例如另一个文件的中间。它们可能不使用头文件保护符,也可能不包含其先决条件。用 .inc 扩展名命名此类文件。谨慎使用,并在可能的情况下首选自包含头文件。
一般来说,头文件应该由一个或多个 .cpp
文件实现。每个 .cpp
文件都应该首先包含定义其接口的头文件。这确保了头文件的所有依赖项都已正确添加到头文件本身,而不是隐式的。对于翻译单元,系统头文件应在用户头文件之后包含。
库分层¶
头文件目录(例如 include/llvm/Foo
)定义了一个库(Foo
)。一个库(包括其头文件和实现)应该只使用其依赖项列表中列出的库中的内容。
经典 Unix 链接器可以强制执行此约束的某些部分(Mac 和 Windows 链接器以及 lld 不强制执行此约束)。Unix 链接器从左到右搜索其命令行上指定的库,并且永远不会重新访问库。这样,库之间就不可能存在循环依赖关系。
这并不能完全强制执行所有库间依赖关系,重要的是不能强制执行由内联函数创建的头文件循环依赖关系。回答“这个分层是否正确”的一个好方法是考虑如果所有内联函数都被定义为非内联,Unix 链接器是否会成功链接程序。(& 对于依赖项的所有有效排序 - 由于链接解析是线性的,因此一些隐式依赖项可能会偷偷溜过:A 依赖于 B 和 C,因此有效的排序是“C B A”或“B C A”,在这两种情况下,显式依赖项都出现在它们的使用之前。但在第一种情况下,如果 B 隐式依赖于 C,或者在第二种情况下相反,B 仍然可以成功链接)
#include
尽可能少¶
#include
会损害编译时间性能。除非必须这样做,否则不要这样做,尤其是在头文件中。
但是等等!有时您需要拥有类的定义才能使用它,或者从它继承。在这些情况下,继续 #include
该头文件。但是请注意,在许多情况下,您不需要拥有类的完整定义。如果您正在使用指向类的指针或引用,则不需要头文件。如果您只是从原型函数或方法返回类实例,则不需要它。事实上,在大多数情况下,您根本不需要类的定义。不 #include
可以加快编译速度。
然而,很容易尝试过度使用此建议。您必须包含您正在使用的所有头文件——您可以直接或间接地通过另一个头文件包含它们。为了确保您不会意外忘记在模块头文件中包含头文件,请确保在实现文件中首先包含您的模块头文件(如上所述)。这样就不会有任何隐藏的依赖关系,您稍后才会发现。
保持“内部”头文件私有¶
许多模块具有复杂的实现,这使得它们使用多个实现 (.cpp
) 文件。通常很想将内部通信接口(辅助类、额外函数等)放在公共模块头文件中。不要这样做!
如果您真的需要这样做,请将私有头文件放在与源文件相同的目录中,并在本地包含它。这确保了您的私有接口保持私有,并且不受外部人员的干扰。
注意
将额外的实现方法放在公共类本身中是可以的。只需使它们成为私有(或受保护)的,一切都很好。
使用命名空间限定符来实现先前声明的函数¶
在源文件中提供函数的行外实现时,请勿在源文件中打开命名空间块。相反,使用命名空间限定符来帮助确保您的定义与现有声明匹配。这样做
// Foo.h
namespace llvm {
int foo(const char *s);
}
// Foo.cpp
#include "Foo.h"
using namespace llvm;
int llvm::foo(const char *s) {
// ...
}
这样做有助于避免定义与头文件中的声明不匹配的错误。例如,以下 C++ 代码定义了 llvm::foo
的新重载,而不是为头文件中声明的现有函数提供定义
// Foo.cpp
#include "Foo.h"
namespace llvm {
int foo(char *s) { // Mismatch between "const char *" and "char *"
}
} // namespace llvm
直到构建即将完成时,当链接器找不到任何原始函数用法的定义时,才会捕获此错误。如果函数改为使用命名空间限定符定义,则在编译定义时会立即捕获错误。
类方法实现必须已经命名类,并且不能在线外引入新的重载,因此此建议不适用于它们。
使用提前退出和 continue
来简化代码¶
在阅读代码时,请记住读者必须记住多少状态和多少先前的决定才能理解代码块。尽可能减少缩进,只要它不会使代码更难以理解。实现这一目标的绝佳方法之一是在长循环中利用提前退出和 continue
关键字。考虑一下这段不使用提前退出的代码
Value *doSomething(Instruction *I) {
if (!I->isTerminator() &&
I->hasOneUse() && doOtherThing(I)) {
... some long code ....
}
return 0;
}
如果 'if'
的主体很大,则此代码存在几个问题。当您查看函数的顶部时,并不立即清楚这仅对非终止符指令执行有趣的操作,并且仅适用于具有其他谓词的事物。其次,相对难以描述(在注释中)为什么这些谓词很重要,因为 if
语句使得难以布置注释。第三,当您深入代码主体时,它会额外缩进一级。最后,在阅读函数的顶部时,不清楚如果谓词不为真,结果是什么;您必须阅读到函数末尾才能知道它返回 null。
最好将代码格式化为这样
Value *doSomething(Instruction *I) {
// Terminators never need 'something' done to them because ...
if (I->isTerminator())
return 0;
// We conservatively avoid transforming instructions with multiple uses
// because goats like cheese.
if (!I->hasOneUse())
return 0;
// This is really just here for example.
if (!doOtherThing(I))
return 0;
... some long code ....
}
这解决了这些问题。类似的问题经常发生在 for
循环中。一个愚蠢的例子是这样的
for (Instruction &I : BB) {
if (auto *BO = dyn_cast<BinaryOperator>(&I)) {
Value *LHS = BO->getOperand(0);
Value *RHS = BO->getOperand(1);
if (LHS != RHS) {
...
}
}
}
当您的循环非常非常小时,这种结构很好。但是如果它超过 10-15 行,人们就很难一目了然地阅读和理解它。这种代码的问题在于它很快变得非常嵌套。这意味着代码的读者必须在他们的大脑中保留很多上下文,以记住循环中立即发生的事情,因为他们不知道 if
条件是否会有 else
等等。强烈建议像这样构造循环
for (Instruction &I : BB) {
auto *BO = dyn_cast<BinaryOperator>(&I);
if (!BO) continue;
Value *LHS = BO->getOperand(0);
Value *RHS = BO->getOperand(1);
if (LHS == RHS) continue;
...
}
这具有为函数使用提前退出的所有好处:它减少了循环的嵌套,更容易描述条件为真的原因,并且读者可以清楚地看到没有 else
即将到来,他们必须将上下文推入他们的大脑。如果循环很大,这可能是一个很大的可理解性优势。
在 return
之后不要使用 else
¶
出于与上述类似的原因(减少缩进和更容易阅读),请不要在中断控制流的内容之后使用 'else'
或 'else if'
——例如 return
、break
、continue
、goto
等。例如
case 'J': {
if (Signed) {
Type = Context.getsigjmp_bufType();
if (Type.isNull()) {
Error = ASTContext::GE_Missing_sigjmp_buf;
return QualType();
} else {
break; // Unnecessary.
}
} else {
Type = Context.getjmp_bufType();
if (Type.isNull()) {
Error = ASTContext::GE_Missing_jmp_buf;
return QualType();
} else {
break; // Unnecessary.
}
}
}
最好这样写
case 'J':
if (Signed) {
Type = Context.getsigjmp_bufType();
if (Type.isNull()) {
Error = ASTContext::GE_Missing_sigjmp_buf;
return QualType();
}
} else {
Type = Context.getjmp_bufType();
if (Type.isNull()) {
Error = ASTContext::GE_Missing_jmp_buf;
return QualType();
}
}
break;
或者更好的是(在这种情况下)如下
case 'J':
if (Signed)
Type = Context.getsigjmp_bufType();
else
Type = Context.getjmp_bufType();
if (Type.isNull()) {
Error = Signed ? ASTContext::GE_Missing_sigjmp_buf :
ASTContext::GE_Missing_jmp_buf;
return QualType();
}
break;
这个想法是减少缩进和您在阅读代码时必须跟踪的代码量。
注意:此建议不适用于 constexpr if
语句。else
子句的子语句可能是丢弃的语句,因此删除 else
可能会导致意外的模板实例化。因此,以下示例是正确的
template<typename T>
static constexpr bool VarTempl = true;
template<typename T>
int func() {
if constexpr (VarTempl<T>)
return 1;
else
static_assert(!VarTempl<T>);
}
将谓词循环转换为谓词函数¶
编写仅计算布尔值的小循环非常常见。人们通常有多种编写这些循环的方式,但这种事物的一个例子是
bool FoundFoo = false;
for (unsigned I = 0, E = BarList.size(); I != E; ++I)
if (BarList[I]->isFoo()) {
FoundFoo = true;
break;
}
if (FoundFoo) {
...
}
我们更喜欢使用谓词函数(可能是静态的),而不是这种循环,该谓词函数使用提前退出
/// \returns true if the specified list has an element that is a foo.
static bool containsFoo(const std::vector<Bar*> &List) {
for (unsigned I = 0, E = List.size(); I != E; ++I)
if (List[I]->isFoo())
return true;
return false;
}
...
if (containsFoo(BarList)) {
...
}
这样做有很多原因:它减少了缩进并分解了代码,这些代码通常可以由检查相同谓词的其他代码共享。更重要的是,它迫使您为函数选择一个名称,并迫使您为其编写注释。在这个愚蠢的例子中,这并没有增加太多价值。但是,如果条件很复杂,这可以使读者更容易理解查询此谓词的代码。与其面对我们如何检查 BarList 是否包含 foo 的内联细节,我们不如信任函数名称并继续以更好的局部性阅读。
低层次问题¶
正确命名类型、函数、变量和枚举器¶
选择不当的名称可能会误导读者并导致错误。我们再怎么强调使用描述性名称的重要性也不为过。在合理的范围内,选择与底层实体的语义和角色匹配的名称。避免缩写,除非它们是众所周知的。选择好名称后,请确保对名称使用一致的大小写,因为不一致性要求客户端要么记住 API,要么查找以找到确切的拼写。
一般来说,名称应该采用驼峰式命名法(例如 TextFileReader
和 isLValue()
)。不同类型的声明有不同的规则
类型名称(包括类、结构体、枚举、typedef 等)应为名词,并以大写字母开头(例如
TextFileReader
)。变量名称应为名词(因为它们代表状态)。名称应为驼峰式,并以大写字母开头(例如
Leader
或Boats
)。函数名称应为动词短语(因为它们代表动作),并且类似命令的函数应为祈使句。名称应为驼峰式,并以小写字母开头(例如
openFile()
或isFoo()
)。枚举声明(例如
enum Foo {...}
)是类型,因此它们应遵循类型的命名约定。枚举的常见用途是用作联合的鉴别符或子类的指示符。当枚举用于类似用途时,它应该有一个Kind
后缀(例如ValueKind
)。枚举器(例如
enum { Foo, Bar }
)和 公共成员变量 应以大写字母开头,就像类型一样。除非枚举器在它们自己的小命名空间或类内部定义,否则枚举器应具有与枚举声明名称对应的前缀。例如,enum ValueKind { ... };
可能包含诸如VK_Argument
、VK_BasicBlock
等枚举器。仅作为便利常量的枚举器免于前缀要求。例如enum { MaxSize = 42, Density = 12 };
作为例外,模仿 STL 类的类可以具有 STL 风格的成员名称,即用下划线分隔的小写单词(例如 begin()
、push_back()
和 empty()
)。提供多个迭代器的类应向 begin()
和 end()
添加单数前缀(例如 global_begin()
和 use_begin()
)。
以下是一些示例
class VehicleMaker {
...
Factory<Tire> F; // Avoid: a non-descriptive abbreviation.
Factory<Tire> Factory; // Better: more descriptive.
Factory<Tire> TireFactory; // Even better: if VehicleMaker has more than one
// kind of factories.
};
Vehicle makeVehicle(VehicleType Type) {
VehicleMaker M; // Might be OK if scope is small.
Tire Tmp1 = M.makeTire(); // Avoid: 'Tmp1' provides no information.
Light Headlight = M.makeLight("head"); // Good: descriptive.
...
}
大量使用断言¶
充分利用“assert
”宏。检查您的所有先决条件和假设,您永远不知道何时可能会通过断言及早捕获错误(甚至不一定是您的错误),这可以大大减少调试时间。“<cassert>
”头文件可能已经包含在您正在使用的头文件中,因此使用它没有任何成本。
为了进一步协助调试,请确保在断言语句中放入某种错误消息,如果断言被触发,则会打印该消息。这有助于可怜的调试器理解为什么要进行和强制执行断言,并希望知道该怎么做。这是一个完整的例子
inline Value *getOperand(unsigned I) {
assert(I < Operands.size() && "getOperand() out of range!");
return Operands[I];
}
以下是更多示例
assert(Ty->isPointerType() && "Can't allocate a non-pointer type!");
assert((Opcode == Shl || Opcode == Shr) && "ShiftInst Opcode invalid!");
assert(idx < getNumSuccessors() && "Successor # out of range!");
assert(V1.getType() == V2.getType() && "Constant types must be identical!");
assert(isa<PHINode>(Succ->front()) && "Only works on PHId BBs!");
你明白了吧。
过去,断言用于指示不应到达的代码段。这些通常采用以下形式
assert(0 && "Invalid radix for integer literal");
这有一些问题,主要问题是一些编译器可能不理解断言,或者在断言被编译掉的构建中警告缺少返回。
今天,我们有更好的东西:llvm_unreachable
llvm_unreachable("Invalid radix for integer literal");
启用断言后,如果到达该点,它将打印消息,然后退出程序。禁用断言时(即在发布版本中),llvm_unreachable
会成为编译器跳过为此分支生成代码的提示。如果编译器不支持此功能,它将回退到“abort”实现。
使用 llvm_unreachable
标记代码中永远不应到达的特定点。这对于解决关于无法访问的分支等的警告尤其理想,但只要到达特定代码路径是某种类型的无条件错误(不是源自用户输入;请参见下文),就可以使用它。 assert
的使用应始终包含可测试的谓词(而不是 assert(false)
)。
如果错误条件可能由用户输入触发,则应使用LLVM 程序员手册中描述的可恢复错误机制。在不切实际的情况下,可以使用 report_fatal_error
。
另一个问题是,仅由断言使用的值在禁用断言时会产生“未使用值”警告。例如,以下代码会发出警告
unsigned Size = V.size();
assert(Size > 42 && "Vector smaller than it should be");
bool NewToSet = Myset.insert(Value);
assert(NewToSet && "The value shouldn't be in the set yet");
这是两个有趣的不同的案例。在第一种情况下,对 V.size()
的调用仅对断言有用,我们不希望在禁用断言时执行它。像这样的代码应该将调用移动到断言本身中。在第二种情况下,无论是否启用断言,调用的副作用都必须发生。在这种情况下,该值应转换为 void 以禁用警告。具体来说,最好这样编写代码
assert(V.size() > 42 && "Vector smaller than it should be");
bool NewToSet = Myset.insert(Value); (void)NewToSet;
assert(NewToSet && "The value shouldn't be in the set yet");
不要使用 using namespace std
¶
在 LLVM 中,我们更喜欢显式地用 “std::
” 前缀来限定标准命名空间中的所有标识符,而不是依赖于 “using namespace std;
”。
在头文件中,添加 'using namespace XXX'
指令会污染任何 #include
头文件的源文件的命名空间,从而造成维护问题。
在实现文件(例如 .cpp
文件)中,该规则更像是一种风格规则,但仍然很重要。基本上,使用显式命名空间前缀使代码更清晰,因为它立即显而易见正在使用哪些工具以及它们来自哪里。并且更具可移植性,因为 LLVM 代码和其他命名空间之间不会发生命名空间冲突。可移植性规则很重要,因为不同的标准库实现公开不同的符号(可能是它们不应该公开的符号),并且 C++ 标准的未来修订版将向 std
命名空间添加更多符号。因此,我们从不在 LLVM 中使用 'using namespace std;'
。
一般规则的例外(即,对于 std
命名空间来说,这不是例外)是针对实现文件。例如,LLVM 项目中的所有代码都实现了位于 ‘llvm’ 命名空间中的代码。因此,对于 .cpp
文件来说,在 #include
之后,在顶部有一个 'using namespace llvm;'
指令是可以接受的,并且实际上更清晰。这减少了源文件中基于花括号缩进的编辑器的缩进,并保持了概念上下文的清洁。此规则的一般形式是,任何在任何命名空间中实现代码的 .cpp
文件都可以使用该命名空间(及其父命名空间),但不应使用任何其他命名空间。
为头文件中的类提供虚方法锚点¶
如果一个类在头文件中定义,并且有一个虚函数表(vtable)(它有虚方法或它从具有虚方法的类派生),它必须始终在该类中至少有一个外部定义的虚方法。如果没有这个,编译器会将虚函数表和 RTTI 复制到每个 .o
文件中,这些文件 #include
了该头文件,从而膨胀了 .o
文件的大小并增加了链接时间。
不要在完全覆盖枚举的 switch 语句中使用 default 标签¶
如果一个 switch 语句,在没有 default 标签的情况下,对枚举进行操作,但没有覆盖每个枚举值,-Wswitch
会发出警告。如果您在完全覆盖枚举的 switch 语句上编写一个 default 标签,那么当向该枚举添加新元素时,-Wswitch
警告将不会触发。为了帮助避免添加这些类型的 default 标签,Clang 提供了警告 -Wcovered-switch-default
,默认情况下它是关闭的,但在使用支持该警告的 Clang 版本构建 LLVM 时会打开。
这种风格要求的连带效应是,当使用 GCC 构建 LLVM 时,如果您从覆盖枚举的 switch 语句的每个 case 返回,您可能会收到与 “control may reach end of non-void function”(控制可能到达非 void 函数的末尾)相关的警告,因为 GCC 假设枚举表达式可能采用任何可表示的值,而不仅仅是单个枚举器的值。要抑制此警告,请在 switch 语句后使用 llvm_unreachable
。
尽可能使用基于范围的 for
循环¶
C++11 中引入的基于范围的 for
循环意味着很少需要显式操作迭代器。对于所有新添加的代码,我们尽可能使用基于范围的 for
循环。例如
BasicBlock *BB = ...
for (Instruction &I : *BB)
... use I ...
不鼓励使用 std::for_each()
/llvm::for_each()
函数,除非可调用对象已经存在。
不要在循环中每次都评估 end()
¶
在无法使用基于范围的 for
循环并且必须编写显式基于迭代器的循环的情况下,请密切关注是否在每次循环迭代时重新评估 end()
。一个常见的错误是以这种风格编写循环
BasicBlock *BB = ...
for (auto I = BB->begin(); I != BB->end(); ++I)
... use I ...
这种构造的问题在于它在每次循环时都会评估 “BB->end()
”。我们强烈建议循环应该这样写,以便在循环开始之前评估一次。一种方便的方法是这样
BasicBlock *BB = ...
for (auto I = BB->begin(), E = BB->end(); I != E; ++I)
... use I ...
细心的人可能会很快指出这两个循环可能具有不同的语义:如果容器(在本例中是基本块)正在被修改,那么 “BB->end()
” 的值可能在每次循环时都发生变化,而第二个循环实际上可能不正确。如果您实际上依赖于这种行为,请以第一种形式编写循环并添加注释,说明您是有意这样做的。
为什么我们更喜欢第二种形式(在正确的情况下)?以第一种形式编写循环有两个问题。首先,它可能比在循环开始时评估效率更低。在这种情况下,成本可能很小——每次循环时增加一些额外的加载。但是,如果基本表达式更复杂,那么成本可能会迅速上升。我见过循环,其中 end 表达式实际上类似于:“SomeMap[X]->end()
”,而 map 查找真的不便宜。通过始终如一地以第二种形式编写,您可以完全消除这个问题,甚至不必考虑它。
第二个(甚至更大的)问题是,以第一种形式编写循环向读者暗示循环正在修改容器(注释可以方便地确认这一事实!)。如果您以第二种形式编写循环,则即使不查看循环体,也很明显容器没有被修改,这使得更容易阅读代码并理解它的作用。
虽然第二种形式的循环需要多按几个键,但我们确实强烈推荐它。
#include <iostream>
是禁止的¶
在此禁止在库文件中使用 #include <iostream>
,因为许多常见的实现会透明地将 静态构造函数 注入到每个包含它的翻译单元中。
请注意,使用其他流头文件(例如 <sstream>
)在这方面没有问题——只有 <iostream>
。但是,raw_ostream
提供了各种 API,对于几乎所有用途来说,其性能都优于 std::ostream
风格的 API。
注意
新代码应始终使用 raw_ostream 进行写入,或使用 llvm::MemoryBuffer
API 读取文件。
使用 raw_ostream
¶
LLVM 在 llvm/Support/raw_ostream.h
中包含了一个轻量级、简单且高效的流实现,它提供了 std::ostream
的所有常用功能。所有新代码都应使用 raw_ostream
而不是 ostream
。
与 std::ostream
不同,raw_ostream
不是模板,可以前向声明为 class raw_ostream
。公共头文件通常不应包含 raw_ostream
头文件,而应使用前向声明和对 raw_ostream
实例的常量引用。
避免使用 std::endl
¶
当与 iostreams
一起使用时,std::endl
修饰符会向指定的输出流输出换行符。然而,除了这样做之外,它还会刷新输出流。换句话说,以下是等效的
std::cout << std::endl;
std::cout << '\n' << std::flush;
大多数时候,您可能没有理由刷新输出流,因此最好使用文字 '\n'
。
在类定义中定义函数时,不要使用 inline
¶
在类定义中定义的成员函数隐式地是内联的,因此在这种情况下不要放置 inline
关键字。
不要这样做
class Foo {
public:
inline void bar() {
// ...
}
};
这样做
class Foo {
public:
void bar() {
// ...
}
};
微观细节¶
本节介绍首选的低级格式化指南以及我们为什么首选它们的原因。
括号前的空格¶
仅在控制流语句中,但在普通函数调用表达式和类似函数的宏中,不要在左括号前放置空格。例如
if (X) ...
for (I = 0; I != 100; ++I) ...
while (LLVMRocks) ...
somefunc(42);
assert(3 != 4 && "laws of math are failing me");
A = foo(42, 92) + bar(X);
这样做的原因并非完全是武断的。这种风格使控制流运算符更加突出,并使表达式流动性更好。
首选前自增¶
硬性快速规则:前自增 (++X
) 可能不比后自增 (X++
) 慢,并且很可能比它快得多。尽可能使用前自增。
后自增的语义包括制作被自增值的副本,返回它,然后前自增 “工作值”。对于原始类型,这不是什么大问题。但对于迭代器,这可能是一个巨大的问题(例如,一些迭代器包含堆栈和集合对象...复制迭代器也可能调用这些对象的复制构造函数)。一般来说,养成始终使用前自增的习惯,您就不会有问题。
命名空间缩进¶
总的来说,我们努力尽可能减少缩进。这很有用,因为我们希望代码适应 80 列而不进行过度换行,而且因为它使代码更容易理解。为了方便这一点并避免有时出现非常深的嵌套,请不要缩进命名空间。如果它有助于可读性,请随意添加注释,指示哪个命名空间被 }
关闭。例如
namespace llvm {
namespace knowledge {
/// This class represents things that Smith can have an intimate
/// understanding of and contains the data associated with it.
class Grokable {
...
public:
explicit Grokable() { ... }
virtual ~Grokable() = 0;
...
};
} // namespace knowledge
} // namespace llvm
当被关闭的命名空间因任何原因而显而易见时,请随意跳过关闭注释。例如,头文件中的最外层命名空间很少引起混淆。但是,源文件中途关闭的匿名和命名命名空间可能需要澄清。
限制可见性¶
函数和变量应具有尽可能最受限制的可见性。对于类成员,这意味着使用适当的 private
、protected
或 public
关键字来限制其访问。对于非成员函数、变量和类,这意味着如果该文件外部没有引用,则将可见性限制为单个 .cpp
文件。
文件作用域非成员变量和函数的可见性可以通过使用 static
关键字或匿名命名空间来限制为当前翻译单元。匿名命名空间是一个很棒的语言特性,它告诉 C++ 编译器命名空间的内容仅在当前翻译单元中可见,从而允许更积极的优化并消除符号名称冲突的可能性。匿名命名空间对于 C++ 来说就像 static
对于 C 函数和全局变量一样。虽然 static
在 C++ 中可用,但匿名命名空间更通用:它们可以使整个类对文件私有。
匿名命名空间的问题在于它们自然而然地倾向于鼓励缩进其主体,并且它们降低了引用的局部性:如果您在 C++ 文件中看到一个随机函数定义,很容易看出它是否标记为 static,但是查看它是否在匿名命名空间中需要扫描文件的很大一部分。
因此,我们有一个简单的指南:使匿名命名空间尽可能小,并且仅将它们用于类声明。例如
namespace {
class StringSort {
...
public:
StringSort(...)
bool operator<(const char *RHS) const;
};
} // namespace
static void runHelper() {
...
}
bool StringSort::operator<(const char *RHS) const {
...
}
避免将类以外的声明放入匿名命名空间
namespace {
// ... many declarations ...
void runHelper() {
...
}
// ... many declarations ...
} // namespace
当您在大型 C++ 文件的中间看到 “runHelper
” 时,您无法立即判断此函数是否是文件的本地函数。相反,当函数标记为 static 时,您无需交叉引用文件中遥远的位置来判断该函数是本地函数。
不要在 if/else/loop 语句的简单单语句体上使用花括号¶
在编写 if
、else
或 for/while 循环语句的主体时,我们倾向于省略花括号,以避免不必要的行噪声。但是,在省略花括号会损害代码的可读性和可维护性的情况下,应使用花括号。
我们认为,当省略花括号时,如果存在伴随注释的单条语句(假设注释不能提升到 if
或循环语句之上,请参见下文),则会损害可读性。
同样,当单语句体足够复杂,以至于难以看到包含以下语句的块从哪里开始时,应使用花括号。if
/else
链或循环被视为此规则的单条语句,并且此规则递归适用。
此列表并非详尽无遗。例如,如果 if
/else
链不对其所有或任何成员使用带花括号的主体,或者具有复杂的条件、深层嵌套等,也会损害可读性。以下示例旨在提供一些指导。
如果 if
的主体以(直接或间接)嵌套的 if
语句结尾,但没有 else
,则会损害可维护性。外部 if
上的花括号将有助于避免陷入 “悬空 else” 的情况。
// Omit the braces since the body is simple and clearly associated with the
// `if`.
if (isa<FunctionDecl>(D))
handleFunctionDecl(D);
else if (isa<VarDecl>(D))
handleVarDecl(D);
// Here we document the condition itself and not the body.
if (isa<VarDecl>(D)) {
// It is necessary that we explain the situation with this surprisingly long
// comment, so it would be unclear without the braces whether the following
// statement is in the scope of the `if`.
// Because the condition is documented, we can't really hoist this
// comment that applies to the body above the `if`.
handleOtherDecl(D);
}
// Use braces on the outer `if` to avoid a potential dangling `else`
// situation.
if (isa<VarDecl>(D)) {
if (shouldProcessAttr(A))
handleAttr(A);
}
// Use braces for the `if` block to keep it uniform with the `else` block.
if (isa<FunctionDecl>(D)) {
handleFunctionDecl(D);
} else {
// In this `else` case, it is necessary that we explain the situation with
// this surprisingly long comment, so it would be unclear without the braces
// whether the following statement is in the scope of the `if`.
handleOtherDecl(D);
}
// Use braces for the `else if` and `else` block to keep it uniform with the
// `if` block.
if (isa<FunctionDecl>(D)) {
verifyFunctionDecl(D);
handleFunctionDecl(D);
} else if (isa<GlobalVarDecl>(D)) {
handleGlobalVarDecl(D);
} else {
handleOtherDecl(D);
}
// This should also omit braces. The `for` loop contains only a single
// statement, so it shouldn't have braces. The `if` also only contains a
// single simple statement (the `for` loop), so it also should omit braces.
if (isa<FunctionDecl>(D))
for (auto *A : D.attrs())
handleAttr(A);
// Use braces for a `do-while` loop and its enclosing statement.
if (Tok->is(tok::l_brace)) {
do {
Tok = Tok->Next;
} while (Tok);
}
// Use braces for the outer `if` since the nested `for` is braced.
if (isa<FunctionDecl>(D)) {
for (auto *A : D.attrs()) {
// In this `for` loop body, it is necessary that we explain the situation
// with this surprisingly long comment, forcing braces on the `for` block.
handleAttr(A);
}
}
// Use braces on the outer block because there are more than two levels of
// nesting.
if (isa<FunctionDecl>(D)) {
for (auto *A : D.attrs())
for (ssize_t i : llvm::seq<ssize_t>(count))
handleAttrOnDecl(D, A, i);
}
// Use braces on the outer block because of a nested `if`; otherwise the
// compiler would warn: `add explicit braces to avoid dangling else`
if (auto *D = dyn_cast<FunctionDecl>(D)) {
if (shouldProcess(D))
handleVarDecl(D);
else
markAsIgnored(D);
}
参见¶
许多这些注释和建议都来自其他来源。对于我们的工作来说,两本特别重要的书是
Effective C++,作者 Scott Meyers。同一作者的 “More Effective C++” 和 “Effective STL” 也很有趣且有用。
Large-Scale C++ Software Design,作者 John Lakos
如果您有空闲时间,并且还没有阅读过它们:请这样做,您可能会学到一些东西。
注释格式化¶
通常,首选 C++ 风格的注释(
//
用于普通注释,///
用于doxygen
文档注释)。但在以下几种情况下,使用 C 风格 (/* */
) 注释很有用编写与 C89 兼容的 C 代码时。
编写可能被 C 源文件
#include
的头文件时。编写由仅接受 C 风格注释的工具使用的源文件时。
记录在调用中用作实际参数的常量的意义时。这对于
bool
参数或传递0
或nullptr
最有帮助。注释应包含参数名称,该名称应有意义。例如,在此调用中,不清楚参数的含义内联 C 风格注释使意图显而易见
不鼓励注释掉大块代码,但如果您真的必须这样做(出于文档目的或作为调试打印的建议),请使用
#if 0
和#endif
。这些可以正确嵌套,并且通常比 C 风格注释表现更好。