威尼斯www.9778.com-威尼斯正版官方网站

Google C++ 编程风格指南:作用域

日期:2020-01-15编辑作者:编程人生

通常每一个 .cc 文件都有一个对应的 .h 文件. 也有一些常见例外, 如单元测试代码和只包含 main() 函数的 .cc 文件.

2.1. 名字空间

鼓励在 .cc 文件内使用匿名名字空间. 使用具名的名字空间时, 其名称可基于项目名或相对路径. 禁止使用 using 指示(using-directive)。禁止使用内联命名空间(inline namespace)。

定义:

名字空间将全局作用域细分为独立的, 具名的作用域, 可有效防止全局作用域的命名冲突.

优点:

虽然类已经提供了(可嵌套的)命名轴线 (YuleFox 注: 将命名分割在不同类的作用域内), 名字空间在这基础上又封装了一层.

举例来说, 两个不同项目的全局作用域都有一个类 Foo, 这样在编译或运行时造成冲突. 如果每个项目将代码置于不同名字空间中, project1::Foo 和 project2::Foo 作为不同符号自然不会冲突.

内联命名空间会自动把内部的标识符放到外层作用域,比如:

namespace X {
inline namespace Y {
void foo();
}
}

X::Y::foo() 与 X::foo() 彼此可代替。内联命名空间主要用来保持跨版本的 ABI 兼容性。

缺点:

名字空间具有迷惑性, 因为它们和类一样提供了额外的 (可嵌套的) 命名轴线.

命名空间很容易令人迷惑,毕竟它们不再受其声明所在命名空间的限制。内联命名空间只在大型版本控制里有用。

在头文件中使用匿名空间导致违背 C++ 的唯一定义原则 (One Definition Rule (ODR)).

结论:

根据下文将要提到的策略合理使用命名空间.

正确使用头文件可令代码在可读性、文件大小和性能上大为改观.

2.1.1. 匿名名字空间

在 .cc 文件中, 允许甚至鼓励使用匿名名字空间, 以避免运行时的命名冲突:

namespace {                             // .cc 文件中

// 名字空间的内容无需缩进
enum { kUNUSED, kEOF, kERROR };         // 经常使用的符号
bool AtEof() { return pos_ == kEOF; }   // 使用本名字空间内的符号 EOF

} // namespace

然而, 与特定类关联的文件作用域声明在该类中被声明为类型, 静态数据成员或静态成员函数, 而不是匿名名字空间的成员. 如上例所示, 匿名空间结束时用注释 // namespace 标识.

不要在 .h 文件中使用匿名名字空间.

下面的规则将引导你规避使用头文件时的各种陷阱.

2.1.2. 具名的名字空间

具名的名字空间使用方式如下:
用名字空间把文件包含, gflags 的声明/定义, 以及类的前置声明以外的整个源文件封装起来, 以区别于其它名字空间:

// .h 文件
namespace mynamespace {

// 所有声明都置于命名空间中
// 注意不要使用缩进
class MyClass {
    public:
    …
    void Foo();
};

} // namespace mynamespace

// .cc 文件
namespace mynamespace {

// 函数定义都置于命名空间中
void MyClass::Foo() {
    …
}

} // namespace mynamespace

通常的 .cc 文件包含更多, 更复杂的细节, 比如引用其他名字空间的类等.

#include “a.h”

DEFINE_bool(someflag, false, “dummy flag”);

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

namespace b {

…code for b…                // b 中的代码

} // namespace b

不要在名字空间 std 内声明任何东西, 包括标准库的类前置声明. 在 std 名字空间声明实体会导致不确定的问题, 比如不可移植. 声明标准库下的实体, 需要包含对应的头文件.

最好不要使用 using 指示,以保证名字空间下的所有名称都可以正常使用.

// 禁止 —— 污染名字空间
using namespace foo;

在 .cc 文件, .h 文件的函数, 方法或类中, 可以使用 using 声明。

// 允许: .cc 文件中
// .h 文件的话, 必须在函数, 方法或类的内部使用
using ::foo::bar;

在 .cc 文件, .h 文件的函数, 方法或类中, 允许使用名字空间别名.

// 允许: .cc 文件中
// .h 文件的话, 必须在函数, 方法或类的内部使用

namespace fbz = ::foo::bar::baz;

// 在 .h 文件里
namespace librarian {
//以下别名在所有包含了该头文件的文件中生效。
namespace pd_s = ::pipeline_diagnostics::sidetable;

inline void my_inline_function() {
  // namespace alias local to a function (or method).
  namespace fbz = ::foo::bar::baz;
  ...
}
}  // namespace librarian

注意在 .h 文件的别名对包含了该头文件的所有人可见,所以在公共头文件(在项目外可用)以及它们递归包含的其它头文件里,不要用别名。毕竟原则上公共 API 要尽可能地精简。

禁止用内联命名空间

1.1. Self-contained 头文件

头文件应该能够自给自足(self-contained,也就是可以作为第一个头文件被引入),以 .h 结尾。至于用来插入文本的文件,说到底它们并不是头文件,所以应以 .inc 结尾。不允许分离出 -inl.h 头文件的做法.

所有头文件要能够自给自足。换言之,用户和重构工具不需要为特别场合而包含额外的头文件。详言之,一个头文件要有 1.2. #define 保护,统统包含它所需要的其它头文件,也不要求定义任何特别 symbols.

不过有一个例外,即一个文件并不是 self-contained 的,而是作为文本插入到代码某处。或者,文件内容实际上是其它头文件的特定平台(platform-specific)扩展部分。这些文件就要用 .inc 文件扩展名。

如果 .h 文件声明了一个模板或内联函数,同时也在该文件加以定义。凡是有用到这些的 .cc 文件,就得统统包含该头文件,否则程序可能会在构建中链接失败。不要把这些定义放到分离的 -inl.h 文件里(译者注:过去该规范曾提倡把定义放到 -inl.h 里过)。

有个例外:如果某函数模板为所有相关模板参数显式实例化,或本身就是某类的一个私有成员,那么它就只能定义在实例化该模板的 .cc 文件里。

2.2. 嵌套类

当公有嵌套类作为接口的一部分时, 虽然可以直接将他们保持在全局作用域中, 但将嵌套类的声明置于 2.1. 名字空间 内是更好的选择.

定义: 在一个类内部定义另一个类; 嵌套类也被称为 成员类 (member class).

class Foo {

private:
    // Bar是嵌套在Foo中的成员类
    class Bar {
        …
    };

};

优点:

当嵌套 (或成员) 类只被外围类使用时非常有用; 把它作为外围类作用域内的成员, 而不是去污染外部作用域的同名类. 嵌套类可以在外围类中做前置声明, 然后在 .cc 文件中定义, 这样避免在外围类的声明中定义嵌套类, 因为嵌套类的定义通常只与实现相关.

缺点:

嵌套类只能在外围类的内部做前置声明. 因此, 任何使用了 Foo::Bar* 指针的头文件不得不包含类 Foo 的整个声明.

结论:

不要将嵌套类定义成公有, 除非它们是接口的一部分, 比如, 嵌套类含有某些方法的一组选项.

1.2. #define 保护

所有头文件都应该使用 #define 来防止头文件被多重包含, 命名格式当是: <PROJECT>_<PATH>_<FILE>_H_ .

为保证唯一性, 头文件的命名应该基于所在项目源代码树的全路径. 例如, 项目 foo 中的头文件 foo/src/bar/baz.h 可按如下方式保护:

#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
…
#endif // FOO_BAR_BAZ_H_

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

使用静态成员函数或名字空间内的非成员函数, 尽量不要用裸的全局函数.

优点:

某些情况下, 非成员函数和静态成员函数是非常有用的, 将非成员函数放在名字空间内可避免污染全局作用域.

缺点:

将非成员函数和静态成员函数作为新类的成员或许更有意义, 当它们需要访问外部资源或具有重要的依赖关系时更是如此.

结论:

有时, 把函数的定义同类的实例脱钩是有益的, 甚至是必要的. 这样的函数可以被定义成静态成员, 或是非成员函数. 非成员函数不应依赖于外部变量, 应尽量置于某个名字空间内. 相比单纯为了封装若干不共享任何静态数据的静态成员函数而创建类, 不如使用 2.1. 名字空间。

定义在同一编译单元的函数, 被其他编译单元直接调用可能会引入不必要的耦合和链接时依赖; 静态成员函数对此尤其敏感. 可以考虑提取到新类中, 或者将函数置于独立库的名字空间内.

如果你必须定义非成员函数, 又只是在 .cc 文件中使用它, 可使用匿名 namespaces`或 “static` 链接关键字 (如 static int Foo() {...}) 限定其作用域.

1.3. 前置声明

威尼斯正版官方网站,尽可能地避免使用前置声明。使用 #include 包含需要的头文件即可。

定义:

所谓「前置声明」(forward declaration)是类、函数和模板的纯粹声明,没伴随着其定义.

优点:

  • 前置声明能够节省编译时间,多余的 #include 会迫使编译器展开更多的文件,处理更多的输入。
  • 前置声明能够节省不必要的重新编译的时间。 #include 使代码因为头文件中无关的改动而被重新编译多次。

缺点:

  • 前置声明隐藏了依赖关系,头文件改动时,用户的代码会跳过必要的重新编译过程。
  • 前置声明可能会被库的后续更改所破坏。前置声明函数或模板有时会妨碍头文件开发者变动其 API. 例如扩大形参类型,加个自带默认参数的模板形参等等。
  • 前置声明来自命名空间 std:: 的 symbol 时,其行为未定义。
  • 很难判断什么时候该用前置声明,什么时候该用 #include 。极端情况下,用前置声明代替 includes 甚至都会暗暗地改变代码的含义:

如果 #include 被 B 和 D 的前置声明替代, test() 就会调用 f(void*) . * 前置声明了不少来自头文件的 symbol 时,就会比单单一行的 include 冗长。 * 仅仅为了能前置声明而重构代码(比如用指针成员代替对象成员)会使代码变得更慢更复杂.

结论:

  • 尽量避免前置声明那些定义在其他项目中的实体.
  • 函数:总是使用 #include.
  • 类模板:优先使用 #include.

至于什么时候包含头文件,参见 name-and-order-of-includes。

2.4. 局部变量

将函数变量尽可能置于最小作用域内, 并在变量声明时进行初始化.

C++ 允许在函数的任何位置声明变量. 我们提倡在尽可能小的作用域中声明变量, 离第一次使用越近越好. 这使得代码浏览者更容易定位变量声明的位置, 了解变量的类型和初始值. 特别是,应使用初始化的方式替代声明再赋值, 比如:

int i;
i = f(); // 坏——初始化和声明分离
int j = g(); // 好——初始化时声明

vector<int> v;
v.push_back(1); // 用花括号初始化更好
v.push_back(2);

vector<int> v = {1, 2}; // 好——v 一开始就初始化

注意, GCC 可正确实现了 for (int i = 0; i < 10; ++i) (i 的作用域仅限 for 循环内), 所以其他 for循环中可以重新使用 i. 在 if 和 while 等语句中的作用域声明也是正确的, 如:

while (const char* p = strchr(str, ‘/’)) str = p + 1;

Warning

如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数.

// 低效的实现
for (int i = 0; i < 1000000; ++i) {
    Foo f;                  // 构造函数和析构函数分别调用 1000000 次!
    f.DoSomething(i);
}

在循环作用域外面声明这类变量要高效的多:

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

1.4. 内联函数

只有当函数只有 10 行甚至更少时才将其定义为内联函数.

定义:

当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用.

优点:

只要内联的函数体较小, 内联该函数可以令目标代码更加高效. 对于存取函数以及其它函数体比较短, 性能关键的函数, 鼓励使用内联.

缺点:

滥用内联将导致程序变得更慢. 内联可能使目标代码量或增或减, 这取决于内联函数的大小. 内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小. 现代处理器由于更好的利用了指令缓存, 小巧的代码往往执行更快。

结论:

一个较为合理的经验准则是, 不要内联超过 10 行的函数. 谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用!

另一个实用的经验准则: 内联那些包含循环或 switch 语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或 switch 语句从不被执行).

有些函数即使声明为内联的也不一定会被编译器内联, 这点很重要; 比如虚函数和递归函数就不会被正常内联. 通常, 递归函数不应该声明成内联函数.(YuleFox 注: 递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数). 虚函数内联的主要原因则是想把它的函数体放在类定义内, 为了图个方便, 抑或是当作文档描述其行为, 比如精短的存取函数.

2.5. 静态和全局变量

禁止使用 class 类型的静态或全局变量:它们会导致难以发现的 bug 和不确定的构造和析构函数调用顺序。不过 constexpr 变量除外,毕竟它们又不涉及动态初始化或析构。

静态生存周期的对象,即包括了全局变量,静态变量,静态类成员变量和函数静态变量,都必须是原生数据类型 (POD : Plain Old Data): 即 int, char 和 float, 以及 POD 类型的指针、数组和结构体。

静态变量的构造函数、析构函数和初始化的顺序在 C++ 中是不确定的,甚至随着构建变化而变化,导致难以发现的 bug. 所以除了禁用类类型的全局变量,我们也不允许用函数返回值来初始化 POD 变量,除非该函数不涉及(比如 getenv() 或 getpid())不涉及任何全局变量。(函数作用域里的静态变量除外,毕竟它的初始化顺序是有明确定义的,而且只会在指令执行到它的声明那里才会发生。)

同理,全局和静态变量在程序中断时会被析构,无论所谓中断是从 main() 返回还是对 exit() 的调用。析构顺序正好与构造函数调用的顺序相反。但既然构造顺序未定义,那么析构顺序当然也就不定了。比如,在程序结束时某静态变量已经被析构了,但代码还在跑——比如其它线程——并试图访问它且失败;再比如,一个静态 string 变量也许会在一个引用了前者的其它变量析构之前被析构掉。

改善以上析构问题的办法之一是用 quick_exit() 来代替 exit() 并中断程序。它们的不同之处是前者不会执行任何析构,也不会执行 atexit() 所绑定的任何 handlers. 如果您想在执行 quick_exit()来中断时执行某 handler(比如刷新 log),您可以把它绑定到 _at_quick_exit(). 如果您想在 exit()和 quick_exit() 都用上该 handler, 都绑定上去。

综上所述,我们只允许 POD 类型的静态变量,即完全禁用 vector (使用 C 数组替代) 和 string (使用 const char [])。

如果您确实需要一个 class 类型的静态或全局变量,可以考虑在 main() 函数或 pthread_once() 内初始化一个指针且永不回收。注意只能用 raw 指针,别用智能指针,毕竟后者的析构函数涉及到上文指出的不定顺序问题。

Yang.Y 译注:

上文提及的静态变量泛指静态生存周期的对象, 包括: 全局变量, 静态变量, 静态类成员变量, 以及函数静态变量.

1.5. #include 的路径及顺序

使用标准的头文件包含顺序可增强可读性, 避免隐藏依赖: 相关头文件, C 库, C++ 库, 其他库的 .h, 本项目内的 .h.

项目内头文件应按照项目源代码目录树结构排列, 避免使用 UNIX 特殊的快捷目录 . (当前目录) 或 .. (上级目录). 例如, google-awesome-project/src/base/logging.h 应该按如下方式包含:

#include "base/logging.h"

又如, dir/foo.cc 的主要作用是实现或测试 dir2/foo2.h 的功能, foo.cc 中包含头文件的次序如下:

  1. dir2/foo2.h (优先位置, 详情如下)
  2. C 系统文件
  3. C++ 系统文件
  4. 其他库的 .h 文件
  5. 本项目内 .h 文件

这种优先的顺序排序保证当 dir2/foo2.h 遗漏某些必要的库时, dir/foo.cc 或 dir/foo_test.cc 的构建会立刻中止。因此这一条规则保证维护这些文件的人们首先看到构建中止的消息而不是维护其他包的人们。

dir/foo.cc 和 dir2/foo2.h 通常位于同一目录下 (如 base/basictypes_unittest.cc 和 base/basictypes.h), 但也可以放在不同目录下.

按字母顺序对头文件包含进行二次排序是不错的主意。注意较老的代码可不符合这条规则,要在方便的时候改正它们。

您所依赖的 symbols 被哪些头文件所定义,您就应该包含(include)哪些头文件,forward-declaration 情况除外。比如您要用到 bar.h 中的某个 symbol, 哪怕您所包含的 foo.h 已经包含了 bar.h, 也照样得包含 bar.h, 除非 foo.h 有明确说明它会自动向您提供 bar.h 中的 symbol. 不过,凡是 cc 文件所对应的「相关头文件」已经包含的,就不用再重复包含进其 cc 文件里面了,就像 foo.cc 只包含 foo.h 就够了,不用再管后者所包含的其它内容。

举例来说, google-awesome-project/src/foo/internal/fooserver.cc 的包含次序如下:

#include "foo/public/fooserver.h" // 优先位置

#include <sys/types.h>
#include <unistd.h>

#include <hash_map>
#include <vector>

#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/public/bar.h"

例外:

有时,平台特定(system-specific)代码需要条件编译(conditional includes),这些代码可以放到其它 includes 之后。当然,您的平台特定代码也要够简练且独立,比如:

#include "foo/public/fooserver.h"

#include "base/port.h"  // For LANG_CXX11.

#ifdef LANG_CXX11
#include <initializer_list>
#endif  // LANG_CXX11

译者 (YuleFox) 笔记

  1. cc 中的匿名名字空间可避免命名冲突, 限定作用域, 避免直接使用 using 关键字污染命名空间;
  2. 嵌套类符合局部使用原则, 只是不能在其他头文件中前置声明, 尽量不要 public;
  3. 尽量不用全局函数和全局变量, 考虑作用域和命名空间限制, 尽量单独形成编译单元;
  4. 多线程中的全局变量 (含静态成员变量) 不要使用 class 类型 (含 STL 容器), 避免不明确行为导致的 bug.
  5. 作用域的使用, 除了考虑名称污染, 可读性之外, 主要是为降低耦合, 提高编译/执行效率.

译者 (YuleFox) 笔记

  1. 避免多重包含是学编程时最基本的要求;
  2. 前置声明是为了降低编译依赖,防止修改一个头文件引发多米诺效应;
  3. 内联函数的合理使用可提高代码执行效率;
  4. -inl.h 可提高代码可读性 (一般用不到吧: D);
  5. 标准化函数参数顺序可以提高可读性和易维护性 (对函数参数的堆栈空间有轻微影响, 我以前大多是相同类型放在一起);
  6. 包含文件的名称使用 . 和 .. 虽然方便却易混乱, 使用比较完整的项目路径看上去很清晰, 很条理, 包含文件的次序除了美观之外, 最重要的是可以减少隐藏依赖, 使每个头文件在 “最需要编译” (对应源文件处 : D) 的地方编译, 有人提出库文件放在最后, 这样出错先是项目内的文件, 头文件都放在对应源文件的最前面, 这一点足以保证内部错误的及时发现了.

译者(acgtyrant)笔记

  1. 注意「using 指示(using-directive)」和「using 声明(using-declaration)」的区别。
  2. 匿名名字空间说白了就是文件作用域,就像 C static 声明的作用域一样,后者已经被 C++ 标准提倡弃用。
  3. 局部变量在声明的同时进行显式值初始化,比起隐式初始化再赋值的两步过程要高效,同时也贯彻了计算机体系结构重要的概念「局部性(locality)」。
  4. 注意别在循环犯大量构造和析构的低级错误。

译者(acgtyrant)笔记

  1. 原来还真有项目用 #includes 来插入文本,且其文件扩展名 .inc 看上去也很科学。
  2. Google 已经不再提倡 -inl.h 用法。
  3. 注意,前置声明的类是不完全类型(incomplete type),我们只能定义指向该类型的指针或引用,或者声明(但不能定义)以不完全类型作为参数或者返回类型的函数。毕竟编译器不知道不完全类型的定义,我们不能创建其类的任何对象,也不能声明成类内部的数据成员。
  4. 类内部的函数一般会自动内联。所以某函数一旦不需要内联,其定义就不要再放在头文件里,而是放到对应的 .cc 文件里。这样可以保持头文件的类相当精炼,也很好地贯彻了声明与定义分离的原则。
  5. 在 #include 中插入空行以分割相关头文件, C 库, C++ 库, 其他库的 .h 和本项目内的 .h 是个好习惯。

本系列文章

  • Google C++ 编程风格指南:头文件
  • Google C++ 编程风格指南:作用域
  • Google C++ 编程风格指南:类
  • Google C++ 编程风格指南:来自 Google 的奇技
  • Google C++ 编程风格指南:其他 C++ 特性
  • Google C++ 编程风格指南:命名约定
  • Google C++ 编程风格指南:注释
  • Google C++ 编程风格指南:格式

本系列文章

  • Google C++ 编程风格指南:头文件
  • Google C++ 编程风格指南:作用域
  • Google C++ 编程风格指南:类
  • Google C++ 编程风格指南:来自 Google 的奇技
  • Google C++ 编程风格指南:其他 C++ 特性
  • Google C++ 编程风格指南:命名约定
  • Google C++ 编程风格指南:注释
  • Google C++ 编程风格指南:格式

本文由威尼斯www.9778.com发布于编程人生,转载请注明出处:Google C++ 编程风格指南:作用域

关键词:

如何做好数据分析师的职业规划?

一、为什么要学习数据分析? 近几年来,无论是科研院所,商业巨头还是初创企业,各行各业都在大力开发或者引进...

详细>>

[转发帖子].net中上传摄像并将种种录像文件转变来.flv格式

原文见: 前公司在创立播客系统(Web程序卡塔尔国中,用到从摄像截图功效. 率先,大家布置一下文书夹.在工程的目录...

详细>>

C# 操作注册表

语言c# ,运行需要 .Net Framework 2.0 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System...

详细>>