【译】Bitsquid代码风格 — C++


:)  喜欢编程、图形、游戏、运动。


【译者序】这篇文章是在游戏引擎工作室 – BitSquid的网站上发现的。当时该工作室出品了一段演示视频,其中用到了很多DX11的新特性。经过进一步的查找发现了这片文章。作者描述的是它们引擎开发时所用的代码风格。关于代码风格有很多规范,而且每个公司都有其自己的一套规范,但是每一种都有其自身的优势。个人认为本篇所描述的规范对代码编写十分有益,所以就将其翻译过来供大家参考,如有不当之处请通过邮件提出评论,谢谢。希望转载的朋友能够将本文的地址也给出。

原文:这里

目录:

  1. 介绍
    1. 统一风格的好处
    2. 可自由选择
    3. 一些无关紧要的风格
    4. 避免版本冲突
  2. 命名
    1. 命名很重要
    2. 命名应该是对完整信息的提炼
    3. 括号越大,命名就要越详尽
    4. 不要对命名进行缩写
    5. 请这样命名函数和变量 – like_this()
    6. 请这样命名类 – LikeThis
    7. 请这样命名成员变量 – _like_this
    8. 请这样命名宏 – LIKE_THIS
    9. 请这样命名空间 – like_this
    10. 请这样命名枚举 – LikeThis,以及枚举值 – LIKE_THIS
    11. 请这样命名文件 – like_this.cpp
    12. 标准函数
    13. 命名要合理
  3. 大括号
    1. 三种大括号的风格及其使用时机
    2. 在作用域内使用大括号以增加可读性
    3. 尽量将括号范围的代码放在一个屏幕内
    4. 使用continue、break甚至goto,避免括号嵌套过深
  4. 缩进和间距
    1. 使用四空格缩进
    2. 请慎重添加空格
    3. 对#if使用缩进
    4. 不要缩进整个文件
    5. 保持代码行的宽度在合理范围内
  5. 注释
    1. 符号//用于描述性注释,符号/*用于隐去无用代码
    2. 不要在源码中保留已废弃代码
    3. 在.h文件中添加接口文档
    4. 对接口文档使用Doxygen规则
    5. 使用注释以便提示读者
    6. 避免呆板的注释
    7. 不要在代码注释中添加过于深奥的内容
    8. 请这样对文件进行注释
    9. 请这样对类进行注释
    10. 请这样对函数进行注释
  6. 设计和实现问题
    1. 请阅读《Effective C++》
    2. 理智的优化
  7. 杂项
    1. 使用#pragma once避免头文件重复引入
    2. 使用简易脚本

1. 介绍

1.1 统一风格的好处

风格统一是十分有用的。它有益于交流,并且让代码更易读易懂。

1.2 可自由选择

本文所述的一些代码风格应该是十分有益的(就本人而言),另外一些是可以自由选择的。有时候使用这种或那种风格可能没什么理由,但还是应该仅使用其中的一个,以便保持风格的统一性。

1.3 一些无关紧要的风格

本指导的目的不是将所有可能的代码风格都介绍一遍。本文未提及的风格,可能无关紧要,您可以随意使用。如果您认为还有一些代码风格应该写入本文,请在讨论区中提出。

1.4 避免版本冲突

如果您在源码文件中看到一些明显不合标准的地方,应该将其改正。对于那些不确定的地方请保留其原样。
实际上没有必要在整个代码库中去查找并修正那些不符合标准的地方。那样做并不见得会提高效率。只要将那些你用到的代码中的违规处修正就可以了。
为了避免代码因为风格而来回改动。如果您不认可此风格,请在代码库中讨论,而不要默不作声。
如果您完全不同意本风格的某些规则,请提出修改建议,而不要默不作声。

2. 命名

2.1 命名很重要

命名是编程基础的一部分。一个好用易懂的API很大程度依赖于一个好命名。
此外,名称的变更往往比实现还要困难。特别是对于那些需要被外部程序调用的名称,如在JSON中脚本函数名和参数名。所以在取名时要多加小心。

2.2 命名应该是对完整信息的提炼

函数名或者变量名应该能够提供完整并有用的信息。它不应该包含哪些多余的信息或本身已经具有的信息(如已经是一个类就不必在名称中标明它是类)。

// 反面举例:名字提供的信息过少
char *pstoc(char *);
float x;
void Image::draw(float, float);

// 反面举例:名字提供的信息过多
char *convert_string_in_pascal_string_format_to_c_string_format(char *);
float the_speed;
void Image::draw_image_at(float, float);

// 正面举例:信息适当
char *pascal_string_to_c(char *s);
float speed;
void Image::draw_at(float x, float y);

如果你在取名时遇到了困难 – 请尽量想出一个来或者咨询一下你的朋友。千万不要使用你自己认为不合适的名字。

// 反面举例:

// link2是什么?它和link有什么区别?
void link();
void link2();

// 这样的名字什么意义都没有。
Stuff the_thing;

2.3 括号越大,命名就要越详尽

当变量越多时(它的作用域也越大),因为本身所具有的信息相对减少,从而需要给其名称中添加更多的描述。
在一个网络游戏中给玩家数变量命名时,我们该如何做呢?在比较小的域内我们将它命名为n,这样就可以避免同域内其它名称混淆了,并且能够轻易地看到它的来由。

int n = num_players();
for (int i=0; i<n; ++i)
...

如果它仅在网络类中使用,n可能代表很多意思,那么我们就要往它的名称中添加更多的信息。

int Network::num_players();

如果它要放到全局中使用,由于没有了网络类的限定,这时我们还需要在它的名称中再加入网络相关的信息:

int _num_players_in_network_game;

注意:
应该尽可能地避免使用全局变量。而当你不得不使用全局变量的情况下,你最好使该变量相对于后面的函数接口是不可见的(例如:console_server::get())。这样可以减少该变量的滥用。

2.4 不要对命名进行缩写

使用缩写名称会带来以下两个问题:

  • 很难理解它的原意。特别是还附加了一些无意义的缩写时,如:wbs2mc。
  • 一旦将缩写和非缩写混合使用时,你将来很难能够记得哪里是缩写,哪里不是。你可能对于word_pos表示word_position很容易理解,但是你可能会不指导函数中应该使用的是哪一个。如果你从不使用缩写,这个问题就不是问题了。

通常情况下不要使用缩写。但是可以有一些例外:
num_
它表示数量,例如:num_players()表示number_of_players()。
注意:上述的规则主要是限定那些对外使用的符号名。本地变量名就不必了,我们可以使用pos或者p什么的。

2.5 请这样命名函数和变量 – like_this()

这种命名风格使用小写字母,并且通过下划线来取代句子中的空格。
这种风格是最有助于阅读理解(更像日常用语)。
不要使用任何类型的匈牙利标记法。那种标记法并不理想。

2.6 请这样命名类 – LikeThis

这种方法由于有别于变量命名的方法,所以有助于我们给类的成员变量找到一个更合适的名称,如下:

Circle circle;

如果类也叫circle,那么其成员变量名该是什么呢?总不能是a_circle、the_circle或tmp吧。

2.7 请这样命名成员变量 – _like_this

这样命名可以快速地区分本地变量和成员变量,以便阅读代码。它可以让赋值和取值函数中的语法显得更自然:

Circle &circle() {return _circle;}
void set_circle(Circle &circle) {_circle = circle;}

前缀使用一个下划线就足够了,如果再添加一些字母的话对阅读不利。

  • 这个 _语句 通过 _下滑线 能够 很容易地 _阅读。
  • 但是 使用了一些 m_字母的话,阅读就不再 m_容易了,不是吗?

此外,可能有些变量就是以m开头的,那么使用_就可以很好地区分成员变量。

2.8 请这样命名宏 – LIKE_THIS

由于宏不容易被理解,容易产生歧义,这样命名就可以使宏名称更明显。(如:微软将GetText宏定义为GetTextA。)

2.9 请这样命名空间 – like_this

这中命名方法是最易于阅读的,没什么理由不用它。

2.10 请这样命名枚举 – LikeThis,以及枚举值 – LIKE_THIS

枚举与类、结构体一样,都是类型的一种,所以它们的命名规则也要一致。
枚举值会有很大的作用范围(全局或类内);而且多数情况下它的用处和宏定义的值一样;

#define ALIGN_LEFT = 0
enum {ALIGN_LEFT = 0};

正是因为枚举值和宏定义的作用范围十分相似,所以我们使用与宏同样的命名规则:

enum Align {ALIGN_LEFT, ALIGN_RIGHT, ALIGN_CENTER};

注意枚举值要用ALIGN_作前缀,原因在于枚举值的作用范围很大。(个人认为C++语言让枚举值拥有如此大的作用范围是设计上的错误。)

2.11 请这样命名文件 – like_this.cpp

还是使用这样的命名风格,理由就是它最易于阅读。
头文件(.h)应该放在与其对应的源文件(.cpp)相同的文件夹下。这也是为了方便浏览这些文件,请不要把它们分开。

2.12 标准函数

取值和赋值函数应该这样写:

Circle &circle() {return _circle;}
void set_circle(Circle &circle) {_circle = circle;}

取值函数名circle比get_circle更好,因为后者的前缀是多余。而使用set_作为前缀可以强调这个函数会改变对象的状态。

2.13 命名要合理

  • 你的命名拼写要正确。
  • 不要将“to”和“for”用“2”和“4”代替。
  • 所有的命名和注释应该使用美式英语。

3. 大括号

3.1 三种大括号的风格及其使用时机

下面有三种大括号的使用方法:

// 单行
int f() {return 3;}

// 左括号置于同一行
while (true) {
do(stuff);
more(stuff);
}

// 使用新行
int X::f()
{
return 3;
}

第一种用于取值和赋值函数,这是一种紧凑的做法。
第二种用于不止一行代码的循环代码。
第三种用于cpp文件中类和函数的声明。
虽然大括号的使用方法不是特别重要,但是大括号内包含的内容越多,就要使用更多的空格将其中的内容进行划分。

3.2 在作用域内使用大括号以增加可读性

替换如下代码:

// BAD
while (a)
if (b)
c;

应该如此:

while (a) {
if (b)
c;
}

只有在最深一级可以忽略大括号。

3.3 尽量将括号范围的代码放在一个屏幕内

同级的左右两个括号最好可以显示在一个屏幕内显示出来以增加可读性。

  • 类和命名空间中的代码可以超出一个屏幕。
  • 函数定义即便有一个清晰的结构也可能会超出一个屏幕,但是最好还是不要超出。
  • while、for和if应该保持在一个屏幕中,这样就不必麻烦地上下翻页查看代码。

3.4 使用continue、break甚至goto,避免括号嵌套过深

如果代码一行中使用了多个缩进,那么这段代码就可能很难理解。一般来讲这是因为它含了有多个循环和判断语句造成的。

// 不要这样:
for (i=0; i<parent->num_children(); ++i) {
Child child = parent->child(i);
if (child->is_cat_owner()) {
for (j=0; j<child->num_cats(); ++j) {
Cat cat = child->cat(j);
if (cat->is_grey()) {
...

我们可以使用continue写出一个较清晰的代码结构

for (i=0; i<parent->num_children(); ++i) {
Child child = parent->child(i);
if (!child->is_cat_owner())
continue;

for (j=0; j<child->num_cats(); ++j) {
Cat cat = child->cat(j);
if (!cat->is_grey())
continue;

...

对错误的检测也有可能会造成过多的缩进:

// 不要这样:
File f = open_file();
if (f.valid()) {
std::string name;
if (f.read(&name)) {
int age;
if (f.read(&age)) {
...
}
}
}

下例通过使用goto可以较好的解决此类问题:

File f = open_file();
if (!f.valid())
goto err;

std::string name;
if (!f.read(&name))
goto err;

int age;
if (!f.read(&age))
goto err;

err:

4. 缩进和间距

4.1 使用四空格缩进

四空格缩进是Visual Studio默认的方式,也是可以兼顾易读和简洁的最佳方式。

4.2 请慎重添加空格

对于条件判断语句,要在关键词和左括号间添加一个空格,还要在同一行的大括号前也加一个空格。而分号前就不必加空格了。

while (x == true) {
do_stuff();
}

运算表达式中的空格并不重要。通常我会在二元操作符前后添加空格,那些一元操作符就不加了(如数组的访问、函数的调用等等)。

z = x * y(7) * (3 + p[3]) - 8;

你也可以使用或简洁或松散的方式来编写代码,当然最好是能够通过空格将表达式中不同优先级的操作符区分开来。如下例,去除掉那些高优先级操作符前后的空格后,代码也很不错:

z = x*y(7)*(3 + p[3]) - 8;

“*”比“-”和“=”的优先级要高。而下例的做法将它们混在一起,会导致不易区分:

// 不要这样:
z=x * y(7) * (3+p [3])-8;

4.3 对#if使用缩进

默认情况下,Visual Studio编辑器会让所有的预处理宏左对齐。这种方式对于那些嵌套宏就显的十分愚蠢,它使得代码难于阅读。

// 不要这样:
void f()
{
#ifdef _WIN32
#define RUNNINGS_WINDOWS
#ifdef PRODUCTION
bool print_error_messages = true
#else
bool print_error_messages = false
#endif
#else
bool win32 = false
#endif

我们应该和编写普通C代码时一样,将那些预处理宏进行缩进:

void f()
{
#ifdef _WIN32
#define RUNNINGS_WINDOWS
#ifdef PRODUCTION
bool print_error_messages = true
#else
bool print_error_messages = false
#endif
#else
bool win32 = false
#endif

你可以将在Visual Studio中 Tools > Text Editor > C/C++ > Tabs 界面上将缩进方式从Smart变为Block方式,这样就不会使用它所默认的缩进方式。

4.4 不要缩进整个文件

当整个文件都在一个或几个命名空间中,你没必要为命名空间将整个文件进行缩进。那样的缩进不但不会有利于阅读代码,而且它会压缩代码在屏幕上的显示空间。
此外,可以在右大括号后添加一些注释,作标记用:

namespace skinny
{

void x();
...

} // namespace skinny

当命名空间并没有包含整个文件并且单屏就可以完整显示时,使用缩进会比较好。

4.5 保持代码行的宽度在合理范围内

很多编码风格文章中提到每行的长度不要超过80个字符。这样的规定实在太苛刻了。如今的显示器可以显示的行宽度已经不止80个字符了,更何况也已经没有人会去打印它了。
所以就没必要像下面这样:

// 不要这样:
int x = the + code +
is + indented +
and + I + dont +
want + to + create
+ long + lines;

要么使用较少的缩进,要么使用较宽的行。
行的宽度本身不会惹人厌,讨厌的是为看到行尾要来回滚动。所以请确保将那些重要的部分放在前边,这样就不会因为屏幕宽度而看不到。
(我认为行的宽度限制和缩进应该由编辑器自动处理,不过这可能还要几年的时间才具备此功能。)

5. 注释

5.1 符号//用于描述性注释,符号/*用于隐去无用代码

符号//最好用于你对代码的描述,因为它不会出现嵌套问题,而且很容易看到注释的内容。
符号/*用于你想废弃某段代码时。

5.2 不要在源码中保留已废弃代码

使用/*…*/注释掉那些旧的或有问题的代码。
在你认为新代码没有任何错误和性能问题时,可以通过它来暂时注释掉那些将被替换的代码。而在你的新代码已经没有任何问题后就该立刻将那些被注释掉的旧代码删除掉。
那些旧的、无用的或被注释的代码会影响代码可读性。因为你看到那些代码后会经常问自己当初为何要注释掉它们,这些注释掉的代码解决了我什么问题,等等。源码控制器会保留版本历史,所以我们无需在文件中一直保留那些被注释掉的旧代码。

5.3 在.h文件中添加接口文档

将接口(函数和类的文档)放在.h文件中。这样我们就可以通过浏览.h文件找到相对应的接口文件。
这样做的缺点在于.h文件会变的很大而不易掌握,但我们还是应该这样做。

5.4 对接口文档使用Doxygen规则

Doxygen接口文档是大家都认可的一种标准。我们可以十分方便地用引擎代码生成HTML格式的文档。
因为Javadoc语法便于输入,所以我们使用它,而不要使用QT语法。使用“@name”方式来命名方法或成员组的名称。

/// @name 时间函数

/// 程序运行的时间长度(秒)
float time();

对于接口中类的公共方法应该有相应的说明。那些有助与理解该类的私有方法和变量也应该添加说明。

5.5 使用注释以便提示读者

代码本身就应该能够起到注释的效果,而且它时常会更新,所以我们不该用其它东西去干扰它,或者说请不要为每行添加注释:

// 不要这样:

/// 返回车速度
float sp() {return _sp;}

// 根据时间和距离计算速度
s = d / t;

// 检查文件结尾
if (c == -1)

应该通过代码来解释它自己所做的事情:

float speed() {return _speed;}

speed = distance / time;

if (c == END_OF_FILE_MARKER)

代码中的注释对于读者要有提示作用。代码中它们应该出现在那些使用了一些小技巧或让人不易理解的地方。对于那些由好几个步骤组合的复杂算法,注释应该能够帮助用户对其有一个大致的了解。

// 使用使用Duff's device方法实现循环展开
// 实例请见: http://en.wikipedia.org/wiki/Duff's_device
switch (count%8)
{
case 0: do { *to = *from++;
case 7: *to = *from++;
case 6: *to = *from++;
case 5: *to = *from++;
case 4: *to = *from++;
case 3: *to = *from++;
case 2: *to = *from++;
case 1: *to = *from++;
} while ((count-=8) > 0);
}

5.6 避免呆板的注释

代码中注释的用途在于信息的传达。请不要在类和函数前出现重复呆板的注释。请确保注释言简意赅。不要重复已经由函数名表达出的信息。比如:

// 不要这样:

/// @param p1 一个点
/// @param p2 另一个点
/// @return p1和p2之间的距离
float distance(const Vector3 &p1, const Vector3 &p2);

5.7 不要在代码注释中添加过于深奥的内容

代码中的注释不应该仅仅是一种记录。它应该是一种有针对性的且详细的代码文档,如API的参考文档。
除了文档要详细,系统也可能会有一些比较深奥的内容。这些内容应该对那些新手能够起到引导作用。它应该表达出系统所含的概念、概念间的关系、目标以及设计上的不同点。
深奥的内容不应放到注释中去。它会使代码之间分割成琐碎的片段,不易阅读。我们可以把它们放到一个单独的HTML文件中,而且为了让其有别于那些代码中的注释,再使用一些漂亮的字体或样式对它做些特别的修饰。

5.8 请这样对文件进行注释

/// @file
///
/// 此文件含有一些类,这些类提供了方法去解析和生成
/// XML文件。

无需将创建日期、作者名等已经在版本控制中有的信息添加到文件注释中。
如果文件中只有一个类,就无需专门注释该文件,仅仅注释那个类就可以了。

5.9 请这样对类进行注释

/// 包含投射光线后的结果
///
/// @ingroup 物理
class RaycastResult

每一个类都应该有一个“@ingroup”以方便在Doxygen所生成的文档中浏览。
如果一个文件含有很多类或逻辑,你应该用分割线将它们分开以方便阅读。

// ----------------------------------------------------

5.10 请这样对函数进行注释

/// 从@a p1到@a p2的花费。
/// @note 在z轴上的花费将是其它的两倍。
static inline float cost(const Vector3 &p1, const Vector3 &p2)

你不必对所有的接口函数进行注释。如果函数本身已经清晰地表达出它的本意,那么注释仅提供一些额外的信息就可以了。如下例子:

// 不要这样:

/// 返回速度。
float speed();

不要反复注释函数的参数、返回值等等。那些重复的注释不但不能表达出更多的信息,而且它会破坏代码的可读性。
例如:下例就过于繁复

// 不要这样:

/************************************************************
* 名称: cost
*
* 描述: 返回由p1到p2的花费。
* 注释: 在z轴上的花费将是其它的两倍。
*
* 参数:
* p1 - 一个点
* p2 - 另一个点
* 返回值:
* 由p1到p2的花费。
*************************************************************/
static inline float cost(const Vector3 &p1, const Vector3 &p2)

6. 设计和实现问题

6.1 请阅读《Effective C++》

此书作者是Scott Meyer。其后续的版本也可以。

6.2 理智的优化

引擎中的代码没必要被优化到极致。那些每帧仅运行一次的代码只会对游戏的性能带来一点影响。不要把大量的时间花在优化代码上去。只有那些负责繁重工作的大堆代码才是你所要关注的。你可以参考分析器的结果找到出问题的关键部分。
要特别警惕那些只为效率不顾简洁的代码。你的代码可能会要花很长时间去反复优化和调试。每添加一点复杂代码,就会增加以后优化的难度。因此,现在所做的优化都会使你以后的优化过程更加艰难,所以请尽量使代码简洁。只有在十分必要的时候才去优化它。
此外还要特别注意那些优化方法所带来的改变。循环次数可能并不重要,重点在于内存访问模式和并行处理方式。实际上优化所要做的是你的代码能够尽可能地使用线性小内存;让代码能够同时并行进入多个SPU中运行;关注数据的框架和变换方式。仔细阅读关于面向数据的设计方法。

7. 杂项

7.1 使用#pragma once避免头文件重复引入

现在所有的编译器都支持#pragma once。并且它比标准的#ifndef语法更简单易读。

// 不要这样:
#ifndef _MY_UNIQUE_HEADER_NAME_H_
#define _MY_UNIQUE_HEADER_NAME_H_
...
#endif

// 请这样:
#pragma once

7.2 使用简易脚本

建立起你自己的源码控制系统,让它在每次提交代码前调用一些短小的脚本。这些脚本的作用在于去除多余的空格、规范每一行以及其它类似的清理工作。

Fork me on GitHub
关于

喜欢编程、图形、游戏、运动。

文章分类 设计 标签: , , , , , , , , , , , ,

Info

Ohloh profile for Alex Chi





Github Alex Chi

An error occured with the GitHub API. Please try again later.