C++代码风格指南
当前页面的内容正在依照C++ style guide的内容进行翻译。如果您熟知页面内容并擅长翻译,欢迎协助改善或校对此页面。
Contents
背景
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里的东西,包含顺序应该这样排列:
- KlayGE/KlayGE.hpp
- dir2/foo2.hpp (推荐的位置——细节参见下面)。
- C系统文件。
- C++系统文件
- 其他库的.hpp文件。
- 你项目的.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>