设计

本篇尝试设计一个优秀的 C 和 C++ 的代码风格,目的是提高代码的可维护性。

正如前文提到的,这里的代码风格的设计过程中,唯一的也是终极的原则就是“提高代码的可维护性”。直观的讲,在设计过程中,遇到某个决策时,会有多个选择,通过根据这个唯一原则评估每个选择的优先级,来选择最优的方案,因为有可能最后有多个选择,所以最优方案可以不唯一。

空格和空行

  1. 使用空格在符号之间增加间隙,类似英文单词间的空格

二元运算符和操作数之间:

// do
a = b + c;

// do not
a=b+c;

控制语句的关键词和后面的括号之间:

// do
if (b) {
}
switch (a) {
}

// do not
if(b) {
}
switch(a) {
}

同一行的开花括号前:

// do
if (b) {
}

// do not
if(b){
}
  1. 使用空行给代码增加段落感

两个函数、方法的声明、实现之间:

// do
void f1();

void f2();

// do not
void f1();
void f2();

用户复合类型实现之间:

// do
struct A
{
};

struct B
{
};

// do not
struct A
{
};
struct B
{
};

数据成员之间可选,如果带有文档注释,极力推荐加空行:

// do
struct A
{
        /// one
        int a;

        /// another one
        int b;
};

// or
struct A
{
        int a;
        int b;
};

函数实现内不同业务逻辑之间:

// do
void fun()
{
        int a = 0;
        ... // do something with a

        ... // check results
}

// do not
void fun()
{
        int a = 0;
        ... // do something with a
        ... // check results
}
  1. 不要使用连续的空格和空行, 除非用于缩进

  2. 不要编写非常长的代码行,在适当的时机 wrap line

一般出现长行的情形:

// 1. 参数太多
int many_params_fn(int argument1, bool boolean_arg, float float_argument, std::string const &str_arg, std::vector<std::string> const &str_arguments);
// 这种最好将每个参数单独置于一行
int many_params_fn(
        int argument1,
        bool boolean_arg,
        float float_argument,
        std::string const &str_arg,
        std::vector<std::string> const &str_arguments
);

// 2. 返回类型(比如模板类型)
std::map<std::string, std::vector<std::pair<std::string, std::set<std::string>>>> const &long_return_type_fn(int arg);
// 像这种实际是种糟糕的写法:
// - 优化接口设计,是否真的要返回这么复杂的类型
// - 尝试作为引用参数的方式
// - 可以考虑使用模板别名
// - 或者把返回类型单独成行
// 后两种实际没有解决长类型的问题
using ReturnType = std::map<std::string, std::vector<std::pair<std::string, std::set<std::string>>>>;

ReturnType const &long_return_type_fn(int arg);
// 1. 算数表达式
auto b = long_expr_abc * long_expr_abc + length_of_string * 4 + bd / bc - digit * (long_expr_int % 2) - 12;
// 一般优先在低优先级的运算符前进行 wrap
auto b = long_expr_abc * long_expr_abc
        + length_of_string * 4 + bd / bc
        - digit * (long_expr_int % 2) - 12;

// 2. 函数调用
auto result = call_lot_of_args(count, names, goods, filters_of_bad, filters_of_very_bad, [](int i) {return i * i; });
// - 优化接口,太多的参数是个糟糕设计,要么是函数实现太复杂,要么是状态太多,
//   是不是应该实现为类,或用结构体代替
// - 很显然,参数前应该 wrap,可以每个参数单独成行,或长度适当的地方

缩进

  1. 使用 Tab 进行缩进,8 个字符宽。

缩进的目的是体现代码的层级,通过概览代码的缩进层次,可以基本了解代码执行流程的复杂度。如果缩进太深,往往意味着流程复杂,这个时候就需要对代码进行重构,典型的,extract 到单独的函数中。

现代的显示器早已经能够容纳超过 80 个字符宽度的代码行了,因此 8 个字符宽的 Tab 缩进不会占用太多空间。

C 语言结构非常简单,应该限制缩进低于 4 个层级;C++ 稍微复杂点,可以限制在 5 个层级内。

  1. 何时增加缩进
时机 indent + 1
进入命名空间 no
类访问描述符 no
复合类型(类、结构体、枚举等)成员 yes
作用域块内(函数体,控制结构体等) yes
switch case no,case 关键字一般不进行缩进,缩进其后的代码
goto 标签 no,和所在函数统一
单独成行的函数参数 yes

注释

尽量避免使用块注释(避免嵌套问题)。使用行注释进行文档的编写。为了区分一般注释和文档,建议文档注释使用三个斜杆 /// 开始行注释。

不要使用行尾注释,因为你没法保证注释够短,多行的行尾注释编辑起来非常麻烦。

/// A segment class
class Segment
{
public:
        /// get length of this segment
        int length();
private:
        // first endpoint
        Point p1;
        // second endpoint
        Point p2;
};

文档注释

尽量把文档写在代码里,这样文档才是‘活的’。

只应该给对外接口编写文档,包括针对某个接口的,以及一般的概述、介绍等之类的。

文档注释一般都在其接口的前面开始,之间没有空行。文档应该具有针对性:类的文档应该只描述这个类,不包括公开的接口等。

针对函数和方法,建议单独为参数编写文档,即参数单独成行,行前编写参数的文档:

/// do something with string.
/// return true if succeed.
bool public_fun(
        /// the string to examing
        char const *str,
        /// count of chars of `str`
        int count
);

文档的编写尽量统一格式,比如可以用 markdown 等。

代码规模

函数和方法

一个函数和方法的实现不要太长,除非内部是比较简单的结构,比如非常多 case 的 switch。努力控制函数的实现在半页(屏)内(这种说法比较笼统,但具体的行数比较难把握具体多少,所以还是看自己平时的编辑器有多高吧),多了就要重构。

源文件

努力控制一个源文件代码行规模,举个极端的例子:GNU 建议每个函数的实现放在单独的源文件里。是的,很少有人(项目、团队)会采取这种方式。但努力做到这点的 coders 往往享受着庞大项目却能快速编译的好处。

命名

花括号

限定符

类型定义