C++编程规范 名字空间与模块

来源:互联网 发布:善领电子狗车载软件 编辑:程序博客网 时间:2024/05/24 05:42

第57条 将类型及其非成员函数接口置于同一名字空间中
非成员也是函数:如果要将非成员函数(特别是操作符和辅助函数)设计成类 X 的接口的一部分,那么就必须在与 X 相同的名字空间中定义它们,以便正确调用。
详细:
1、公有成员函数和非成员函数都是类的公有接口的组成部分。接口原则是这样表述的:对于一个类 X 而言,所有在同一个名字空间中“提及” X 和“随” X “一起提供的”函数(包括非成员函数)逻辑上都是 X 的一部分,因为它们形成了 X 接口的组成部分。(见 C44)
2、之所以要在语言中增加参数依赖查找(argument-dependent lookup,ADL,也称 Koenig 查找),是因为要确保使用类型 X 的对象 x 的代码能够像使用成员函数那样使用其非成员函数接口。

第58条 应该将类型和函数分别置于不同的名字空间中,除非有意想让它们一起工作
协助防止名字查找问题:通过将类型(以及与其直接相关的非成员函数,见 C57)置于自己单独的名字空间中,可以使类型与无意的 ADL(参数依赖查找,也称 Koenig 查找)隔离开来,促进有意的 ADL。要避免将类型和模板化函数或者操作符放在相同的名字空间中。
详细:
1、遵循这一建议,不仅可以避免追踪代码中难以辨识的错误,而且还可以避免了解那些极其难以理解的语言细节——你可能永远都不需要了解的细节。

#include <vector>namespace N{struct X {};template<typename T>int * operator+(T, unsigned) { /* 进行具体工作 */ }}int main(){std::vector<N::X> v(5);v[0];// 这里可能会导致调用到 N 中的 operator+}
2、在 C++ 中,使用任何与自己不特别相关的函数(特别是模板化函数,而最特别的是操作符)在同一名字空间中定义的类型,都可能而且确实会出现这一问题。切莫为之。
3、标准库确实也存在此问题,这也正是产生此类微妙问题的原因。以史为鉴,切莫再为。

第59条 不要在头文件中或者 #include 之前编写名字空间 using
名字空间 using 是为了使我们更方便,而不是让我们用来叨扰别人的:在 #include 之前,绝对不要编写 using 声明或者 using 指令。
推论:在头文件中,不要编写名字空间级的 using 指令或者 using 声明,相反应该显式地用名字空间限定所有的名字。(第二条规则是从第一条直接得出的,因为头文件无法知道以后其他头文件会出现什么样的 #include 。)
详细:
1、简而言之:可以而且应该在实现文件中的 #include 指令之后自由地使用名字空间级的 using 声明和指令,而且会感觉良好。
2、这一建议并不适用于编写类成员级的 using 声明以导入需要的基类成员名字的情况。

第60条 要避免在不同的模块中分配和释放内存
物归原位:在一个模块中分配内存,而在另一个模块中释放它,会在这两个模块之间产生微妙的远距离依赖,使程序变得脆弱。必须使用相同版本的编译器、同样的标志(比较著名的比如用 debug 还是 NDEBUG)和相同的标准库实现对它们进行编译,实践中,在释放内存时,用来分配内存的模块最好仍在内存中。
详细:
1、跨越模块边界时——尤其是跨越无法保证能用相同的 C++ 编译器和相同构建选项编译的模块边界时,不要对释放函数(比如:operator delete 或者 std::free)做太多假设。
2、应该尽最大可能地注意在同一模块或者子系统中分配和释放内存。
3、确保删除由合适的函数进行,一个很好的方式是使用 shared_ptr 设施。

第61条 不要在头文件中定义具有链接的实体
重复会导致膨胀:具有链接的实体(entity with linkage),包括名字空间级的变量或函数,都需要分配内存。在头文件中定义这样的实体将导致连接时错误或者内存的浪费。请将所有具有链接的实体放入实现文件。
详细:
1、要避免在头文件中定义具有外部链接的实体,类似如下:
// 要避免在头文件中定义具有外部链接的实体int fudgeFactor;string hello("Hello, world!");void foo() { /* ... */ }
2、解决之道非常简单——只在头文件中放置声明即可:
extern int fudgeFactor;extern string hello;void foo();// “extern”对函数声明而言是可有可无的
而实际的定义则放在一个单独的实现文件中:
int fudgeFactor;string hello("Hello, world!");void foo() { /* ... */ }
3、同样,不要在头文件中定义名字空间级的 static 实体。
// 要避免在头文件中定义具有静态链接的实体static int fudgeFactor;static string hello("Hello, world!");static void foo() { /* ... */ }
4、不要试图通过在头文件中(滥)用未命名的名字空间来绕开此问题,因为其结果仍然是无法令人满意的。
// 在一个头文件中,这和 static 一样糟糕namespace{int fudgeFactor;string hello("Hello, world!");void foo() { /* ... */ }}
5、以下具有外部链接的实体可以放入头文件中:内联函数、函数模板、类模板的静态数据成员。
6、一种称为“Schwarz 计数器”或者“灵巧计数器(nifty counter)”的全局数据初始化技术要求在头文件中放入静态的(或未命名名字空间中的)数据。标准 I/O 流的 cin, cout, cerr 和 clog 的初始化中使用了这一技术。

第62条 不要允许异常跨越模块边界传播
不要向邻家的花园抛掷石头:C++ 异常处理没有普遍通用的二进制标准。不要在两段代码之间传播异常,除非能够控制用来构建两段代码的编译器和编译选项;否则模块可能无法支持可兼容地实现异常传播。这通常可以一言以蔽之:不要允许异常跨越模块或者子系统边界传播。
详细:
1、最低限度,应用程序必须在以下位置有捕获所有异常的 catch(...) 兜底语句,其中大多数都直接适用于模块:在 main 函数的附近、在从无法控制的代码中执行回调附近、在线程边界的附近、在模块接口边界的附近、在析构函数内部。
2、确保所有模块在内部一致地使用一种错误处理策略(最好是 C++ 异常,见 C72),而在其接口中也一致地使用另一种错误处理策略(例如,用于 C 语言的 API 的错误代码);两种策略可能会碰巧相同,但通常情况下还是不同的。
3、一种好的解决方案是,定义一些中枢性的函数,在异常和子系统返回的错误代码之间进行转换。这样就能够很容易地将来自对应模块的错误转换为内部使用的异常,简化了集成工作。
4、在本条款所提到的位置之外使用 catch(...),经常是一种不良设计的征兆,因为这意味着你想要捕获所有异常,却未必具备处理它们的专门知识(见 C74)。
5、理想情况下,错误可以在模块内部到处顺畅地传播,在跨越模块边界时转换(不得不付出的代价),在按策略设置的边界上进行处理。

第63条 在模块的接口中使用具有良好可移植性的类型
生成(模块的)边缘,必须格外小心:不要让类型出现在模块的外部接口中,除非能够确保所有的客户代码都能正确地理解该类型。应该使用客户代码能够理解的最高层抽象。
详细:
1、一般而言,如果能够控制用来构建模块和所有客户代码的编译器和编译选项,就可以使用任何类型;如果不能,就只能使用平台所提供的类型和 C++ 的内置类型(尽管如此,对于后一种情况还是应该记录所希望的大小和表示形式)。
2、在使用并非所有客户都能正确理解的类型,和使用低层抽象之间,需要进行权衡。
3、即使选择在模块的外部接口中使用较低层的抽象,也还是应该始终在内部使用最高层的抽象,并在模块的边界处将其转换为低层抽象。
4、使用的抽象层次越低,可移植性就越好,但复杂性也越高。

返回 目录

返回《C++ 编程规范及惯用法》

原创粉丝点击