Files
wiki/编码规范.md
2024-07-16 19:11:04 +08:00

20 KiB
Raw Permalink Blame History

开发团队C++编码规范

代码风格

缩进

  • 采用4个空格进行缩进
  • 使用空格不要用TAB

变量声明

  • 每行一个变量

      // 错误示例
      int width, height;
    
      // 正确
      int height;
      int width;
    
  • 尽可能避免短的无意义的变量名(比如"a", "rbarr", "nughdeget")

      // 错误示例
      int a;
      double b;
    
      // 正确
      int number;
      double speed;
    
  • 单字符的变量只在临时变量或循环的计数中使用

      // 错误示例
      int i = 1025;
    
      // 正确
      int count = 1025;
      for ( auto i = 0; i < count; ++i )
      {
        ...
      }
    
  • 等到真正需要使用时再定义变量

      // 错误示例
      void exampleFunction() 
      {
          int step = 0;   // 变量定义在函数开头,但尚未使用
          int number = 0; // 变量定义在函数开头,但尚未使用
    
          // 此处是其它业务逻辑代码
          ...
    
          // 很久之后才使用 变量 x 和 y
          step = 5;
          number = 10;
      }
    
    
      // 正确
      void exampleFunction() 
      {
          // 变量在需要时定义
          int step = 5;
          int number = 10;
    
          std::cout << "step: " << step << ", number: " << number << "\n";
      }
    
  • 首字母小写,后续单词以大写开头(驼峰式)

  • 避免使用缩写

      // 错误示例
      short Cntr;
      char ITEM_DELIM = '';
    
      // 正确
      short counter;
      char itemDelimiter = '';
    
  • 类名总是以大写开头

     // 错误示例
     class person 
     {
       ...
     }
    
     // 正确
     class Person 
     {
       ...
     }
    

空白

  • 利用空行将语句恰当地分组

  • 总是使用一个空行(不要空多行)

  • 总是在每个关键字和大括号前使用一个空格

      // 错误示例
      if(foo)
      {
      }
    
      // 正确
      if (foo) 
      {
      }
    
  • 对指针和引用,在类型和*、&之间加一个空格,但在*、&与变量之间不加空格?

      char *something;
      const QString &myString;
      const char* const message = "hello";
    
  • 二元操作符前后加空白

  • 类型转换后不加空白

  • 尽量避免C风格的类型转换

      // 错误示例
      char* blockOfMemory = (char* ) malloc(data.size());
    
      // 正确
      char *blockOfMemory = reinterpret_cast<char*>(malloc(data.size()));
    

大括号

  • 基本原则:左大括号和语句之间换行,左大括号总是单独占一行

      // 错误示例
      if (codec){
      }
    
      // 正确
      if (codec) 
      {
      }
    
  • 例外1定义命名空间时左大括号与命名空间同行

      namespace Test {
        qDebug("foo: %i", g);
      }
    
  • 例外2控制语句的body为空时大括号与控制语句同行

      while(a) {}
    
  • 控制语句的body中只有一行时不使用大括号

      // 错误示例
      if (address.isEmpty()) 
      {
        return false;
      }
    
      for (int i = 0; i < 10; +''i) 
      {
        qDebug("%i", i);
      }
    
      // 正确
      if (address.isEmpty())
        return false;
    
      for (int i = 0; i < 10;i)
        qDebug("%i", i);
    
  • 例外1如果父语句跨多行则使用大括号

      // 正确
      if (address.isEmpty() || !isValid()
      || !codec) 
      {
        return false;
      }
    
  • 例外2在if-else结构中有一处跨多行则使用大括号

      // 错误示例
      if (address.isEmpty())
        return false;
      else 
      {
        qDebug("%s", qPrintable(address));
        it;
      }
    
      // 正确
      if (address.isEmpty()) 
      {
        return false;
      } 
      else 
      {
        qDebug("%s", qPrintable(address));
        it;
      }
    
      // 错误示例
      if (a)
      if (b)
        
      else
        
    
      // 正确
      if (a) 
      {
        if (b)
          
        else
          
      }
    
  • 如果控制语句的body为空则使用大括号

      // 错误示例
    while (a);
    
      // 正确
    while (a) {}
    

圆括号

  • 使用圆括号将表达式分组,避免需要考虑运算符优先级的情况

      // 错误示例
    if (a && b || c)
    
      // 正确
    if ((a && b) || c)
    
      // 错误示例
    a | b & c
    
    // 正确
    (a + b) & c
    

Switch 语句

  • case 和 switch 位于同一列

  • 每一个case必须有一个break(或renturn)语句或者用注释说明无需break

     switch (myEnum) 
     {
     case Value1:
       doSomething();
     break;
     case Value2:
       doSomethingElse();
     // fall through
     default:
       defaultHandling();
     break;
     }
    

断行

  • 保持每行短于100个字符需要时进行断行

  • 逗号放一行的结束,操作符放到一行的开头。如果你的编辑器太窄,一个放在行尾的操作符不容易被看到。

      // 错误示例
      if (longExpression +
        otherLongExpression +
        otherOtherLongExpression) 
      {
        ...
      }
    
      // 正确
      if (longExpression
       + otherLongExpression
       + otherOtherLongExpression)
      {
        ...
      }
    

继承与关键字 virtual|override

  • 子类中重新实现一个虚函数时,头文件中不放置 virtual 关键字但必须显式的标注override关键字。

include 规范

在文件首部注释(和包含保护,如果是头文件)之后,该文件需要的最小 #include 列表应该被列出来。要求的 #include 顺序如下:

  • 主模块头文件
  • 局部的/私有的头文件
  • 项目/子项目头文件 clang/..., lldb/..., llvm/..., etc
  • 系统文件

并且每一个种类的头文件完整路径应该做一个排序。

主模块头文件适用于声明实现接口的 .cpp 文件的 .h 文件。该 #include 不管处在什么文件系统下总是应该第一个被包含。通过在实现该接口的第一包含头文件可以确保头文件没有隐藏需要但没有显式包含的依赖。同样的,这也是在 .cpp中一种指明接口声明在何处的记录。

项目和子项目的头文件应该从最高优先级到最低优先级分组理由同上。举个例子LLDB 同时依赖 clang 和 LLVM并且 clang 依赖 LLVM。所以一个 LLDB 源文件应该最先包含 LLDB ,其次是 clang 头文件,其次是 LLVM 头文件这样做是为了在源文件中的头文件或更前的头文件包含情况下降低LLDB头文件包含缺失的可能。clang 同样的应该在包含LLVM文件之前包含其自身的头文件。

使用class和struct关键字

在C++中,classstruct 关键字在绝大数的情况下可以互换。仅仅的区别在于定义一个 class 时所有的成员默认为 private而struct默认为 public。

  • 给定 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) { }
    };
    

使用auto类型推导提高代码可读性

一些人主张在C++11种几乎总是使用auto的原则然而LLVM保持更温和的态度。仅在如果使用auto可以提升代码的可读性或者更利于维护的情况下使用。并不几乎总是使用 auto,而是在初始化像 cast<Foo>(...) 或者类型可以明显的从上下文中获取的地方中使用。其它时候 auto 良好用于其用途是无论如何该类型都会被抽象化,经常出现在容器的 typedef 之后如 std::vector<T>::iterator

同样的C++14 新增类型可以为auto的通用lambda表达式可以在原本使用模板的地方使用它。

小心auto带来的不必要拷贝

auto 的便利性更容易遗忘它默认是拷贝的行为,尤其是在基于范围的循环,粗心的代价很昂贵。

值使用 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(); }

头文件独立

头文件应该保持独立(独立编译)并且以.h结尾。非头文件的包含应该以.inc结尾并且谨慎使用。

所有的头文件都应该是独立的。用户或者重构工具不应该强制附加特定条件才能够包含这个头文件。特别的,一个头文件应该存在头文件保护和所有需要的其他头文件。

有一些及少见的情况,设计被包含的文件不是独立的。它们往往有意的被包含在不常用的地方,比如一个文件的中间位置。它们可能不使用头文件保护并且可能不包括必要的先决条件。这些文件的名称以 .inc 扩展。谨慎使用这种文件,尽可能使用独立的头文件。

通常而言,一个头文件应该被一个或多个 .cpp 实现。每一个 .cpp 文件都应该首先包含定义接口的头文件。确保头文件所有的依赖都能够显式正确的添加至该头文件中。

库层次

头文件目录(如 include/llvm/Foo定义了库Foo。库之间的依赖在它们实现lib/Foo中的 LLVMBuild.txt 定义。一个库应该仅使用依赖所列出库的内容。

一些经典的Unix链接器Mac & Windows 链接器比如lld不强制执行可以强制执行某些约束。Unix 链接器从左往右在命令行中搜索特定的库并且不会重复访问同一个库。这种情况下,库之间就不存在循环依赖关系。

这样不会完全强制的执行所有的相互库依赖,且重要的是不会执行由内联函数带来的头文件循环依赖。回答是否正确分层的一个好方法是判断 Unix 链接器是否可以正确链接使用非内联函数代替内联函数的程序。(对于所有依赖的有效顺序 - 由于链接的方案是线性的因此可能潜伏一些隐式的依赖A 依赖于B和C所以有效的顺序为“CBA” 或者“BCA”两种显示的依赖都在使用之前出现。但对于第一种情况B如果隐式的依赖于C仍然可以链接成功或者在第二种情况相反的依赖也可以链接成功

尽可能的减少 #include

#include 降低了编译的性能,在不是必需的时候不要包含,尤其是在头文件中。

有一些情况是你需要获取到类的定义再去使用或者继承它,这些情况则在文件的首部进行 #include。然而也要想到,存在非常多的情况是不需要拥有类的完整定义的。如果正在使用类指针或引用,则不必包含该头文件。如果只是的从原型函数或者方法中返回一个类的实例,也不需要头文件。实际上,在大多数的情况下,你根本不需要类的定义。不进行 #include 可以加速编译。

这个建议很容易导致偏激的态度,然而,你必须包含所有正在使用的头文件,无论是直接还是间接从其它文件包含。为确保你不会意外的忘记在你的模块头文件中包含头文件,确认在实现文件中第一个包含你的模块头文件(就像上面提到的)。这样,你后面就会发现不会再有隐藏的依赖了。

保持私有的内部头文件

很多模块实现非常复杂导致使用了多个实现文件(.cpp文件)。不要尝试将公共交互接口(帮助类,扩展函数等)放入到公共的模块头文件中。

如果你真的需要这么做的话,在相同的目录下放入一个私有的头文件让源文件局部包含。这样确保你的私有接口保留了私有属性并且不向外发布。

允许将扩展实现的方法放入到一个公共类自身中,但是需要让它们设为私有(保护),一切就正常。

使用 namespace 限定符莱实现前置声明的函数

当源文件中提供一个非内联(或者叫外部实现)的函数实现时,不要在源文件中打开 namespace 块namespace xx {})。相反的,使用 namespace 标记符来帮助确保你的定义匹配上已经存在的声明。像这样:

// Foo.h
namespace llvm {
int foo(const char *s);
}

// Foo.cpp
#include "Foo.h"
using namespace llvm;
int llvm::foo(const char *s) {
  // ...
}

这样做能够避免实现不能匹配头文件中声明的bug。举个例子下面的C++代码定义了一个新的重载llvm::foo来取代在头文件中存在函数声明的实现:

// Foo.cpp
#include "Foo.h"
namespace llvm {
int foo(char *s) { // Mismatch between "const char *" and "char *"
}
} // end namespace llvm

这个错误在快构建完成之前不能够被捕获直到产生函数调用找不到函数定义的链接错误。如果这个函数使用namespace标记符来代替这个错误直接在这个定义编译时被捕获。

类方法实现必须类已经被命名并且新的重载不能够引入到外部,所以这个建议对它们不起作用。

提前退出或 continue 以简化代码

在阅读代码时,读者需要在记住有多少状态及前置条件去阅读代码块。为了减少尽可能缩进而又不是代码变得更难理解,在长循环中使用提前退出或者continue关键字是一个好的方法。观察未使用提前退出的代码:

Value *doSomething(Instruction *I) {
  if (!I->isTerminator() &&
      I->hasOneUse() && doOtherThing(I)) {
    ... some long code ....
  }

  return 0;
}

这份代码当if作用体非常庞大时有一些问题。当你正在查看这个函数的顶部时对于是只做和结束指令相关的事情还是其他的判断操作是不够清晰的。其二由于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;

这样做的目的是减少缩进和在阅读代码时需要跟踪的代码量。

将判断循环转换为判断函数

通过短循环计算一个 boolean 值是非常常见的写法。有一系列的常见方式,比如这种:

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)) {
  ...
}

这样做有很多理由减少了缩进并且分离出可以让其它代码检测相同判断的共享代码。更重要的是让你强制为这个函数起一个名字并且强制让你再为它写上注释。在这个简短的例子中没有添加很多值然而如果这个if条件非常复杂读者通过这个判断可以非常容易地理解代码。而不是一开始就面对如何检测 BarList 中是否包含了 foo 的内联细节,我们可以信任这个函数名称并且继续在更好的位置进行阅读。

通用例外

如果它使你的代码看起来不好,你可以打破任何一个规则 。

附录A 参考列表

  1. Qt编码风格/Qt Coding Style
  2. LLVM编码标准/LLVM Coding Standards
  3. 谷歌C++风格指南/Google C++ Style Guide
  4. C++核心指南/Cpp Core Guidelines