C++代码风格指南

From KlayGE
Jump to: navigation, search

当前页面的内容正在依照C++ style guide的内容进行翻译。如果您熟知页面内容并擅长翻译,欢迎协助改善或校对此页面

背景

C++是KlayGE使用的主要编程语言。每个C++程序员都知道,这个语言有着很多强大的特性,但强大的同时也带来了各种复杂性。结果就是更容易出bug,并且难以阅读和维护。

本指南将详细描述各种在写C++代码的过程中该做和不该做的事情,以达到控制复杂性的目的。这些规则一方面让代码库便于管理,另一方面允许开发者高效地使用C++的语言特性。

风格,也称为可读性,是掌控我们C++代码的规则。风格这个术语也许有些不当,因为这些规则远远超过了源代码格式的范畴。

我们管理代码库的一种方式是强制保持一致性。让任何开发者可以很快地看懂和明白其他人的代码非常重要。维护一个统一的风格并遵循规则,意味着我们可以更容易地使用“模式匹配”来推断各种符号是什么意思,以及有哪些不变性。建立一个通用的、必需的范式和模式会让代码容易理解得多。在某些情况下,可能会有很好的理由需要改变一些风格的规则,但为了保证一致性,我们仍然需要保持规则不变。

本指南要解决的另一个问题是应对C++的特性膨胀。C++是一个巨大的语言,有着许多先进特性。有些时候我们约束、甚至禁止使用一些特性。我们这么做可以保持代码简单易懂,并避免那些特性可能带来的多种常见错误和问题。本指南列出了这些特性,并解释为什么要限制使用他们。

注意,本指南不是个C++教程。我们假设读者熟悉这门语言。

头文件

一般来说,每个.cpp文件都应该有一个对应的.hpp文件。也有一些常见的例外,比如单元测试和只包含一个main()函数的小.cpp文件。

正确使用头文件会对你代码的可读性、大小和性能产生巨大的改善。

下面的规则将会指引你通过多种使用头文件的陷阱。

#define防护

所有的头文件都应该同时有“#define防护”和“#pragma once”,以防止多次包含,并能加速编译。符号名的格式是_<FILE>_HPP。比如,文件foo.hpp应该有这样的防护:

#ifndef _FOO_HPP
#define _FOO_HPP

#pragma once

...

#endif		// _FOO_HPP

头文件依赖

如果前置声明就足够的话,就不要用#include。

当你包含了一个头文件,你就引入了一个依赖。每次那个头文件改变的时候你的代码就需要重新编译。如果你的头文件包含了其他头文件,那么对那些文件的任一次修改都会需要重新编译所有包含该头文件的代码。因此,包含越少越好,特别是在一个头文件里包含其它头文件。

通过使用前置声明,你可以显著地减少在你的头文件中需要包含的其他头文件数量。比如,如果你的头文件使用了File类,但不需要访问到File类的定义,你的头文件只需要前置声明一个class File,而不用#include "base/file.hpp"。在KlayGE中,所有的类和结构都在"KlayGE/PreDeclare.hpp"中进行了前置声明。你可以在你的.hpp开头#include它。

如何在头文件中使用Foo类而不访问它的定义?

  • 可以把数据成员的类型定义成Foo*或Foo&。
  • 在函数的声明(但不是定义)中,可以使用Foo类型的参数和/或返回类型。(例外之一是,如果一个参数Foo或Foo const &有非显式的、单参数构造函数,你就需要完整的定义才能支持自动类型转换。)
  • 可以声明Foo类型的静态数据成员。这是因为静态数据成员是在类定义之外进行定义的。

另一方面,如果你的类是Foo的子类或有个Foo类型的数据成员,你就必须包含Foo的头文件。

有时候,用指针(用scoped_ptr就更好了)来代替对象成员更有道理。但是,这让代码的可读性变复杂了,并增加了一个性能惩罚。所以如果只是为了减少包含头文件,可以不用做这个转换。

当然,.cpp文件一般需要他们所用的类的定义,所以需要包含一些头文件。

注意:如果在你的源文件中使用Foo符号,你应该自己引入Foo的定义,而不是通过#include或前置声明。不该依赖于不是通过直接包含的头文件带来的符号。一个例外是,如果myfile.cpp中用到了Foo,则可以在myfile.hpp里#include(或前置声明)Foo,而不是在myfile.cpp中。

内联函数

只有当函数很小,比如10行以下的时候才定义成内联函数。

定义:

如果把一个函数声明为内联,就说明允许编译器在调用的时候内联地展开它,而不是像一般的函数调用机制那样调用。

优点:

只好内联函数比较小,那么内联可以产生更有效率的二进制代码。访问和修改函数,以及其他短的、对性能很关键的函数应该内联。

缺点:

过度使用内联实际上会使程序变慢。根据函数的大小,内联会造成代码长度的增加或减少。内联一个很小的访问函数一般会减少代码长度,而内联很大的函数会显著地增加代码长度。在现代处理器上,短代码一般比较快,因为可以更好地利用指令cache。

决定:

一个相当好的经验法则是如果一个函数的代码超过10行,就不要内联。注意析构函数,他们经常比看起来的要长,因为隐含了成员和基类的构造函数调用!

另一个有用的经验法则:如果函数里有循环或switch语句,内联一般是不合算的(除非,在大部分时候循环或switch没执行过)。

有个很重要的事情是即便一个函数声明成内联了,它也不总是内联的。比如,虚函数和递归函数通常是不会内联的。递归函数也不该内联。让虚函数内联的主要原因是把他们的声明放在类里,要么为了方便,要么为了写清楚它的行为,比如访问和修改函数。

函数参数顺序

当定义一个函数的时候,参数的顺序是,输入、然后输出。

C/C++函数的参数要么是函数的输入,要么是函数的输出,或二者兼备。输入参数通常是值或常量引用,输出和输入/输出参数是非常量指针。当排列函数参数顺序时,把纯输入的参数放在所有输出参数之前。特别是,不要仅仅因为是新参数就把它加在函数后面。新的纯输入参数也要放在输出参数之前。

这不是个死规定。同时有输入和输出的参数(经常是类/结构体)会来搅混水,和平常一样,与相关函数保持一致就可能需要你调整这个规则。

名字和包含的顺序

为了可读性和避免隐藏的依赖,使用标准顺序:KlayGE的.hpp,C库,C++库,其他库的.hpp,你项目的.hpp。

所有KlayGE的公共头文件都在"Include/KlayGE"目录,并且在包含路径中。不要通过UNIX的短路径.(当前目录)或..(上级目录)。比如,KlayGE/foo.hpp应该这样包含:

#include <KlayGE/foo.hpp>

对于dir/foo.cpp或dir/foo_test.cpp这样的文件,主要目的是实现或测试dir2/foo2.hpp里的东西,包含顺序应该这样排列:

  1. KlayGE/KlayGE.hpp
  2. dir2/foo2.hpp (推荐的位置——细节参见下面)。
  3. C系统文件。
  4. C++系统文件
  5. 其他库的.hpp文件。
  6. 你项目的.hpp文件。

对于推荐的位置,如果dir/foo2.hpp漏掉了任何需要的头文件,那么编译dir/foo.cpp或dir/foo_test.cpp就会失败。因此,这条规则保证了先看到编译失败的是在这些文件上工作的人,而不是在其他包上的无辜者。

dir/foo.cpp和dir2/foo2.hpp经常在同一个目录(比如base/basictypes_test.cpp和base/basictypes.hpp),但也可能在不同的目录。

比如,KlayGE/src/foo.cpp里的包含顺序看起来像这样:

#include <KlayGE/KlayGE.hpp>

#include <KlayGE/foo.hpp>

#include <vector>

#include <boost/shared_ptr.hpp>

#include <KlayGE/Math.hpp>
#include <KlayGE/ResLoader.hpp>

范围

命名空间

鼓励在.cpp文件中使用匿名命名空间。对于具名命名空间,要根据项目来命名,可能的话再加上路径。不要用using指示符。

定义:

命名空间把全局域划分成独立的、有名字的范围,可以有效地解决全局域中的名字冲突。

优点:

命名空间提供一个(分层的)命名坐标轴,之前class已经提供了另一个(也是分层的)命名坐标轴。

例如,如果两个不同的项目都在全局域中有个类Foo,这些符号将会在编译期或运行期出现冲突。如果每个项目把代码放在自己的命名空间内,project1::Foo和project2::Foo现在是独立的符号了,不会发生冲突。

缺点:

命名空间可能会制造混乱,因为除了class提供的(分层的)命名坐标轴之外,命名空间又提供了一个(分层的)命名坐标轴。

在头文件中使用匿名命名空间经常会违反C++的单次定义法则(ODR).

决定:

根据下面的规则使用命名空间。

匿名命名空间

  • 在.cpp文件中使用匿名命名空间是允许的,甚至鼓励的,可以避免运行期名字冲突:
namespace
{
	// 在.cpp文件里

	// 命名空间里的内容需要缩进
	enum 
	{
		Unused,
		EOF,
		Error
	};       // 常用的常量

	bool AtEof()
	{
		return EOF == pos_;
	}  // 使用命名空间里的EOF
}

但是,文件域的声明中与一个特定类相关的部分可以在类中声明成类型、静态数据成员或静态成员函数,而不是匿名命名空间的成员。

  • 不要在.hpp文件中使用匿名命名空间

具名命名空间

具名命名空间应该像这样使用:

  • 命名空间可以围绕整个源文件,在include、定义/声明、以及来自其他命名空间的类的前置声明之后:
// 在.hpp文件中
namespace mynamespace
{
	// 所有的声明都在命名空间的范围内
	// 注意缩进
	class MyClass
	{
	public:
		...
		void Foo();
	};
}

// 在.cpp文件中
namespace mynamespace
{
	// 在命名空间范围内的函数定义
	void MyClass::Foo()
	{
		...
	}
}

典型的.cpp文件可能有更复杂的细节,包括需要引用其他命名空间中的类。

#include "a.hpp"

#define someflag "dummy flag"

class C;	// 在全局命名空间中类C的前置声明
namespace a
{
	class A;	// a::A的前置声明
}

namespace b
{
	...code for b...
}
  • 不要在std命名空间内声明任何东西,即使是标准库类的前置声明。在std命名空间中声明一个东西会导致未定义的行为,也就是,不可移植。要声明标准库中的东西,就包含适当的头文件。
  • 不能使用using指示符从一个命名空间中开放出所有的名字。

✗

// 禁止 -- 污染了命名空间。
using namespace foo;
  • 可以在.cpp文件的任何地方使用using声明,在.hpp文件的函数、方法和类中使用using声明。

✓

// 可以在.cpp文件中
// 在.hpp中的话,就必须在函数、方法或类中
using ::foo::bar;
  • 可以在.cpp文件的任何地方使用命名空间别名,在围绕整个.hpp文件的名字空间内的任何地方、以及函数和方法内使用命名空间别名。
// 在.cpp文件中把常用的名字缩短
namespace fbz = ::foo::bar::baz;

// 在.hpp文件中把常用的名字缩短
namespace librarian
{
	// 下面的别名可以用于任何包含着个头文件的地方
	// (在librarian命名空间中):
	// 别名应该在项目内保持一致
	namespace pd_s = ::pipeline_diagnostics::sidetable;

	inline void my_inline_function()
	{
		// 命名空间别名在函数(或方法)内
		namespace fbz = ::foo::bar::baz;
		...
	}
}

注意,在.hpp文件中的别名在所有包含那个文件的地方都是可见的。所以公共头文件(在项目外也可用的)以及它们包含的头文件应该避免定义别名,保持公共API越小越好是通用准则的一部分。

内嵌类

虽然当内嵌类也是接口的一部分时,你可以使用一个公开的内嵌类,但更好的方法是用一个命名空间来保证声明不在全局范围。

定义:

一个类可以在内部定义另一个类,也称为成员类。

class Foo
{
private:
	// Bar是一个成员类,内嵌在Foo中
	class Bar
	{
		...
	};
};

优点:

当内嵌类(或成员类)仅仅在包含它的类中使用的时候挺有用,这可以使它在包含类的范围内,而不会污染到外部域。内嵌类可以前置声明在包含类内,然后在.cpp文件内定义。这样可以避免在包含类的声明中定义内嵌类,因为内嵌类的定义通常只和实现有关。

缺点:

内嵌类可以仅仅是前置声明在包含类的声明中。因此,任何含有Foo::Bar*指针的头文件都必须包含整个Foo类的声明。

决定:

不要公开内嵌类,除非它们真的是接口的一部分,比如,一个类对于某个方法有多种选择。

非成员、静态成员和全局函数

优先选择命名空间中的非成员函数或静态成员函数,而不是全局函数。几乎不用完全的全局函数。

优点:

非成员和静态成员函数在一些情况下很有用。把非成员函数放在命名空间中,以防止污染全局命名空间。

缺点:

把那些非成员和静态成员函数作为一个新类的成员可能更有道理,特别是如果它们访问外部资源或者有明显的依赖性。

决定:

有时候很有用,甚至必须把一个函数定义成不绑定于类的某个实例上。这样的函数可以是静态成员或非成员函数。非成员函数不能依赖于外部变量,而且应该总是存在于某个命名空间中。如果建立一个类只是为了把静态成员函数归在一起,而不共享静态数据,那么就应该改用命名空间。

如果一个函数和类定义在了同一个编译单元内,当它被其他编译单元直接调用的时候,可能引入不必要的耦合性和连接期依赖。静态成员函数特别容易受这个影响。适当考虑提取出一个新类,或把这样的函数放在一个独立库的命名空间中。

如果你必须定义一个非成员函数,而它只会在它的.cpp文件中用到,就用一个匿名命名空间来限定它的范围。

局部变量

把函数的变量放在尽量小的范围内,并在声明的时候初始化。

C++允许你在函数的任何地方声明变量。我们鼓励把变量声明在尽可能小的范围内,并且尽量靠近第一次使用的地方。这会让读者更容易找到声明、看到变量的类型、以及它被初始化成什么值。特别是,应该使用初始化而不是声明后赋值。比如:

✗

int i;
i = f();	// 错误 -- 初始化和声明分开了

✓

int j = g();	// 正确 -- 声明就初始化

注意,MSVC和gcc正确地实现了for (int i = 0; i < 10; ++ i)(i的生命期只在for循环的范围内),所以你在同范围的其他循环仍可以重用i。在if和while语句内,声明范围也是这样的规则,比如:

while (const char* p = strchr(str, '/'))
{
	str = p + 1;
}

有一个需要留心的地方:如果变量是个对象,那么在每一次进入范围和建立的时候都会调用它的构造函数,每一次出范围的时候都会调用它的析构函数。

// 低效的实现:
for (int i = 0; i < 1000000; ++ i)
{
	Foo f;  // 构造和析构各会调用1000000次
	f.DoSomething(i);
}

把这样的变量声明在循环外会更高效:

Foo f;  // 构造和析构只调用一次
for (int i = 0; i < 1000000; ++ i)
{
	f.DoSomething(i);
}

静态和全局变量

class类型的静态和全局变量是禁止的:由于构造和析构的不确定顺序产生的bug往往难以寻找。在一些平台上(比如Android),带有构造函数的全局变量会直接造成崩溃。

含有静态存储周期的对象,包括全局变量、静态变量、静态类成员变量、以及函数静态变量,必须是纯数据的类型(POD): int、char、floats、指针、或POD的数组或结构体。

静态变量的类构造函数和初始化调用顺序在C++中只是部分定义而已,甚至在不同次构建之间都会改变。这回产生很难跟踪的bug。因此除了禁止class类型的全局变量,我们也禁止静态的POD变量用函数的返回值来初始化,除非那个函数(比如getenv()或getpid())本身不依赖于其他全局变量。

同样,析构函数的调用顺序被定义成了和构造函数调用相反的顺序。因为构造的顺序是不确定的,析构顺序也是不确定的。比如,在程序端一个静态变量可能已经被销毁了,但代码还在执行——可能在另一个线程——试图访问它,就会失败。或者一个静态string变量可能先于引用它的另一个变量之前析构。

结果我们只允许静态变量包含POD数据。这个规则完全禁止了vector(使用C数组来代替),或字符串(使用char const []来代替)。

如果你需要一个class类型的静态或全局变量,可以初始化一个指针(并且从来不释放)——要么从你的main()函数,要么从pthread_once()。注意那必须是个原始的指针,不是“智能”指针,因为智能指针的析构函数也会出现析构顺序问题,而那正是我们想避免的。

类是C++代码的基本单元。我们很自然会频繁地使用它。本节列出了一些在写一个类时应该和不应该做的事情。

在构造函数里做事情

一般来说,构造函数应该只是把成员变量设置成它们的初始值。其他复杂的初始化应该放到一个显式的Init()函数中。

定义:

在构造函数内进行初始化。

优点:

输入方便。不需要考虑这个类是否已经被初始化过了。

缺点:

在构造函数里做事情的问题在于:

  • 让构造函数发出错误信号很不容易,没法使用异常。
  • 如果失败了,就意味着你有了个初始化失败的对象,也就是处于一个不确定的状态。
  • 如果调用了虚函数,那么将不会调用到子类实现。即使你的类现在还没有子类,一旦以后做了修改,仍将会默默地引入一些问题,造成很多混乱。
  • 如果有人建立了这个类型的一个全局变量(这是违反规则的),构造函数的代码会在main()之前就被调用,可能会破坏构造函数代码中的一些隐含假设。

决定

如果你的对象需要非平凡的初始化,考虑用一个显式的Init()函数。特别是,构造函数不能调用虚函数、不能抛出错误、访问潜在未初始化的全局变量,等等。

默认构造函数

如果你的类定义了成员变量,但没有别的构造函数,就必须定义一个默认构造函数。否则编译器会把这件事情搞得很遭。

定义:

当不带参数新建一个类对象的时候,默认构造函数就会被调用。当调用new[]申请数组的时候,默认构造函数总是会被调到。

优点:

把结构体默认初始化成“不可能”的值,可以简化调试。

缺点:

写代码需要额外的工作。

决定

如果你的类定义了成员变量,但没有别的构造函数,就必须定义一个默认构造函数(没有参数的)。最好能把对象初始化成内部状态是一致而且有效的。

这么做的原因是如果你没有其他构造函数而且也没有默认构造函数,编译器会产生一个。编译器产生的构造函数可能无法聪明地把你的对象初始化得很好。

如果你的类继承了另一个类,但你增加了新的成员变量,不是一定要加一个默认构造函数。

显式构造函数

对于只有一个参数的构造函数,要使用C++关键字explicit.

定义:

通常,如果一个构造函数只有一个参数,它可能用作转换。比如,如果你定义了Foo::Foo(string name),然后把字符串传给一个希望得到Foo的函数,那么字符串会通过那个构造函数转换成Foo,然后把Foo传给那个函数。这看起来很方便,但同时也成为麻烦之源,你不希望出现的新对象被构造和转换。把构造函数定义成explicit就能阻止它以转换的形式被隐式调用。

优点:

避免不希望的转换。

缺点:

无。

决定

我们要求所有单参数构造函数都定义成explicit。在类定义里,总是把explicit放在单参数构造函数的前面:explicit Foo(string name);

例外情况是拷贝构造函数,除非极个别的情况,它不该是explicit。故意要透明地转换成其他类的类也是例外。这些例外需要清楚地在注释里标明。

拷贝构造函数

只在必要的时候后提供拷贝构造函数和赋值操作符。其他情况下,通过私有的拷贝构造函数和赋值操作符来禁用它们。

定义:

拷贝构造函数和赋值操作符用来建立对象的拷贝。在一些情况下,拷贝构造函数会被编译器隐式调用,比如,按值传递对象。

优点:

拷贝构造函数简化了对象拷贝。STL容器要求所有内容都必须是可以拷贝和赋值的。拷贝构造函数比CopyFrom()风格的方法更高效,因为它们合并了构造和拷贝,编译器在一些时候会去掉它们,以减少堆分配。

缺点:

C++中对象的隐式拷贝很可能成为bug和性能问题的源头。同时这也降低了可读性,因为很难追踪那个对象按值传递而不是按引用,因此也就不知道对象在哪里被修改了。

决定

很少有类需要拷贝,大部分应该没有构造函数和赋值操作符。在很多时候,指针或引用和拷贝值一样能用,并且性能更好。比如,你可以通过引用或指针来传递函数参数,而不用值,你也可以把指针而不是对象放入STL容器。

如果你的对象需要拷贝,最好提供一个拷贝方法,比如CopyFrom()或Clone(),而不是拷贝构造函数。因为这些方法不会被隐式调用。如果拷贝方法不足以对付你的情况(比如,因为性能原因,或因为你的类需要按值存入STL容器中),那么需要同时提供拷贝构造函数和赋值操作符。

如果你的类不需要拷贝构造函数和赋值操作符,你必须显式禁止它们。方法是,在类的private:里加一个拷贝构造函数和赋值操作符的空声明,但不要提供任何相应的定义(所以试图使用它们一定会导致链接错误)。比如,在class Foo:

class Foo
{
public:
	Foo(int f0, int f1);
	~Foo();

private:
	Foo(Foo const &);
	void operator=(Foo const &);
};

结构体和类

结构体仅用于只有数据的被动对象,其他都应该是类。

在C++中,struct和class关键字的行为几乎完全相同。我们给每个关键字增加了语义上的含义,所以你应该对定义的数据类型使用适当的关键字。

结构体应该用于被动对象,只包含数据,以及相关的常量,但没有任何除了访问/设置数据成员之外的功能。通过直接访问成员,而不是通过调用函数来访问/设置一个成员。函数只应该用来建立数据成员,比如,构造函数、析构函数、Initialize()、Reset()、Validate()。

如果需要更多的功能,定义成类更合适。如果还犹豫不决,就定义成类。

为了和STL一致,定义functor和trait可以使用结构体而不是类。

继承

组合经常比继承更合适。当使用继承的时候,用public。

定义:

当子类从基类继承而来的时候,它包含了基类定义的所有数据和操作。实际上,继承在C++里主要用作两种方式:实现继承——实际代码被子类继承,以及接口继承——只有方法名被继承。

优点:

在实现继承对一个现有类型进行特化的时候,它可以通过重用基类的代码来减少代码长度。因为继承是编译期声明的,你和编译器都可以明白这个操作,以及检测错误。接口继承可以用来在程序上强制一个类暴露出特定的API。在这种情况下,当一个类并没有定义API需要的方法时,编译器可以检测出错误。

缺点:

对于实现继承,因为子类的代码实现分散在了基类和子类之间,所以会更难理解。子类不能覆盖不是virtual的函数,所以子类不能改变实现。基类可能也得定义一些数据成员,所以基类的物理布局也就确定了。

决定

所有的继承都应该是public的。如果你要private继承,你应该改成包含一个基类对象作为成员。

不要过分使用实现继承,组合经常更合适。尽量把继承的使用限制在“是一个”的情况:如果Bar从Foo继承而来,是因为说Bar“是一种”Foo是合理的。

必要的话把析构函数声明为虚的。如果你的类有虚函数,构造函数也必须是虚的。

只有可能会被子类访问到的成员函数才用protect。注意数据成员应该是private。

当重定义一个继承来的虚函数时,在派生类的声明里把它显式声明成virtual。基本原理:如果virtual被省略了,读者就必须检查它的所有祖先类,才能确定这个函数是否是虚函数。

多重继承

多重继承只在一种稀有的情况下真能发挥作用。我们只有在最多一个基类有实现、其他基类都是纯接口类的时候才允许多重继承。

定义:

多重继承允许子类有多于一个基类。我们需要区分纯接口的基类和有实现的基类。

优点:

多重实现继承可以让你比单继承重用更多的代码(参见继承)。

缺点:

多重实现继承只在一种稀有的情况下真能发挥作用。当多重实现继承看似一个解决方案时,你通常可以找到另一个更明显、更干净的解决方案。

决定

只有当所有父类,除了第一个可以例外,其他都是纯接口的时候才允许多重继承。为了确保它们确实是纯接口,类名必须有Interface的后缀。

接口

满足一定条件的类可以,但不是必须,以Interface后缀为结尾。

定义:

一个类如果符合下列要求,就是一个纯接口:

  • 只含有public纯虚("= 0")方法和静态方法(析构函数看下面)。
  • 不能有非静态数据成员。
  • 不需要定义任何构造函数。如果提供了构造函数,它不能有参数而且必须是protected。
  • 如果是个子类,它只能从符合这些条件而且有Interface后缀的类继承而来。

接口类不能被直接实例化,因为它声明了纯虚方法。为了确保这个接口的所有实现都正确地销毁了,这个接口也必须声明虚析构函数(作为第一条规则的例外,析构不能是纯虚的)。细节参见Stroustrup的《The C++ Programming Language》第三版,第12.4节。

优点:

给一个类加上Interface的后缀可以让其他人知道他们不能增加函数实现或非静态数据成员。这一点在多重继承的时候尤为重要。另外,Java程序员已经非常了解接口的概念了。

缺点:

Interface后缀加长了类名,使得类较难阅读和明白。同时,接口属性算作实现细节,不该暴露给用户。

决定

只有符合上面要求的类可以以Interface结尾。但是,我们不要求反过来也成立:符合上面要求德雷不是必须以Interface结尾。

操作符重载

不要重载操作符,除了在很稀少、很特殊的情况下。

定义:

一个类可以定义操作符,比如+和/,就好象它们是内建类型一样。

优点:

可以让代码更直观,因为一个类会表现得像内建类型(比如int)。比起函数那样平淡无奇的名字,例如Equals()或Add(),重载操作符的名字则更有趣。为了让一些模板函数正确执行,你可能必须定义操作符。

缺点:

虽然操作符重载会让代码更直观,但它也有几个缺点:

  • 它会愚弄你的直觉,让你以为昂贵的操作是廉价的内建操作。
  • 重载操作符让调用的位置更加难以寻找。搜索Equals()比搜索==的相关调用容易得多。
  • 有些操作符也能接受指针,更容易引入bug。Foo + 4做的是一件事情,而&Foo + 4则会做完全不同的事情。编译器不会报告其中任何一种情况,使得debug更困难。
  • 重载也会有意外的副作用。例如,如果一个类重载了一元操作符&,它就不能安全地进行前置声明。

决定

总的来说,不要重载操作符。特别是赋值操作符(operator=)很危险,应该避免定义。如果需要的话,可以定义类似Equals()和 CopyFrom()的函数。同样,无论如何应该避免定义危险的一元操作符&,如果这个类有任何会被前置声明的可能性的话。

但是,在非常罕见的情况下,你需要重载操作符来与模板和“标准”C++类交互(比如用operator<<(ostream&, T const &)来记录日志)。如果条件完全满足的话也是可以接受的,但你应该试图尽量避免这些。特别是,不要仅仅为了让你的类可以在STL容器中用作key就重载operator==或operator<;取而代之的是,你在声明容器的时候应该建立相等和比较的仿函数类型。

有些STL算法确实需要你重载operator==,在这些情况下你也得那么做,同时在文档中标明原因。

参见拷贝构造函数函数重载

访问控制

数据成员都应该是private的,并通过需要的访问函数来访问它们。一般来说一个变量应该命名为foo_,访问函数是Foo()。你可能也需要一个修改函数Foo()。例外:静态常量数据成员(一般称为FOO)不需要私有。

访问函数的定义一般inline在头文件里。

参见继承函数名

声明顺序

在类里面使用特定的声明顺序:public:在private:之前,函数在数据成员(变量)之前,等。

类定义应该从public:区域开始,接着是protected:区域,然后是private:区域。如果任何一个区域是空的,就跳过去。

在每一区域内,声明顺序一般如下:

  • Typedef和枚举
  • 常量(静态常量数据成员)
  • 构造函数
  • 析构函数
  • 函数,包括静态函数
  • 数据成员(除了静态常量数据成员)

友元声明应该总是在私有区域,用来禁止拷贝的拷贝构造和赋值操作符应该在private:区域的结尾。它应该是类的最后一个东西。参见拷贝构造函数

在相应.cpp文件中定义的函数应该和声明顺序一致,至少尽可能一致。

不要在类定义中放入大函数定义。通常,只有琐碎的或者性能相关、而且非常短的函数可能定义成内联。细节参见内联函数

写短函数

优先采用短小专一的函数。

我们发现长函数有的时候是有用的,所以在函数长度上没有强制限制。如果一个函数超过了大概40行,就应该考虑是否可以在不影响程序结构的基础上把它拆开。

即使你的长函数现在的工作状况良好,在未来几个月内别人对它的修改可能会增加新的行为。这会引入难以寻找的bug。保持函数短小简单可以让别人更容易阅读和修改你的代码。

有时候你会发现冗长复杂的函数。不要立即修改现有代码:如果和这样的函数打交道很困难、发现了难以调试的错误、或者在不同的情况下你只要使用其中一段,可以考虑把那个函数拆成更小更好管理的片段。

其他C++特性

引用参数

所有通过引用传递的参数必须加上const。

定义: 在C里,如果一个参数需要修改变量,参数就必须是指针,也就是,int foo(int* pval)。在C++里,函数也可以定义成引用参数:int foo(int& val)。

优点: 把参数定义成引用可以避免丑陋的代码,比如++ (*pval)。对于一些类似拷贝构造函数来说是必须的。需要注意的是,不像指针,NULL不被认为是个可能的值。

缺点: 引用可能容易混淆,因为他们有值的语法,但是指针的语义。

决定: 函数参数里列出的所有引用都必须是const:

void Foo(string const & in, string* out);

实际上代码中一个很强的惯例是,输入参数是值或常量引用,输出参数是指针。输入参数可能是常量指针,但我们从来不允许非常量引用参数。

但是,有些情况下用T const *比用T const &作为输入参数更好。比如:你要传进NULL。函数往输入中存入指针或引用。记住大部分时候输入参数应该指定为T const &。用T const *会告诉读者这个输入由于某些原因需要特别对待。所以如果你选了T const *而不是T const &,就需要一个合理的理由;否则会让读者感到混乱,他们会试图寻找一个不存在的解释。

Function Overloading

Use overloaded functions (including constructors) only if a reader looking at a call site can get a good idea of what is happening without having to first figure out exactly which overload is being called.

Definition: You may write a function that takes a const string& and overload it with another that takes char const *.

class MyClass
{
public:
	void Analyze(string const & text);
	void Analyze(char const * text, size_t textlen);
};

Pros: Overloading can make code more intuitive by allowing an identically-named function to take different arguments. It may be necessary for templatized code, and it can be convenient for Visitors.

Cons: If a function is overloaded by the argument types alone, a reader may have to understand C++'s complex matching rules in order to tell what's going on. Also many people are confused by the semantics of inheritance if a derived class overrides only some of the variants of a function.

Decision: If you want to overload a function, consider qualifying the name with some information about the arguments, e.g., AppendString(), AppendInt() rather than just Append().

Default Arguments

We do not allow default function parameters, except in a few uncommon situations explained below.

Pros: Often you have a function that uses lots of default values, but occasionally you want to override the defaults. Default parameters allow an easy way to do this without having to define many functions for the rare exceptions.

Cons: People often figure out how to use an API by looking at existing code that uses it. Default parameters are more difficult to maintain because copy-and-paste from previous code may not reveal all the parameters. Copy-and-pasting of code segments can cause major problems when the default arguments are not appropriate for the new code.

Decision: Except as described below, we require all arguments to be explicitly specified, to force programmers to consider the API and the values they are passing for each argument rather than silently accepting defaults they may not be aware of.

One specific exception is when default arguments are used to simulate variable-length argument lists.

// Support up to 4 params by using a default empty AlphaNum.
string StrCat(AlphaNum const & a,
	AlphaNum const & b = EmptyAlphaNum,
	AlphaNum const &c = EmptyAlphaNum,
	AlphaNum const &d = EmptyAlphaNum);

Variable-Length Arrays and alloca()

We do not allow variable-length arrays or alloca().

Pros: Variable-length arrays have natural-looking syntax. Both variable-length arrays and alloca() are very efficient.

Cons: Variable-length arrays and alloca are not part of Standard C++. More importantly, they allocate a data-dependent amount of stack space that can trigger difficult-to-find memory overwriting bugs: "It ran fine on my machine, but dies mysteriously in production".

Decision: Use a safe allocator instead, such as scoped_ptr/scoped_array.

Friends

We allow use of friend classes and functions, within reason.

Friends should usually be defined in the same file so that the reader does not have to look in another file to find uses of the private members of a class. A common use of friend is to have a FooBuilder class be a friend of Foo so that it can construct the inner state of Foo correctly, without exposing this state to the world. In some cases it may be useful to make a unittest class a friend of the class it tests.

Friends extend, but do not break, the encapsulation boundary of a class. In some cases this is better than making a member public when you want to give only one other class access to it. However, most classes should interact with other classes solely through their public members.

Exceptions

We use C++ exceptions judiciously.

Pros:

  • Exceptions allow higher levels of an application to decide how to handle "can't happen" failures in deeply nested functions, without the obscuring and error-prone bookkeeping of error codes.
  • Exceptions are used by most other modern languages. Using them in C++ would make it more consistent with Python, Java, and the C++ that others are familiar with.
  • Some third-party C++ libraries use exceptions, and turning them off internally makes it harder to integrate with those libraries.
  • Exceptions are the only way for a constructor to fail. We can simulate this with a factory function or an Init() method, but these require heap allocation or a new "invalid" state, respectively.
  • Exceptions are really handy in testing frameworks.

Cons:

  • When you add a throw statement to an existing function, you must examine all of its transitive callers. Either they must make at least the basic exception safety guarantee, or they must never catch the exception and be happy with the program terminating as a result. For instance, if f() calls g() calls h(), and h throws an exception that f catches, g has to be careful or it may not clean up properly.
  • More generally, exceptions make the control flow of programs difficult to evaluate by looking at code: functions may return in places you don't expect. This causes maintainability and debugging difficulties. You can minimize this cost via some rules on how and where exceptions can be used, but at the cost of more that a developer needs to know and understand.
  • Exception safety requires both RAII and different coding practices. Lots of supporting machinery is needed to make writing correct exception-safe code easy. Further, to avoid requiring readers to understand the entire call graph, exception-safe code must isolate logic that writes to persistent state into a "commit" phase. This will have both benefits and costs (perhaps where you're forced to obfuscate code to isolate the commit). Allowing exceptions would force us to always pay those costs even when they're not worth it.
  • Turning on exceptions adds data to each binary produced, increasing compile time (probably slightly) and possibly increasing address space pressure.
  • The availability of exceptions may encourage developers to throw them when they are not appropriate or recover from them when it's not safe to do so. For example, invalid user input should not cause exceptions to be thrown. We would need to make the style guide even longer to document these restrictions!

Decision: Use exception to throw error, not passing a parameter or calling a function.

Run-Time Type Information (RTTI)

We do use Run Time Type Information (RTTI) carefully.

Definition: RTTI allows a programmer to query the C++ class of an object at run time.

Pros: It is useful in some unittests. For example, it is useful in tests of factory classes where the test has to verify that a newly created object has the expected dynamic type.

In rare circumstances, it is useful even outside of tests.

Cons: A query of type during run-time typically means a design problem. If you need to know the type of an object at runtime, that is often an indication that you should reconsider the design of your class.

Decision: Use RTTI carefully. If you find yourself in need of writing code that behaves differently based on the class of an object, consider one of the alternatives to querying the type.

Virtual methods are the preferred way of executing different code paths depending on a specific subclass type. This puts the work within the object itself.

If the work belongs outside the object and instead in some processing code, consider a double-dispatch solution, such as the Visitor design pattern. This allows a facility outside the object itself to determine the type of class using the built-in type system.

If you think you truly cannot use those ideas, you may use RTTI. But think twice about it. :-) Then think twice again. Do not hand-implement an RTTI-like workaround. The arguments against RTTI apply just as much to workarounds like class hierarchies with type tags.

Casting

Use C++ casts like static_cast<>(). Do not use other cast formats like int y = (int)x; or int y = int(x);.

Definition: C++ introduced a different cast system from C that distinguishes the types of cast operations.

Pros: The problem with C casts is the ambiguity of the operation; sometimes you are doing a conversion (e.g., (int)3.5) and sometimes you are doing a cast (e.g., (int)"hello"); C++ casts avoid this. Additionally C++ casts are more visible when searching for them.

Cons: The syntax is nasty.

Decision: Do not use C-style casts. Instead, use these C++-style casts.

Use static_cast as the equivalent of a C-style cast that does value conversion, or when you need to explicitly up-cast a pointer from a class to its superclass. Use const_cast to remove the const qualifier (see const). Use reinterpret_cast to do unsafe conversions of pointer types to and from integer and other pointer types. Use this only if you know what you are doing and you understand the aliasing issues. Use dynamic_cast carefully. If you need to know type information at runtime in this way outside of a unittest, you probably have a design flaw.

Preincrement and Predecrement

Use prefix form (++ i) of the increment and decrement operators with iterators and other template objects.

Definition: When a variable is incremented (++ i or i ++) or decremented (-- i or i --) and the value of the expression is not used, one must decide whether to preincrement (decrement) or postincrement (decrement).

Pros: When the return value is ignored, the "pre" form (++i) is never less efficient than the "post" form (i++), and is often more efficient. This is because post-increment (or decrement) requires a copy of i to be made, which is the value of the expression. If i is an iterator or other non-scalar type, copying i could be expensive. Since the two types of increment behave the same when the value is ignored, why not just always pre-increment?

Cons: The tradition developed, in C, of using post-increment when the expression value is not used, especially in for loops. Some find post-increment easier to read, since the "subject" (i) precedes the "verb" (++), just like in English.

Decision: For simple scalar (non-object) values there is no reason to prefer one form and we allow either. For iterators and other template types, use pre-increment.

Use of const

We strongly recommend that you use const whenever it makes sense to do so.

Definition: Declared variables and parameters can be preceded by the keyword const to indicate the variables are not changed (e.g., const int foo). Class functions can have the const qualifier to indicate the function does not change the state of the class member variables (e.g., class Foo { int Bar(char c) const; };).

Pros: Easier for people to understand how variables are being used. Allows the compiler to do better type checking, and, conceivably, generate better code. Helps people convince themselves of program correctness because they know the functions they call are limited in how they can modify your variables. Helps people know what functions are safe to use without locks in multi-threaded programs.

Cons: const is viral: if you pass a const variable to a function, that function must have const in its prototype (or the variable will need a const_cast). This can be a particular problem when calling library functions.

Decision: const variables, data members, methods and arguments add a level of compile-time type checking; it is better to detect errors as soon as possible. Therefore we strongly recommend that you use const whenever it makes sense to do so:

  • If a function does not modify an argument passed by reference or by pointer, that argument should be const.
  • Declare methods to be const whenever possible. Accessors should almost always be const. Other methods should be const if they do not modify any data members, do not call any non-const methods, and do not return a non-const pointer or non-const reference to a data member.
  • Consider making data members const whenever they do not need to be modified after construction.

However, do not go crazy with const. Something like const int * const * const x; is likely overkill, even if it accurately describes how const x is. Focus on what's really useful to know: in this case, const int** x is probably sufficient.

The mutable keyword is allowed but is unsafe when used with threads, so thread safety should be carefully considered first.

Where to put the const

Prefer int const * foo to const int* foo. It keeps the rule that const always follows the object it's describing.

Integer Types

Of the built-in C++ integer types, the only one used is int. If a program needs a variable of a different size, use a precise-width integer type from <boost/stdint.hpp>, such as int16_t.

Definition: C++ does not specify the sizes of its integer types. Typically people assume that short is 16 bits, int is 32 bits, long is 32 bits and long long is 64 bits.

Pros: Uniformity of declaration.

Cons: The sizes of integral types in C++ can vary based on compiler and architecture.

Decision: <boost/stdint.hpp> defines types like int16_t, uint32_t, int64_t, etc. You should always use those in preference to short, unsigned long long and the like, when you need a guarantee on the size of an integer. Of the C integer types, only int should be used. When appropriate, you are welcome to use standard types like size_t and ptrdiff_t.

We use int very often, for integers we know are not going to be too big, e.g., loop counters. Use plain old int for such things. You should assume that an int is at least 32 bits, but don't assume that it has more than 32 bits. If you need a 64-bit integer type, use int64_t or uint64_t.

For integers we know can be "big", use int64_t.

You should not use the unsigned integer types such as uint32_t, unless the quantity you are representing is really a bit pattern rather than a number, or unless you need defined twos-complement overflow. In particular, do not use unsigned types to say a number will never be negative. Instead, use assertions for this.

On Unsigned Integers

Some people, including some textbook authors, recommend using unsigned types to represent numbers that are never negative. This is intended as a form of self-documentation. However, in C, the advantages of such documentation are outweighed by the real bugs it can introduce. Consider:

for (unsigned int i = foo.Length() - 1; i >= 0; -- i)
{
	...
}

This code will never terminate! Sometimes gcc will notice this bug and warn you, but often it will not. Equally bad bugs can occur when comparing signed and unsigned variables. Basically, C's type-promotion scheme causes unsigned types to behave differently than one might expect.

So, document that a variable is non-negative using assertions. Don't use an unsigned type.

64-bit Portability

Code should be 64-bit and 32-bit friendly. Bear in mind problems of printing, comparisons, and structure alignment.

  • printf() specifiers for some types are not cleanly portable between 32-bit and 64-bit systems. C99 defines some portable format specifiers. Unfortunately, MSVC 7.1 does not understand some of these specifiers and the standard is missing a few, so we have to define our own ugly versions in some cases (in the style of the standard include file inttypes.h):
// printf macros for size_t, in the style of inttypes.h
#ifdef _LP64
#define __PRIS_PREFIX "z"
#else
#define __PRIS_PREFIX
#endif

// Use these macros after a % in a printf format string
// to get correct 32/64 bit behavior, like this:
// size_t size = records.size();
// printf("%"PRIuS"\n", size);

#define PRIdS __PRIS_PREFIX "d"
#define PRIxS __PRIS_PREFIX "x"
#define PRIuS __PRIS_PREFIX "u"
#define PRIXS __PRIS_PREFIX "X"
#define PRIoS __PRIS_PREFIX "o"
Type DO NOT use DO use Notes
void * (or any pointer) %lx %p
int64_t %qd, %lld %"PRId64"
uint64_t %qu, %llu, %llx %"PRIu64", %"PRIx64"
size_t %u %"PRIuS", %"PRIxS" C99 specifies %zu
ptrdiff_t %d %"PRIdS" C99 specifies %zd

Note that the PRI* macros expand to independent strings which are concatenated by the compiler. Hence if you are using a non-constant format string, you need to insert the value of the macro into the format, rather than the name. It is still possible, as usual, to include length specifiers, etc., after the % when using the PRI* macros. So, e.g. printf("x = %30"PRIuS"\n", x) would expand on 32-bit Linux to printf("x = %30" "u" "\n", x), which the compiler will treat as printf("x = %30u\n", x).

  • Remember that sizeof(void*) != sizeof(int). Use intptr_t if you want a pointer-sized integer.
  • You may need to be careful with structure alignments, particularly for structures being stored on disk. Any class/structure with a int64_t/uint64_t member will by default end up being 8-byte aligned on a 64-bit system. If you have such structures being shared on disk between 32-bit and 64-bit code, you will need to ensure that they are packed the same on both architectures. Most compilers offer a way to alter structure alignment. For both MSVC and gcc, you can use #pragma pack().
  • Use the LL or ULL suffixes as needed to create 64-bit constants. For example:
int64_t my_value = 0x123456789LL;
uint64_t my_mask = 3ULL << 48;

If you really need different code on 32-bit and 64-bit systems, use #ifdef _LP64 to choose between the code variants. (But please avoid this if possible, and keep any such changes localized.)

Preprocessor Macros

Be very cautious with macros. Prefer inline functions, enums, and const variables to macros.

Macros mean that the code you see is not the same as the code the compiler sees. This can introduce unexpected behavior, especially since macros have global scope.

Luckily, macros are not nearly as necessary in C++ as they are in C. Instead of using a macro to inline performance-critical code, use an inline function. Instead of using a macro to store a constant, use a const variable. Instead of using a macro to "abbreviate" a long variable name, use a reference. Instead of using a macro to conditionally compile code ... well, don't do that at all (except, of course, for the #define guards to prevent double inclusion of header files). It makes testing much more difficult.

Macros can do things these other techniques cannot, and you do see them in the codebase, especially in the lower-level libraries. And some of their special features (like stringifying, concatenation, and so forth) are not available through the language proper. But before using a macro, consider carefully whether there's a non-macro way to achieve the same result.

The following usage pattern will avoid many problems with macros; if you use macros, follow it whenever possible:

  • Don't define macros in a .h file.
    1. define macros right before you use them, and #undef them right after.
  • Do not just #undef an existing macro before replacing it with your own; instead, pick a name that's likely to be unique.
  • Try not to use macros that expand to unbalanced C++ constructs, or at least document that behavior well.
  • Prefer not using ## to generate function/class/variable names.

0 and NULL

Use 0 for integers, 0.0 for reals, NULL for pointers, and '\0' for chars.

Use 0 for integers and 0.0 for reals. This is not controversial.

For pointers (address values), there is a choice between 0 and NULL. Bjarne Stroustrup prefers an unadorned 0. We prefer NULL because it looks like a pointer. In fact, some C++ compilers, such as gcc 4.1.0, provide special definitions of NULL which enable them to give useful warnings, particularly in situations where sizeof(NULL) is not equal to sizeof(0).

Use '\0' for chars. This is the correct type and also makes code more readable.

sizeof

Use sizeof(varname) instead of sizeof(type) whenever possible.

Use sizeof(varname) because it will update appropriately if the type of the variable changes. sizeof(type) may make sense in some cases, but should generally be avoided because it can fall out of sync if the variable's type changes.

✓

// Good
Struct data;
memset(&data, 0, sizeof(data));

✗

// Bad
memset(&data, 0, sizeof(Struct));

C++11

Any libraries and language extensions from C++11 (formerly known as C++0x) must be inside #ifdef KLAYGE_CXX11_SUPPORT/#endif.

Definition: C++11 is the latest ISO C++ standard. It contains significant changes both to the language and libraries.

Pros: C++11 has become the official standard, and eventually will be supported by most C++ compilers. It standardizes some common C++ extensions that we use already, allows shorthands for some operations, and has some performance and safety improvements.

Cons: The C++11 standard is substantially more complex than its predecessor (1,300 pages versus 800 pages), and is unfamilar to many developers. The long-term effects of some features on code readability and maintenance are unknown. We cannot predict when its various features will be implemented uniformly by tools that may be of interest (gcc, icc, clang, Eclipse, etc.).

As with Boost, some C++11 extensions encourage coding practices that hamper readability - for example by removing checked redundancy (such as type names) that may be helpful to readers, or by encouraging template metaprogramming. Other extensions duplicate functionality available through existing mechanisms, which may lead to confusion and conversion costs.

Decision: Any C++11 libraries and language features must be separated by #ifdef KLAYGE_CXX11_SUPPORT/#endif. And implement a C++98 version for compatibility. Avoid writing code that is incompatible with C++11 (even though it works in C++03).

命名

注释

格式

这些规则的例外情况

结语