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::mapstd::unordered_map 使用,并且 llvm::SmallVector 通常应该代替 std::vector 使用。

我们明确避免使用某些标准工具,例如 I/O 流,而是使用 LLVM 的流库(raw_ostream)。有关这些主题的更多详细信息,请参阅 LLVM 程序员手册

有关 LLVM 数据结构及其权衡的更多信息,请参阅 程序员手册中的该部分

Python 版本和源代码格式

所需的 Python 最低版本在 LLVM 系统入门 部分有说明。LLVM 代码库中的 Python 代码应仅使用此版本的 Python 中可用的语言特性。

LLVM 代码库中的 Python 代码应遵循 PEP 8 中概述的格式指南。

为了保持一致性和限制代码变化,代码应使用符合 PEP 8 的 black 实用程序自动格式化。使用其默认规则。例如,即使它不默认为 80,也避免指定 --line-length。默认规则在 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^)

机械源代码问题

源代码格式

注释

注释对于代码的可读性和可维护性非常重要。编写注释时,请使用正确的英文语法,包括大小写、标点符号等。注释的目标是描述代码试图做什么以及为什么,而不是在微观层面上如何做到这一点。以下是一些需要记录的重要事项

文件头

每个源文件都应该有一个文件头,描述该文件的基本用途。标准文件头如下所示

//===-- llvm/Instruction.h - Instruction class definition -------*- C++ -*-===//
//
// 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.
///
//===----------------------------------------------------------------------===//

关于此特定格式,需要注意以下几点:“-*- C++ -*-”字符串位于第一行,用于告诉Emacs源文件是C++文件,而不是C文件(默认情况下,Emacs假设.h文件是C文件)。

注意

此标记在.cpp文件中不是必需的。文件名称也位于第一行,以及对文件用途的非常简短的描述。

文件中的下一部分是简要说明,定义了文件发布所依据的许可证。这使得源代码可以根据哪些条款分发变得非常清楚,并且不应以任何方式修改。

主体部分是一个Doxygen注释(由///注释标记而不是通常的//标识),描述了文件的目的。第一句话(或以\brief开头的段落)用作摘要。任何其他信息都应由空行分隔。如果算法基于论文或在其他来源中进行了描述,请提供参考。

头文件保护

头文件保护应该是在大写中使用用户包含此头文件的路径,使用“_”代替路径分隔符和扩展名标记。例如,头文件llvm/include/llvm/Analysis/Utils/Local.h将被包含为#include "llvm/Analysis/Utils/Local.h",因此它的保护是LLVM_ANALYSIS_UTILS_LOCAL_H

类概述

类是面向对象设计的基石。因此,类定义应该有一个注释块,解释该类的用途和工作原理。每个非平凡的类都应该有一个doxygen注释块。

方法信息

方法和全局函数也应该有文档记录。简要说明它做什么以及边缘情况的描述在这里就足够了。读者应该能够理解如何使用接口,而无需阅读代码本身。

这里需要讨论的好东西是当发生一些意外情况时会发生什么,例如,方法是否返回空值?

注释格式

通常,更喜欢C++风格的注释(//用于普通注释,///用于doxygen文档注释)。但是,在某些情况下使用C风格(/* */)注释非常有用

  1. 当编写与C89兼容的C代码时。

  2. 当编写可能被C源文件#include的头文件时。

  3. 当编写由仅接受C风格注释的工具使用的源文件时。

  4. 当记录用作调用中实际参数的常量的意义时。这对于bool参数或传递0nullptr最有用。注释应包含参数名称,该名称应具有意义。例如,在此调用中不清楚参数的含义

    Object.emitName(nullptr);
    

    内联C风格注释使意图变得明显

    Object.emitName(/*Prefix=*/nullptr);
    

不鼓励注释掉大块代码,但如果确实必须这样做(出于文档目的或作为调试打印的建议),请使用#if 0#endif。这些可以正确嵌套,并且通常比C风格注释表现得更好。

文档注释中的Doxygen使用

使用\file命令将标准文件头转换为文件级注释。

为所有公共接口(公共类、成员函数和非成员函数)包含描述性段落。避免重述可以从API名称中推断出的信息。第一句话(或以\brief开头的段落)用作摘要。尝试使用单个句子作为\brief,因为它会增加视觉上的混乱。将详细的讨论放在单独的段落中。

要在段落内引用参数名称,请使用\p name命令。不要使用\arg name命令,因为它会开始一个新段落,其中包含该参数的文档。

将非内联代码示例包装在\code ... \endcode中。

要记录函数参数,请使用\param name命令开始一个新段落。如果参数用作输出参数或输入/输出参数,则分别使用\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() { ... }

错误和警告消息

清晰的诊断消息对于帮助用户识别和修复输入中的问题非常重要。使用简洁但正确的英文散文,为用户提供理解问题所在所需的上下文。此外,为了匹配其他工具通常生成的错误消息样式,请以小写字母开头第一句话,并在最后一句话末尾不加句号,如果它本来会以句号结尾的话。以不同标点符号结尾的句子,例如“你是否忘记了‘;’?”,仍然应该这样做。

例如,这是一个好的错误消息

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

  1. 主模块头文件

  2. 本地/私有头文件

  3. LLVM项目/子项目头文件(clang/...lldb/...llvm/...等)

  4. 系统#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_castreinterpret_castconst_cast,而不是 C 风格的强制类型转换。有两个例外。

  • 将强制类型转换为void以抑制有关未使用的变量的警告(作为[[maybe_unused]]的替代方法)。在这种情况下,优先使用 C 风格的强制类型转换。

  • 在整型之间进行强制类型转换(包括不是强类型枚举)时,允许使用函数式强制类型转换作为static_cast的替代方法。

不要使用静态构造函数

不应向代码库中添加静态构造函数和析构函数(例如,类型具有构造函数或析构函数的全局变量),并且应在任何可能的地方删除它们。

不同源文件中的全局变量以任意顺序初始化,这使得代码更难以理解。

静态构造函数对使用 LLVM 作为库的程序的启动时间有负面影响。我们希望链接其他 LLVM 目标或其他库到应用程序的成本为零,但静态构造函数破坏了这一目标。

使用classstruct关键字

在 C++ 中,classstruct关键字几乎可以互换使用。唯一的区别是当它们用于声明类时:class默认使所有成员私有,而struct默认使所有成员公有。

  • 给定classstruct的所有声明和定义都必须使用相同的关键字。例如

// 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,但是要将autocast<Foo>(...)之类的初始化器一起使用,或者在从上下文中已经可以明显看出类型的其他地方使用。在以下情况下,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 语句使得难以布置注释。第三,当您深入代码主体时,它会缩进一个额外的级别。最后,当阅读函数顶部时,不清楚如果谓词不为真则结果是什么;您必须阅读到函数的末尾才能知道它返回空。

最好像这样格式化代码

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

出于与上述类似的原因(减少缩进和更易于阅读),请不要在中断控制流的内容(如 returnbreakcontinuegoto 等)之后使用 'else''else if'。例如

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,要么查找以找到准确的拼写。

一般来说,名称应该使用驼峰命名法(例如 TextFileReaderisLValue())。不同类型的声明有不同的规则。

  • **类型名称**(包括类、结构体、枚举、typedef 等)应该为名词,并以大写字母开头(例如 TextFileReader)。

  • **变量名称**应该为名词(因为它们表示状态)。名称应使用驼峰命名法,并以大写字母开头(例如 LeaderBoats)。

  • **函数名称**应该为动词短语(因为它们表示动作),命令式函数应该使用祈使语气。名称应使用驼峰命名法,并以小写字母开头(例如 openFile()isFoo())。

  • **枚举声明**(例如 enum Foo {...})是类型,因此它们应遵循类型命名约定。枚举的一个常见用途是作为联合体的区分符,或子类的指示符。当枚举用于此类用途时,它应该具有 Kind 后缀(例如 ValueKind)。

  • **枚举器**(例如 enum { Foo, Bar })和**公共成员变量**应该以大写字母开头,就像类型一样。除非枚举器在其自己的小命名空间或类内部定义,否则枚举器应该有一个与枚举声明名称相对应的前缀。例如,enum ValueKind { ... }; 可能包含像 VK_ArgumentVK_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' 指令会污染包含该头文件的任何源文件的命名空间,从而产生维护问题。

在实现文件(例如 .cpp 文件)中,该规则更像是一个风格规则,但仍然很重要。基本上,使用显式命名空间前缀使代码更**清晰**,因为它立即清楚地表明正在使用哪些功能以及它们来自哪里。并且更**便携**,因为命名空间冲突不会发生在 LLVM 代码和其它命名空间之间。可移植性规则很重要,因为不同的标准库实现公开了不同的符号(可能是一些它们不应该公开的符号),并且 C++ 标准的未来修订版将向 std 命名空间添加更多符号。因此,我们从未在 LLVM 中使用 'using namespace std;'

一般规则的例外情况(即它不是 std 命名空间的例外)是实现文件。例如,LLVM 项目中的所有代码都实现了存在于 'llvm' 命名空间中的代码。因此,.cpp 文件在包含语句之后顶部包含 'using namespace llvm;' 指令是可以的,实际上更清晰。这减少了基于大括号缩进的源代码编辑器中的缩进,并使概念上下文更清晰。此规则的一般形式是,任何实现任何命名空间中的代码的 .cpp 文件都可以使用该命名空间(及其父命名空间),但不应使用任何其他命名空间。

在头文件中为类提供虚拟方法锚点

如果类在头文件中定义并且具有 vtable(它具有虚拟方法或派生自具有虚拟方法的类),则它必须始终在类中至少具有一个非内联虚拟方法。没有它,编译器将把 vtable 和 RTTI 复制到每个包含该头文件的 .o 文件中,从而使 .o 文件大小膨胀并增加链接时间。

不要在枚举的完全覆盖的 switch 语句中使用 default 标签

-Wswitch 如果 switch 语句(没有 default 标签)对枚举没有覆盖每个枚举值,则会发出警告。如果你在枚举的完全覆盖的 switch 语句上编写 default 标签,那么当向该枚举添加新元素时,-Wswitch 警告将不会触发。为了帮助避免添加此类默认值,Clang 具有警告 -Wcovered-switch-default,该警告默认情况下是关闭的,但在使用支持该警告的 Clang 版本构建 LLVM 时会打开。

此风格要求的连锁效应是,当使用 GCC 构建 LLVM 时,如果你从覆盖的 switch-over-enum 的每个 case 返回,你可能会收到与“控制可能到达非 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()”可能会在每次循环迭代中改变其值,并且第二个循环实际上可能不正确。如果您确实依赖于此行为,请以第一种形式编写循环,并添加注释以表明您是故意这样做的。

为什么我们更喜欢第二种形式(当它正确时)?以第一种形式编写循环有两个问题。首先,它可能不如在循环开始时计算它高效。在这种情况下,成本可能很小——每次循环迭代都会增加一些额外的加载。但是,如果基本表达式更复杂,那么成本可能会迅速上升。我见过一些循环,其中结束表达式实际上类似于:“SomeMap[X]->end()”,而映射查找确实不便宜。通过始终以第二种形式编写它,您可以完全消除这个问题,甚至不必考虑它。

第二个(更重要)的问题是以第一种形式编写循环会暗示读者该循环正在修改容器(注释会方便地确认这一事实!)。如果您以第二种形式编写循环,则无需查看循环体即可立即清楚地知道容器没有被修改,这使得代码更容易阅读和理解其功能。

虽然第二种形式的循环需要多敲几个键,但我们确实强烈推荐它。

#include <iostream> 被禁止

在库文件中使用 #include <iostream> 被禁止,因为许多常见的实现会将一个静态构造函数透明地注入到包含它的每个翻译单元中。

请注意,使用其他流头文件(例如 <sstream>)在这方面没有问题——只有 <iostream>。但是,raw_ostream 提供了各种 API,这些 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

std::endl 修饰符与 iostreams 一起使用时,会将换行符输出到指定的输出流。但是,除了执行此操作之外,它还会刷新输出流。换句话说,这些是等价的

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

当出于任何原因要关闭的命名空间很明显时,可以随意跳过结束注释。例如,头文件中最外层的命名空间很少是混淆的来源。但是,源文件中处于匿名或命名状态的命名空间,如果在文件中间关闭,则可能需要澄清。

匿名命名空间

在讨论了命名空间的一般情况后,您可能想知道匿名命名空间的具体情况。匿名命名空间是一个很棒的语言特性,它告诉 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/循环语句的简单单语句主体上使用大括号

在编写 ifelse 或 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);
}

// 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);
}

另请参阅

许多这些注释和建议都来自其他来源。对我们的工作特别重要的两本书是

  1. Effective C++,作者 Scott Meyers。同样有趣且有用的还有同一作者的“More Effective C++”和“Effective STL”。

  2. Large-Scale C++ Software Design,作者 John Lakos

如果您有空闲时间,并且还没有阅读过:请阅读它们,您可能会学到一些东西。