定义与 ODR (单一定义规则)
定义是完全定义了声明中所引入的实体的声明。除了以下情况外的声明都是定义:
- 无函数体的函数声明:
int f(int); // 声明但不定义 f
extern const int a; // 声明但不定义 a extern const int b = 1; // 定义 b
- 在类定义中的非 inline (C++17 起) 静态数据成员的声明:
struct S { int n; // 定义 S::n static int i; // 声明但不定义 S::i inline static int x; // 定义 S::x }; // 定义 S int S::i; // 定义 S::i
struct S { static constexpr int x = 42; // 隐含 inline,定义 S::x }; constexpr int S::x; // 声明 S::x ,不是重复定义 |
(C++17 起) |
- (通过前置声明或通过在其他声明中使用详细类型说明符)对类名字进行的声明:
struct S; // 声明但不定义 S class Y f(class T p); // 声明但不定义 Y 和 T(以及 f 和 p)
enum Color : int; // 声明但不定义 Color |
(C++11 起) |
- 模板形参的声明:
template<typename T> // 声明但不定义 T
- 并非定义的函数声明中的形参声明:
int f(int x); // 声明但不定义 f 和 x int f(int x) // 定义 f 和 x { return x + a; }
- typedef 声明:
typedef S S2; // 声明但不定义 S2(S 可以是不完整类型)
using S2 = S; // 声明但不定义 S2(S 可以是不完整类型) |
(C++11 起) |
using N::d; // 声明但不定义 d
|
(C++17 起) |
|
(C++11 起) |
extern template f<int, char>; // 声明但不定义 f<int, char> |
(C++11 起) |
- 不是定义的显式特化声明:
template<> struct A<int>; // 声明但不定义 A<int>
asm 声明不定义任何实体,但它被归类为定义。
如果必要,编译器就会隐式定义默认构造函数、复制构造函数、移动构造函数、复制赋值运算符、移动赋值运算符和析构函数。
如果任何对象的定义导致出现具有不完整类型或抽象类类型的对象,那么程序非良构。
单一定义规则(ODR)
任何变量、函数、类类型、枚举类型、概念 (C++20 起)或模板,在每个翻译单元中都只允许有一个定义(其中部分可以有多个声明,但只允许有一个定义)。
在整个程序(包括所有的标准或用户定义的程序库)中,被 ODR 使用(见下文)的非 inline 函数或变量只允许有且仅有一个定义。不要求编译器诊断这条规则是否被违反,但违反它的程序的行为是未定义的。
对于 inline 函数或 inline 变量 (C++17 起)来说,在 ODR 使用了它的每个翻译单元中都需要一个定义。
在以需要将类作为完整类型的方式予以使用的每个翻译单元中,要求有且仅有该类的一个定义。
以下各种实体:类类型、枚举类型、inline 函数、inline 变量 (C++17 起)、模板实体(模板和模板成员,但不含完全模板特化),在程序中可以出现多个定义,只要满足下列条件:
- 每个定义在不同的翻译单元出现
|
(C++20 起) |
- 每个定义都由相同的记号序列构成(典型情况下是在同一个头文件中)
- 每个定义内进行的名字查找(在重载决议后)都找到相同实体,除了
- 具有内部链接或无链接的常量可以指代不同的对象,只要不 ODR 使用它们并在它们在各个定义中都具有相同的值
|
(C++11 起) |
- 被重载的运算符(包括转换,分配和解分配函数),在各个定义中都代表相同的函数(除非它们代表的是在这个定义中所定义的函数)
- 每个定义中对应的实体具有相同的语言链接(比如包含文件时并未处于某个 extern "C" 块之中)
- 如果一个
const
对象在其中一个定义中以常量被初始化,那么它在每个定义中都会以常量被初始化 - 以上规则同样适用于各个定义中的所有默认实参
- 如果该定义是带有隐式声明的构造函数的类定义,那么在每个 ODR 使用它的翻译单元中必须为基类和成员调用相同的构造函数
- 如果该定义是模板定义,那么所有这些要求一同适用于定义点的各个名字和实例化点的各个待决名
如果满足了所有这些要求,那么程序的行为如同在整个程序中只有一个定义。否则程序非良构,不要求诊断。
注意:在 C 中,类型没有全程序范围的 ODR ,而同一变量的 extern 声明甚至可以在不同翻译单元中具有不同的类型,只要它们是兼容的类型即可。在 C++ 中,用于同一个类型的声明的源代码记号必须与上述相同:如果一个 .cpp 文件定义了 struct S { int x; };
而另一个 .cpp 文件定义了 struct S { int y; };
,那么将它们链接到一起的程序的行为未定义。无名命名空间通常被用来解决这种问题。
ODR 使用
非正式地说: 1. 一个对象在它的值被读取(除非它是编译时常量)或写入,或它的地址被取,或者被引用绑定时,这个对象被 ODR 使用。 2. 使用“所引用的对象在编译期未知”的引用时,这个引用被 ODR 使用。 3. 一个函数在被调用或它的地址被取时,被 ODR 使用。 如果一个对象、引用或函数被 ODR 使用,那么程序中必须有它的定义;否则通常会有链接时错误。
struct S { static const int x = 0; // 静态数据成员 // 如果 ODR 使用它,就需要一个类外的定义 }; const int& f(const int& r); int n = b ? (1, S::x) // S::x 在此处未被 ODR 使用 : f(S::x); // S::x 在此处被 ODR 使用:需要一个定义
正式地说,
- 对 x 进行左值到右值转换产生了一个没有调用任何非平凡函数的常量表达式
- 或者 x 不是对象(即 x 是引用),或者当 x 是对象时,它是某个更大的表达式 e 的 潜在结果 之一,而这个更大的表达式要么是弃值表达式,要么对它实施了左值到右值转换
struct S { static const int x = 1; }; // 对 S::x 实施左值到右值转换产生常量表达式 int f() { S::x; // 弃值表达式不会 ODR 使用 S::x return S::x; // 实施了左值到右值转换的表达式不会 ODR 使用 S::x }
3) 如果结构化绑定作为潜在求值表达式出现,那么它被 ODR 使用。
|
(C++17 起) |
表达式 E 的 潜在结果集合 是 E 中所出现的标识表达式的(可能为空的)集合。组合起来有:
- 当 E 是标识表达式时,表达式 E 就是它唯一的潜在结果。
- 当 E 是数组下标表达式(E1[E2])且其中操作数之一是数组时,集合包含该操作数的潜在结果。
- 当 E 是形式为 E1.E2 或 E1.template E2 的指名非静态数据成员的类成员访问表达式时,集合包含
E1
的潜在结果。 - 当 E 是指名静态数据成员的类成员访问表达式时,集合包含表示该成员的标识表达式。
- 当 E 是形式为 E1.*E2 或 E1.*template E2 成员指针访问表达式,且它的第二个操作数是常量表达式时,集合包含 E1 的潜在结果。
- 当 E 是带有括号的表达式((E1))时,集合包含 E1 的潜在结果。
- 当 E 是泛左值条件表达式(E1 ? E2 : E3 且 E2 和 E3 都是泛左值)时,集合包含 E2 和 E3 的潜在结果的并集。
- 当
E
是逗号表达式(E1, E2)时,集合包含 E2 的潜在结果。 - 否则,集合为空。
struct S { static const int a = 1; static const int b = 2; }; int f(bool x) { return x ? S::a : S::b; // x 是子表达式 "x"(? 的左边)的一部分,它应用了左值到右值转换, // 但对 x 实施这个转换不产生常量表达式,所以 x 被 ODR 使用 // S::a 和 S::b 都是左值,并均作为泛左值条件表达式的结果的“潜在结果” // 该结果随即进行了为复制初始化返回值所要求的左值到右值转换, // 因此 S::a 和 S::b 未被 ODR 使用 }
本节未完成 原因:列出所有 ODR 使用产生差异的情形 |
指名函数
函数在下列情况被表达式或转换指名:
- 函数的名字在表达或转换式中出现(这包括具名函数,被重载的运算符,用户定义的转换,用户定义的布置形式的 new 运算符,以及非默认的初始化等情况),如果重载决议选择它,那么它被该表达式或转换指名,除非它是无限定的纯虚成员函数或指向纯虚函数的成员指针。
- 类的分配或解分配函数,由在表达式中出现的 new 表达式所指名。
- 类的解分配函数,由在表达式中出现的 delete 表达式所指名。
- 即便发生了复制消除,仍认为选择用于复制或移动对象的构造函数被该表达式或转换指名。在某些语境中使用纯右值不会复制或移动对象,见强制消除。 (C++17 起)
如果潜在求值的表达式或转换指名函数,那么 ODR 使用它。
指名 constexpr 函数的潜在常量求值的表达式或转换使它需要用于常量求值,这会触发预置函数的定义或函数模板特化的实例化,即使该表达式不被求值。 |
(C++11 起) |
缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
缺陷报告 | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
CWG 261 | C++98 | 多态类的解分配函数即使在程序中没有相关的 new 或 delete 表达式的时候也有可能会被 ODR 使用 |
补充 ODR 使用的场景, 覆盖构造函数和析构函数 |
CWG 678 | C++98 | 一个实体可以有多个语言链接不同的定义 | 此时行为未定义 |
CWG 1472 | C++98 | 满足在常量表达式中出现的条件的引用变量即使在立即 被应用左值到右值转换的情况下也会被 ODR 使用 |
此时它们不会被 ODR 使用 |
CWG 1614 | C++98 | 取纯虚函数地址会 ODR 使用它 | 不 ODR 使用该函数 |
CWG 1741 | C++98 | 潜在求值表达式中被立即从左值转换到右值的常量对象会被 ODR 使用 | 它们不会被 ODR 使用 |
CWG 1926 | C++98 | 数组下标表达式不传播潜在结果 | 它们会传播 |
CWG 2242 | C++98 | 不明确 const 对象只在它一部分的定义中以常量被初始化时是否违反了 ODR
|
不违反 ODR;此时该对象会以常量被初始化 |
CWG 2300 | C++11 | 不同编译单元中 lambda 表达式的闭包类型永不相同 | 在单一定义规则下可以相同 |
CWG 2353 | C++98 | 静态数据成员不是访问它的成员访问表达式的潜在结果 | 它是 |
CWG 2433 | C++14 | 程序中变量模板不能有多个定义 | 可以有 |
引用
- C++20 标准(ISO/IEC 14882:2020):
- 6.3 One definition rule [basic.def.odr]
- C++17 标准(ISO/IEC 14882:2017):
- 6.2 One definition rule [basic.def.odr]
- C++14 标准(ISO/IEC 14882:2014):
- 3.2 One definition rule [basic.def.odr]
- C++11 标准(ISO/IEC 14882:2011):
- 3.2 One definition rule [basic.def.odr]
- C++03 标准(ISO/IEC 14882:2003):
- 3.2 One definition rule [basic.def.odr]
- C++98 标准(ISO/IEC 14882:1998):
- 3.2 One definition rule [basic.def.odr]