本篇尝试设计一个优秀的 C 和 C++ 的代码风格,目的是提高代码的可维护性。
正如前文提到的,这里的代码风格的设计过程中,唯一的也是终极的原则就是“提高代码的可维护性”。直观的讲,在设计过程中,遇到某个决策时,会有多个选择,通过根据这个唯一原则评估每个选择的优先级,来选择最优的方案,因为有可能最后有多个选择,所以最优方案可以不唯一。
二元运算符和操作数之间:
// do
= b + c;
a
// do not
=b+c; a
控制语句的关键词和后面的括号之间:
// do
if (b) {
}
switch (a) {
}
// do not
if(b) {
}
switch(a) {
}
同一行的开花括号前:
// do
if (b) {
}
// do not
if(b){
}
两个函数、方法的声明、实现之间:
// 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
}
不要使用连续的空格和空行, 除非用于缩进
不要编写非常长的代码行,在适当的时机 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>>>>;
const &long_return_type_fn(int arg); ReturnType
// 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,可以每个参数单独成行,或长度适当的地方
缩进的目的是体现代码的层级,通过概览代码的缩进层次,可以基本了解代码执行流程的复杂度。如果缩进太深,往往意味着流程复杂,这个时候就需要对代码进行重构,典型的,extract 到单独的函数中。
现代的显示器早已经能够容纳超过 80 个字符宽度的代码行了,因此 8 个字符宽的 Tab 缩进不会占用太多空间。
C 语言结构非常简单,应该限制缩进低于 4 个层级;C++ 稍微复杂点,可以限制在 5 个层级内。
时机 | 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 往往享受着庞大项目却能快速编译的好处。