(注:本文翻译自《C++ template:the complete guide》的Appendix A)
Appendix A. The One-Definition Rule
被 亲切的称为ODR的One-Define-Rule是构建良好的C++程序的基础。ODR常见的表现形式很容易理解和使用:对non-inline function在所有文件中保证只存在一处定义;对于类和内联函数,在每个TU中最多被定义一次,而且要保证不同TU之间定义的一致性。
然而,魔鬼存在于细节之中;当与模板机制相结合时,这些细节可能会变得令人沮丧。该附录用于为感兴趣的读者提供ODR的全面概览。
A.1 编译单元(Translation Units)
我们实际编写C++程序时是在文件中写入代码。然而对于ODR原则,文件之间的边界并不很重要;真正重要的是所谓的编译单元(Translation Units)。本质上来说,一个编译单元是对程序员提交给编译器的文件执行预处理后的结果。预处理器按照条件编译命令((#if,#ifdef)去除掉未 被选中的代码块,去除注释,递归的插入"#include"指令所引用的文件,并将宏展开。
因此,假设有如下两个文件





















从OCR所关心的角度来看,这两个文件和下面的单一文件是等价的







要建立跨跨编译单元的联系,需要在两个编译单元中将相应的声明设置为external linkage,或者借助被导出的模板的实例化过程中的ADL
注意编译单元的概念比起"预处理后的文件"要更为抽象一些。例如,如果我们将同一个"预处理后的文件”提交给编译器两次,结果会在程序中存在两个不同的编译单元(这么作毫无意义,所以不要去做)。
A.2 Declarations and Definitions
术语"声明”和"定义”在程序员的交流中经常交替使用。然而对于ODR来说,这些术语的确切含义是很重要的。
C++中的声明是用于在成语中引入或重新引入一个名字。声明也可以是定义,这依赖于它所引入的实体和引入的方式
1) Namespace and namespace aliases
声明就是定义,尽管在这个上下文中术语"定义”的含义不太寻常,因为名字空间的成员列表可以在这之后被扩展(这点于class和enumeration不一样)。
2) Class,Class template,functions,function templates,member functions,member function templates
声明同时也是定义,当且仅当声明中包括"brace-enclosed body"。该规则适用于union,运算符,成员运算符,静态成员函数,构造函数和析构函数,以及模板的显式特化版本。
3) Enumeration:
声明也是定义,当且仅当声明中包含"brace-enclosed list of enumerators”
4) Local variables and nonstatic data members
这些实体的声明总是可以被视为定义,虽然差别很少起作用
5) Global variables
如果声明前面没有加关键字extern 或者该声明包含初始化,全局变量的声明同时也是该变量的定义
6) Static data members
声明也是定义,当且仅当该声明出现在其所属的类或类模板的外部
7) Typedefs, using-declarations, and using-directives
这些永远不是定义,尽管typedef可以与class或union的定义组合使用
8) Explicit instantiation directives
它们被视为定义
A.3 The One-Definition Rule in Detail
正如我们在该附录的介绍部分所暗示的,实际规则中存在很多的细节。我们根据范围(scope)来组织ODR规则的各种约束。
A.3.1 One-per-Program Constraints
如下实体在整个程序中最多存在一个定义:
1). non-inline 函数和 non-inline 成员函数
2). 具有external linkage的变量(即,在名字空间范围和全局范围内声明的变量,以及带有static修饰符的变量)
3). 类的静态数据成员
4). non-inline 函数模板,non-inline成员函数模板和通过export声明的类模板的non-inline成员
5). 类模板的静态数据成员,当它们通过export声明时
例如,下面这个包含两个编译单元的C++程序是非法的





该规则不作用于具有internal linkage的实体(即,在匿名名字空间中、或在全局空间中使用static修饰符声明的实体),因为即使这样的两个实体有同样的名称,它们也被视为不 同的。同样的,在不同编译单元中大的匿名名字空间中声明的同名实体,也被视为不同。
(译注:此处有误,匿名名字空间中声明的实体仍然是默认具备external linkage)。
例如,下面两个编译单元可以合并成一个合法的C++程序:




























此外,如果程序中"使用"了前述某实体,则该实体在程序中必须存在且只存在一份定义。这里的"使用"一词的精确意识是:在程序中的某处存在对实体的"引用 "。这里的"引用"不应理解为C++中狭义的引用;它可以是对变量值的访问,或者是对实体地址的访问;可以是在源码中显式出现的,也可能是隐式的。例如, 一个new表达式在构造函数抛出异常、要求清理已分配但尚未使用的内存是,会隐式的调用相关的delete运算符来完成必要的处理。另一个例子则是复制构 造函数,即使可以被优化掉,仍然要求其定义的存在。虚函数也被隐式使用(被实现虚函数调用机制的内部结构),除非它们是纯虚函数。还存在其他类型的隐式使 用,但我们这里为了简明起见略去了。
有两种"引用",不构成前述的"使用":第一种是对实体的引用作为sizeof运算符的一部分出现;第二种类似但有些变化:如果引用作为typeid运算 符的一部分出现,那么就不构成前述的"使用",除非typeid运算符的操作对象是一个多态对象(即具有虚函数)
例如,考虑下面这个单文件程序:



















这是一个合法的C++程序,当且仅当符号DYNAMIC是未定义符号的时候。实际上此时,变量d是未定义的,然而在"sizeof(d)"中出现的对d 的"引用"并不构成"使用";而"typeid(d)"中对d的引用,当且仅当d是一个多态类型的对象是才构成"使用"(这是因为通常来说直到运行时才能 够判断出typeid运算符的运算结果)
根绝C++标准,在本节描述的约束并不要求C++实现(编译器)给出诊断信息;实际上,它们几乎总是由连接器来报告重复或缺少定义。
A.3.2 One-per-Translation Unit Constraints
在一个编译单元中,实体最到被定义一次。因此下面的例子在C++中是非法的:




这是在头文件中用所谓的guard将代码保护起来的主要原因之一。





这样的guard能够保证:该头文件在同一编译单元中第二次被"#include"指令引入时,内容被忽略,因此避免了对其包含的任何类、inline函数或模板的重复定义。
ODR原则还规定,某些实体必须在某些环境中定义。这点对于class,inline function和non-export template成立。在下面几段,我们将详细的描述规则
一个class类型 X(包括struct和union),必须在一个编译单元中对其的如下任何使用之前被定义:
1) 创建一个类型为X的对象(例如,通过变量声明或new表达式)。创建可能是间接的,例如,当一个内部包含类型X的对象的对象被创建的时候。
2) 声明类型X中的数据成员
3) 对一个类型为X的对象使用sizeof或typeid运算符
4) 显式或隐式的访问类型X的成员
5) 将一个表达式转换为类型X或反过来,或者是到类型 X*,X&的相互转换
6) 对类型X的对象进行赋值
7) 定义或调用函数,该函数具有类型为X的参数;只是对函数进行声明并不需要该类型的定义
上述规则同样适用于由类模板生成的类型X,这意味着相关模板在这些情形下都必须具备定义。这些情形构成了所谓的实例化点(POI)
inline函数必须在每它们被使用(被调用或取地址)的每个编译单元中被定义。然而,于class类型不同,它们的定义可出现在使用点之后。










尽管这是合法的C++代码,某些编译器实际上不会对尚未看见函数体的函数调用实行inline,因此,预期的效果可能不会实现。
类似于类模板,对参数化的函数声明(函数模板、成员函数模板或者类模板的成员函数)所生成的函数的调用,构成了一个POI。然而,与类模板不同的是,相应的定义可以出现在POI之后(如果使用了export的话,甚至根本不用出现)。
本节中解释的ODR的各个方面可以很容易的在C++编译器中进行检查。因此,C++标准要求当规则之一被违背时编译器生成诊断信息。一个例外是缺少non-exported 参数化函数的定义,这样的情况通常不生成和诊断信息。
A.3.3 Cross-Translation Unit Equivalence Constraints
对于某些实体,允许在多个编译单元中存在定义引入了出现一种新的错误的可能:多个定义之间的不匹配。不行的是,在传统的一次处理一个编译但愿的编译器技术 下,这样的错误是难以检查的。因此,C++标准并不强制多个定义之间的不同被检测到或生成诊断信息(当然,它允许!)。然而,如果跨编译单元的约束被违背 的话,C++标准限定这将导致undefined behaviour,即意味着任何合理或不合理的事情都可能发生。通常,这样未检测到的错误可能导致程序崩溃或错误的结果,但是原则上它们可能引发其他更 直接的损害(例如,文件损坏)。
跨编译单元的约束规定:当一个实体在两个地点被定义时,两个地点必须有完全相同的一串符号(关键字,运算符,标识符以及其他预处理后的符号)。而且,这些符号必须在各自的上下文中具备一致的意思(例如,标识符都指代同一个变量)。
考虑下面的例子:



















这个例子是非法的,因为尽管两个单元中内联函数increase_counter的符号串看起来一样,它们含有一个指向不同实体的符号:counter。 实际上,因为这两个名为counter的变量具有internal linkage(static修饰符),尽管有相同的名字,他们是无关的。注意,尽管两个内联函数都未被调用嗯,这依然是个错误。
将可以在多个编译单元中定义的实体的定义放入头文件,并在任何需要的时候通过"#include"命令将头文件引入,能够保证在绝大多数情况下符号序列是 相同的。使用这种方法,两个相同符号指代不同实体的情况会变得相当罕见,但是当它确实发生是,产生的错误通常是神秘而难以追踪的
跨编译单元的约束的作用对象不仅包括可以在多个地点定义的实体,还包括声明中的默认参数。换句话说,下面的程序属于undefined behavior。








这里需要注意符号序列的等价性有时可能涉及微妙的隐式效果。下面的例子引自C++标准:




































在这个例子中,问题的发生是因为隐式生成的class D的构造函数在两个编译单元中是不同的。一个调用带有一个参数的X的构造函数,而另一个调用带有两个参数的X的构造函数。如果说这个例子有什么实际意义的 话,那就是将默认参数的声明限制在一个位置(如果可能的话,应该位于头文件中)。幸运的是在实际使用中,在类外定义中使用默认参数是很罕见的。
对于"相同符号必须指代同一实体"这一约束还存在一个例外。如果相同符号指代具有相同值的不相关的常量,而且并未对表达式的结果取地址,那么相同符号被认为是定价的。这条例外允许编写如下的结构:










原 则上说,当这个头文件被包含进两个不同的编译单元是,两个不同的名为length的常量(constant variable)被创建,因为const默认static。然而,这样的常量经常用于定义编译时常值,而不是某个运行时特定的存储位置。因此,如果我们 不强制这样的存储位置存在(通过引用变量的地址),两个常量具备相同的值就足够了。ODR的这条例外只对整数和枚举类型的值有效(浮点和指针类型不适用)
最 后,关于模板的一点注意。模板中的名字綁定是分两阶段几行的。所谓的非从属(non-dependent)名字是在模板被定义处进行綁定的。对于这些名 字,同样和类似的应用等价规则。对于在实例化时进行綁定的名字,等价规则必须在那个时刻应用,并且綁定必须是等价的。这导致了一个微妙的结果:尽管使用了 export的模板只在一处进行定义,他们可能拥有多个必须遵守等价规则的实例。
这里是一个特意构造的对ODR规则的违反:

































































要 理解这个例子,我们必须记住在匿名名字空间中定义的函数具有external linkage,但是他们与其他编译单元的匿名空间中的任何函数都不同。因此,例子中的两个paint()函数是不同的。然而,在exported template中的函数paint()的调用有一个依赖于模板的参数,因此綁定在实例化的时候才进行。在我们的例子中,存在两处 highlight<Color>的实例化点,但是它们綁定到不同的paint函数,因此这个程序是非法的。