文章通过段落将内容变得更具节奏,代码通过换行来达到相同的效果。换行对于代码阅读来说非常重要,过多的空行会减少屏幕显示的有效代码;过少的空行又易使代码上下黏连,造成“文不加点”的阅读困惑;不恰当的换行有可能使语义割裂,从而形成阅读障碍,甚至具有误导性。

格式类规范总体要求:凸显相关内容的关联性,隔离不同分组,使代码简洁而又层次分明。

如果任何的编码规范违背了上述要求,均应避免削足适履,当跳出规范外进行设计,将好的设计作为特例场景固化。

基础规则

完全禁止使用连续空行

若存在希望通过两个连续空行来带代码进行分块的场景,使用单空行+注释的方式进行分割。

文件域布局

文件层级经常会包含以下元素:文件头注释(包含版权声明)、#includenamespace、常量、enum/union/struct/class、全局函数。其他还有:全局变量、宏、extern声明、using namespaceusing/typedef。约定整体布局如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/*
 * Document for the file head.
 */
#include <string>

namespace maple {

constexpr int32_t kConstInt = 0;

enum Enum {};

union Union {};

struct Struct {};
  
class Class {};

void GlobalFunction() {}
} // maple

文件头尾

  1. 文件起始必须为有效的代码或注释,不可为空行。

  2. 文件结束必须有且仅有一个空行。

    文件必须以空行结束有其历史原因,主要原因为C标准行的定义

注释

上置注释与其注释内容间不应有空行。

更有严者,除右置注释外,其他注释均不应与下方代码之间有空行,即换种说法,不应存在无对应代码的注释。

特殊场景解释:文件头注释并非注释头文件保护#define,或头文件引用#include,但无空行对可读性无负面影响,且使代码更加紧凑,所以约定头文件注释与以上两者之间无空行。若源文件中头文件注释紧跟的为namespace或其他代码,则文件头注释需要与下文保持一个空行,为未来可能添加#include预留空间。

头文件引用代码域

头文件引用#include代码域与其上内容不留空行,但与其下的namespace或其他代码片段之间有空行。

通常来说#include之间不应有空行,但若基于以下几个原因之一,或其他充分的解释,可以适当添加空行:

  1. 包含大量头文件(半屏乃至一屏),难以管理

  2. 头文件分组更易做扩展(如phase_manager中对phase的头文件按类别分组)

  3. 方舟编译器C++语言编程规范》中例外的场景

    例外: 平台特定代码需要条件编译,这些代码可以放到其它 includes 之后。

    1
    2
    3
    4
    5
    6
    7
    
    #include "foo/public/FooServer.h"
    
    #include "base/Port.h"  // For LANG_CXX11.
    
    #ifdef LANG_CXX11
    #include <initializer_list>
    #endif  // LANG_CXX11
    

下面以一个C++的分类方式进行分组示例(Test.cpp):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include "Test.h"

#include <string>
// So many from STL
#include <vector>

#include "utils/utils.h"
// So many from `-I` by the system configuration
#include "utils/meta.h"

#include "abc.h"
// So many by relative search
#include "sub_folder/xyz.h"

命名空间代码域

命名空间代码域与其上内容之间有空行(在文件起始例外),但与其下的代码片段之间有空行。

方舟编译器C++语言编程规范

  • 大括号内的代码块行首之前和行尾之后不要加空行。

*”与其下的代码片段之间有空行”*违背了开源规范中的此原则,但此开源规范应当有一条先决条件,即当大括号内代码块缩进级别多于大括号的代码块时。所以如下代码,依然需要空行,避免干扰下方代码块的阅读。

1
2
3
4
5
6
7
namespace maple {

template <typename T>
void Func(T &) {

}
} // maple

命名空间代码域包含以下几类内容:

  1. 声明命名空间namespace
  2. 引用命名空间using namespace
  3. 引用类型using Type

这几类内容中间均无空行,如下所示。

1
2
3
4
5
6
7
8
9
namespace maple {
namespace utils {
using namespace A::B::C;
using namespace A1::B1;
using A2::B2::B2Object;

// Code Body
} // utils
} // maple

命名空间的右括号由于需要添加空间结束的右置注释,所以其与上方代码块之间的空行多数场景可以考虑省略。即

1
2
3
4
5
6
namespace maple {
namespace utils {

// Code Body
} // utils
} // maple

1
2
3
4
namespace maple { namespace utils {

// Code Body
}} // maple::utils

类型代码域

类型代码域包含了enum/enum class/union/struct/class/GlobalFunction(全局函数可看作类型,乃是从仿函数的角度来看,包括其使用大驼峰命名)。其各自形成块,并以}};作为终结符。

基础规则:类型代码域中每个类型之间必须留空行,与其他代码域之间必须留空行。由于每个类型均包含相当多的信息,一般不会在一个行块内定义完全,这些类型自身已形成一个分组,所以他们之间必须留空行。

例外场景如下:

  1. 类型与命名空间的结束符(})之间的空行,在不影响阅读的情况下,往往可以移除。

  2. constexpr/const常量、using/typedef与类型有强关联,可以形成大的分组时,可看作一个整体,其内部的空行多数可以省略。示例如下:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    constexpr int kScopeLocal = 2;   // the default scope level for function variables
    constexpr int kScopeGlobal = 1;  // the scope level for global variables
    
    constexpr int kSymKindCount = 7;
    enum MIRSymKind {
      kStInvalid,
      kStVar,
      ...
    };
    
    using HolderType = std::vector<int32_t>;
    void Func(const HolderType &data) {
      for (int32_t elem : data) {}
    }
    
    constexpr int kNumberLimit = 32;
    // Document for `Num`
    class Number {
     public:
    };
    

常量代码域

常量代码域包含了constexpr/const定义的常量,以及enum/enum class定义的枚举常量,枚举常量随类型代码域中的规约。constexpr/const定义的常量从是否需要进行显示分组的角度,可以分为以下两种场景进行参考(分模块、分组也是设计中重要的一份内容,尤其是在本文中,会经常提及)。

  1. 无显示分组

    1
    2
    3
    
    constexpr int kScopeLocal = 2;   // the default scope level for function variables
    constexpr int kScopeGlobal = 1;  // the scope level for global variables
    constexpr int kSymKindCount = 7;
    
  2. 按业务显示分组

    1
    2
    3
    4
    5
    6
    
    // Consts of Scope.
    constexpr int kScopeLocal = 2;   // the default scope level for function variables
    constexpr int kScopeGlobal = 1;  // the scope level for global variables
    
    // Consts of SymolKind
    constexpr int kSymKindCount = 7;
    

常量代码域与其他代码域之间需要保留一个空行。

其他

  1. 函数宏的规则同函数,常量宏的规则同常量

  2. 集中的extern声明往往命名空间代码域下,规则同常量

  3. using/typedef通常不会有集中声明,若存在,规则同常量

  4. 全局变量上下必须有空行,并有详细的注释来解释必须使用全局变量的原因

类域布局

enum/enum class/union/struct

C++中,这几种关键字创建的为数据对象,而class创建的通常是算法对象或业务对象。这也是为什么当class定义的类型中出现大量Get/Set成员变量时,会看起来比较新手,C++有自己的数据对象,而且对象设计应尽量遵循**Tell, Don’t ask"原则,题外话了。

这里以struct作为示例,struct包含成员函数的场景同class

1
2
3
4
5
struct XX {
  char a;     
  char b;     
  char c;
};
  1. 成员变量与结构体定义的{}之间均无空行
  2. 成员变量之间无空行。若要对成员变量进行分组,可以使用注释。

class

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Class {
 public:
  Class() = default;
  ~Class() = default;

 protected:
  void ProtectedFunc();

 private:
  void PrivateFunc();

 private:
  int32_t member;
};

struct规范的基础上,plublic/protected/private可见性关键字上方需要空格,而下方紧接代码,无空格。除成员函数例外,其他如类内的常量、成员变量、using/typedef文件域布局::常量代码域,类内静态成员变量同全局变量。

成员函数间必须要空行,但也有例外:

  1. 构造函数与析构函数间的空行可以省去,构造函数上方的空行以及析构函数下方的空行不能省。
  2. 对于纯函数声明,若多个函数存在相关性,即可以分组,则他们之间的空行可以省去。

函数域布局

函数域内的空行难以用精确的规则来约束,此处只能尽量描述其思想以及常见插入空行的示例。函数内空行的基本原则是:

  1. 避免为每行代码都追加一个空行,会导致代码过于松散
  2. 避免超过5行或10行且缩进层级少的代码中,一个空行都没有,或导致代码
  3. 避免随性切割代码,用以达到适当换行这样的描述

代码块之间好的空行应有类似阅读时抑扬顿挫之感。以下是几个常见可以考虑插入换行的场景:

  1. 入参校验与业务实现之间

    1
    2
    3
    4
    5
    6
    7
    
    void Func(char *str) {
      if (str == nullptr) {
        return;
      }
    
      // Business code
    }
    
  2. 参数准备与业务实现之间

    1
    2
    3
    4
    5
    6
    7
    
    void Func(const std::string &filePath) {
      std::string fileName = filePath.substr();
      std::string fileType = fileName.substr();
      std::string dirName = filePath.substr();
    
      // Business code
    }
    
  3. 业务实现与构建返回信息

    1
    2
    3
    4
    5
    6
    7
    8
    
    std::vector<int32_t> Func(const std::string &filePath) {
      // Business code
    
      std::vector<int32_t> rst;
      rst.push_back(0);
      rst.push_back(1);
      return rst;
    }
    
  4. 业务实现与区域资源回收之间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    void Func(int32_t dataCode1, int32_t dataCode2) {
      LockResource(dataCode1);
      LockResource(dataCode2);
    
      // Business code
    
      UnlockResource(dataCode1);
      UnlockResource(dataCode2);
    }
    
  5. 平行的业务实现与业务实现之间

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    void Func(char *str) {
      // Check input
      if (str == nullptr) {
        return;
      }
    
      // Check input
      std::string fileName = filePath.substr();
      std::string fileType = fileName.substr();
      if (fileType != "csv") {
        return;
      }
      std::string dirName = filePath.substr();
      if (dirName.find("..") != std::string::npos) {
        return;
      }
    
      // Business code
    }
    

函数域内空行添加空行的原则最终的原则:无论从结构设计抑或是业务概念上,可以进行分组,在不会显得松散的情况下,可以添加空行。换句话说想好怎么回复质疑的理由,那便可以换行