Skip to content

Latest commit

 

History

History
240 lines (165 loc) · 9.77 KB

作用域.md

File metadata and controls

240 lines (165 loc) · 9.77 KB

作用域

命名空间

除了少数例外, 都应该将代码放入命名空间中. 命名空间的命名基于其项目名, 并且应该独一无二. 禁止使用 using 指示 (例如 using namespace foo). 禁止使用内联命名空间. 关于匿名命名空间, 参见匿名命名空间和静态变量.

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

优点:

命名空间提供了一种防止大型程序中的名称冲突的方法, 同时允许大多数代码使用合理的短名称.

比如, 如果两个工程在全局作用域中都有一个叫 Foo 的类, 在编译或运行时可能会造成冲突. 如果每个工程都将其代码置于不同的命名空间中, project1::Fooproject2::Foo 作为不同符号自然不会冲突, 各自命名空间中的代码在调用时仍然可以免去前缀直接调用 Foo.

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

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

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

缺点:

命名空间可能会令人困惑, 因为它们让找出名称所指定义的机制变得复杂化.

内联命名空间尤甚, 因为命名实际上并不局限于所声明的命名空间中. 内联只有在部分大型版本控制中才有用.

在某些情况下, 有必要通过其全限定名称 (fully-qualified names) 重复引用符号. 对于深层嵌套的命名空间, 这可能会导致很多混乱的情况.

结论:

根据以下规则使用命名空间:

  • 遵循 命名空间的命名 中的规则
  • 使用注释终止命名空间, 如给出的例子所示.
  • 位于头文件包含, gflags定义/声明以及其他命名空间类的前置声明之后的所有代码都应置于命名空间中
// In the .h file
namespace mynamespace {

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

}  // namespace mynamespace
// In the .cc file
namespace mynamespace {

// Definition of functions is within scope of the namespace.
void MyClass::Foo() {
  ...
}

}  // namespace mynamespace

更复杂的 .cc 文件可能有更多的细节, 比如 flag 或者 声明命名空间等.

#include "a.h"

DEFINE_FLAG(bool, someflag, false, "dummy flag");

namespace a {

using ::foo::bar;

...code for a...         // Code goes against the left margin.

}  // namespace a
  • 禁止在命名空间 std 内声明任何东西, 包括标准库类的前置声明. 在命名空间 std 内声明实体是未定义行为, 会导致不可移植等问题. 声明标准库中的实体需要包含对应的头文件.

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

// 禁止 -- 会污染命名空间.
using namespace foo;
  • 除了明确标记的只在内部使用的命名空间外, 禁止在头文件中的命名空间作用域内使用命名空间别名, 因为导入到头文件中的命名空间的任何内容都将成为该文件导出的公共API的一部分。
// 缩短 `.cc` 文件中的常用命名
namespace baz = ::foo::bar::baz;
// 缩短 `.h` 文件中的常用命名
namespace librarian {
namespace impl {  // 内部使用, 并非API的一部分.
namespace sidetable = ::pipeline_diagnostics::sidetable;
}  // namespace impl

inline void my_inline_function() {
  // namespace alias local to a function (or method).
  namespace baz = ::foo::bar::baz;
  ...
}
}  // namespace librarian
  • 禁止使用内联命名空间.

匿名命名空间和静态变量

.cc 文件中的定义不需要在该文件之外引用时, 将他们放在匿名命名空间中, 或将其声明为静态. 禁止在 .h 文件中这样做.

定义:

可以将声明置于匿名命名空间内, 将函数或者变量声明为静态来获得内部链接. 所有这样声明的内容都无法再其他文件中访问. 如果不同文件声明了同名的东西, 那么这两个实体是完全独立的.

结论:

鼓励在 .cc 文件中使用内部链接, 这样就不需要在别的地方引用了. 禁止在 .h 文件中使用内部链接.

匿名命名空间应使用和未匿名命名空间一样的格式. 在终止注释中, 将命名空间的名字留空:

namespace {
...
}  // namespace

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

尽量将非成员函数置于命名空间中; 尽量不要使用裸的全局函数. 尽量使用命名空间来对函数进行分组, 而不是使用类来实现命名空间的功能. 类的静态方法通常应与类的实例或类的静态数据密切相关.

优点:

非成员函数和静态成员函数在某些情况下可能有用. 将非成员函数置于命名空间中可以避免污染全局命名空间.

缺点:

非成员函数和静态成员函数作为一个新类的成员可能更有意义, 特别是当他们访问了外部资源 或者具有重要依赖关系的时候.

结论:

有时定义一个没有绑定到类实例的函数是有用的, 可以是静态成员函数或是非成员函数. 非成员函数不应依赖于外部变量, 并且总是存在于命名空间中. 不要创建一个不共享静态数据, 仅用于对静态成员函数进行分组的类, 如果有这种需求, 使用命名空间. 举例来说, 对于头文件 myproject/foo_bar.h, 可以这样写:

namespace myproject {
namespace foo_bar {
void Function1();
void Function2();
}  // namespace foo_bar
}  // namespace myproject

而不要这样:

namespace myproject {
class FooBar {
 public:
  static void Function1();
  static void Function2();
};
}  // namespace myproject

如果定义了一个非成员函数, 而且只需要在其 .cc 文件中使用, 通过内部链接来限制其作用域.

局部变量

将函数的变量限制在尽可能小的作用域内, 并在声明中初始化变量.

C++ 允许在函数的任何地方声明变量. 我们提倡在尽可能小的作用域中声明变量, 并且声明离初次使用的地方越近越好. 这样代码的阅读者就容易找到变量的声明, 查看变量的类型和初始化的值. 尤其注意, 应使用变量初始化的方式替代先声明再赋值, 比如:

int i;
i = f();      // 烂 -- 变量初始化和声明分离.
int j = g();  // 好 -- 声明时初始化.
std::vector<int> v;
v.push_back(1);  // 用花括号初始化更好.
v.push_back(2);
std::vector<int> v = {1, 2};  // 好 -- v 一开始就初始化.

if , whilefor 语句中需要的变量通常应该在这些语句的范围中声明, 这样就保证了变量局限在了语句的作用域中, 例如:

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 和不确定的构造和析构函数调用顺序. 但如果是 constexpr 的话是允许的, 毕竟它们又不涉及动态初始化或析构.

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

静态变量的构造函数, 析构函数和初始化的顺序在 C++ 中是不确定的, 甚至随着构建变化而变化, 会导致难以发现的 bug . 因此除了禁止类类型的全局变量之外, 我们也不允许用函数返回值来初始化 POD 变量, 除非该函数 (比如 getenv() 或 getpid()) 本身不依赖于其他全局变量.

Likewise, global and static variables are destroyed when the program terminates, regardless of whether the termination is by returning from main() or by calling exit(). The order in which destructors are called is defined to be the reverse of the order in which the constructors were called. Since constructor order is indeterminate, so is destructor order. For example, at program-end time a static variable might have been destroyed, but code still running — perhaps in another thread — tries to access it and fails. Or the destructor for a static string variable might be run prior to the destructor for another variable that contains a reference to that string.

One way to alleviate the destructor problem is to terminate the program by calling quick_exit() instead of exit(). The difference is that quick_exit() does not invoke destructors and does not invoke any handlers that were registered by calling atexit(). If you have a handler that needs to run when a program terminates via quick_exit() (flushing logs, for example), you can register it using at_quick_exit(). (If you have a handler that needs to run at both exit() and quick_exit(), you need to register it in both places.)

As a result we only allow static variables to contain POD data. This rule completely disallows std::vector (use C arrays instead), or string (use const char []).

If you need a static or global variable of a class type, consider initializing a pointer (which will never be freed), from either your main() function or from pthread_once(). Note that this must be a raw pointer, not a "smart" pointer, since the smart pointer's destructor will have the order-of-destructor issue that we are trying to avoid.