C++编程规范:名称空间与模块
第57条:将类型及其非成员函数接口置于同一名字空间中
- 如果要将非成员函数设计成类X的接口的一部分,那么久必须在与X相同的名字空间中定义它们,以便正确调用。
- ADL:参数依赖查找(argument-dependent lookup,也称Kornig查找),当编译器对无限定域的函数调用进行名字查找时,除了当前名字空间域外,也会把函数参数类型所处的名字空间列入查找范围。
- 接口原则:对于一个类X而言,所有在同一个名字空间中提及X和随X一起提供的函数逻辑上都是X的一部分,因为它们组成了X接口的组成部分
- 如果operator+不是在于X相同的名字空间中被声明的,那么调用者的代码将无法正确工作,调用者有两种变通方法使其工作
第一种方法:使用显式的限定:
x3 = N::operator+(x1,x2)
这种方法要求用户放弃自然的操作符语法
第二种方法:使用using语句
using N::operator+;
x3 = x1 + x2;
第58条:应该将类型和函数分为置于不同的名字空间中,除非有意让它们在一起工作
避免将不属于类型X的接口的非成员函数与X放在同一个名字空间中,尤其是绝对不要将模板化函数或者操作符与用户自定义类型放在同一名字空间中。
第59条:不要在头文件中或者#include之前编写名字空间using
- using声明只导入指定的名称,如果该名称与全局名称有冲突,编译器会报错 using指令会导入整个名字空间中所有成员的名称,包括那些根本用不到的名称。如果与局部名称发生冲突,则编译器不会发出任何警告信息,而是用局部名称自动覆盖命名空间中的同名成员
- 绝对不要编写using声明或者在#include之前编写using指令。
推论:在头文件中,不要编写名字空间级的using指令或者using声明,相反应该显式地用名字空间限定所有的名字。
不要在头文件中编写using声明的原因:头文件要被无数的实现文件所包含,当然不应该干扰其他代码的意义。
不要在#include之前使用using声明的原因:不希望干扰其他人头文件中代码的意义
- 例外情况:从老的ANSI/ISO标准化之前的标准库实现将一个大型项目移植到更新后的标准库上,可能会迫使仔细地在头文件中加入一个using指令。
第60条:要避免在不同的模块中分配和释放内存
在一个模块中分配内存,而在另一个模块中释放它,会在这两个模块之前产生微妙的远距离依赖,使程序变得脆弱。必须使用相同版本的编译器、同样的标志和相同的标准库实现对它们进行编译。 在实践中,在释放内存时,用来分配内存的模块最好仍在内存中。
第61条:不要在头文件中定义具有链接的实体
- 重复会导致膨胀,具有链接的实体(entity with linkage),包括名字空间级的变量或者函数都需要分配内存。在头文件中定义这样的实体将导致连接时的错误或者内存的浪费,要将所有具有链接的实体放入头文件中。
- 所谓具有链接的名字,是指它可能与另一作用域中某个声明所引入的名字表示同一个实体。 链接有三种情况:外部链接、内部连接和无链接。内外链接的区别在于是否能从另一编译单元中引用,无链接指名字所表示的实体不能从作用域之外引用。 判断链接的有无规则比较繁琐。但是,名字空间级的实体肯定具有内部或外部连接,在局部作用域声明的名字肯定没有外部链接。
- 不要在头文件中定义名字空间级的static实体,不要试图通过在头文件中使用未命名的名字空间来让开此问题。 ``` c++ //不要在头文件中定义具有静态链接的实体 static int fudgeFactor; static string hello(“Hello world”); static void foo(){}
//不要在头文件中使用未命名的名字空间 namespace{ int fudgeFactor; string hello(“Hello world”); void foo(){} } ```
- 例外情况:以下具有外部链接的实体可以放入头文件中:
- 内联函数。它们具有外部链接,但是连接器肯定不会拒绝多个副本
- 函数模板。它们与内联函数相似,其实例化行为与常规函数一样,只不过可以接受重复的版本
- 类模板的静态数据成员
第62条:不要允许异常跨越模块边界传播
- 不要在两段代码之间传播异常,除非能够控制用来构建两段代码的编译器和编译选项,否则模块可能无法支持可兼容地实现异常传播。
- C++标准并没有规定异常传播必须实现的方式,甚至也没有大多数系统共同遵守的事实标准。异常传播的机制不仅随着所用的操作系统和编译器的不同而异,而且对于给定操作系统上的给定编译器,也会随着构建应用程序每个模块所用编译选项的不同而不同
- 最低限度,应用程序必须在以下位置有捕获所有异常的catch(…)语句
- 在main函数的附近。捕获并用日志记录任何将使程序不正常终止而其他地方有没有捕获的异常。
- 在从无法控制的代码中执行回调附近。操作系统和程序库会提供一些框架,可以传递一个指向以后才会调用的函数的指针,不要让异常传播到回调函数之外,因为调用回调函数的代码很有可能使用不同的异常处理机制。
- 在线程边界的附近。要确保线程的mainline函数不会向系统传播异常,否则将出乎系统的意料。
- 在模块接口边界的附近。子系统会公开一些公共接口供外部使用,如果子系统要封装为一个库,那么应该使异常仅限于内部
- 在析构函数内部。析构函数不能抛出异常,如果析构函数要调用可能会抛出异常的函数,就需要防止这些异常向外泄漏。
- 错误处理策略只有在跨越模块边界时才可以改变,应该明确如何处理模块之间的策略的接口。一种好的解决方案是,定义一些中枢性的函数,在异常和子系统返回的错误代码之间进行转换,这样能够很容易地将来自对应模块的错误转换为内部使用的异常,简化了集成工作。
第63条:在模块的接口中使用具有良好可移植性的类型
- 不要让类型出现在模块的外部接口中,除非能够确保所有的客户代码都能正确地理解该类型,应该使用客户代码能够理解的最高层抽象。
- 使用的抽象层次越低,可移植性就越好,但是复杂性就越高。 所以及时在模块的外部接口中使用较低层的抽象,也还是应该始终在内部使用最高层的抽象,并在模块的边界处将其转换为低层抽象。