定义与 ODR (单一定义规则)
定义是声明,它完整地定义了声明引入的实体。每个声明都是一个定义,除了以下情况
- 没有函数体的函数声明
int f(int); // declares, but does not define f
extern const int a; // declares, but does not define a extern const int b = 1; // defines b
- 类定义内 非内联(自 C++17 起) 静态数据成员 的声明
struct S { int n; // defines S::n static int i; // declares, but does not define S::i inline static int x; // defines S::x }; // defines S int S::i; // defines S::i
struct S { static constexpr int x = 42; // implicitly inline, defines S::x }; constexpr int S::x; // declares S::x, not a redefinition |
(自 C++17 起) |
- 类名称的声明(通过前置声明或在另一个声明中使用详述类型说明符)
struct S; // declares, but does not define S class Y f(class T p); // declares, but does not define Y and T (and also f and p)
enum Color : int; // declares, but does not define Color |
(自 C++11 起) |
- 模板形参的声明
template<typename T> // declares, but does not define T
- 不是定义的函数声明中的形参声明
int f(int x); // declares, but does not define f and x int f(int x) // defines f and x { return x + a; }
- typedef 声明
typedef S S2; // declares, but does not define S2 (S may be incomplete)
using S2 = S; // declares, but does not define S2 (S may be incomplete) |
(自 C++11 起) |
using N::d; // declares, but does not define d
|
(自 C++17 起) |
|
(自 C++11 起) |
extern template f<int, char>; // declares, but does not define f<int, char> |
(自 C++11 起) |
- 显式特化,其声明不是定义
template<> struct A<int>; // declares, but does not define A<int>
asm 声明不定义任何实体,但它被归类为定义。
在必要时,编译器可以隐式地定义默认构造函数、复制构造函数、移动构造函数、复制赋值运算符、移动赋值运算符和析构函数。
如果任何对象的定义导致了不完整类型或抽象类类型的对象,则程序是非良构的。
目录 |
[编辑] 单一定义规则
在任何一个翻译单元中,只允许存在任何变量、函数、类类型、枚举类型、概念(自 C++20 起)或模板的一个定义(其中一些可能具有多个声明,但只允许一个定义)。
在整个程序(包括任何标准库和用户定义的库)中,需要出现每个非内联函数或变量的一个且仅一个定义,这些函数或变量是odr-使用的(见下文)。编译器不要求诊断此违规行为,但违反它的程序的行为是未定义的。
对于内联函数或内联变量(自 C++17 起),在它被odr-使用的每个翻译单元中都需要一个定义。
对于类,在类被使用且需要它是完整的任何地方,都需要一个定义。
在一个程序中,以下各项可以有多个定义:类类型、枚举类型、内联函数、内联变量(自 C++17 起)、模板实体(模板或模板的成员,但不是完整的模板特化),只要满足以下所有条件
- 每个定义出现在不同的翻译单元中。
|
(自 C++20 起) |
- 具有内部或无连接的常量可以引用不同的对象,只要它们不是 odr-使用的,并且在每个定义中具有相同的值。
|
(自 C++11 起) |
- 重载运算符,包括转换、分配和释放函数,从每个定义中引用相同的函数(除非引用在定义内定义的函数)。
- 相应的实体在每个定义中具有相同的语言连接(例如,include 文件不在 extern "C" 块内)。
- 如果一个
const
对象在任何定义中被常量初始化,则它在每个定义中都是常量初始化的。 - 上述规则适用于每个定义中使用的每个默认实参。
- 如果定义是针对具有隐式声明的构造函数的类,则 odr-使用它的每个翻译单元都必须为基类和成员调用相同的构造函数。
|
(自 C++20 起) |
- 如果定义是针对模板,则所有这些要求都适用于定义点的名称和实例化点的依赖名称。
如果满足所有这些要求,则程序的行为就好像整个程序中只有一个定义一样。否则,程序是非良构的,不需要诊断。
注意:在 C 中,类型没有程序范围的 ODR,即使同一变量的 extern 声明在不同的翻译单元中也可能具有不同的类型只要它们是兼容的。在 C++ 中,用于声明相同类型的源代码记号必须与上述描述的相同:如果一个 .cpp 文件定义了 struct S { int x; };,而另一个 .cpp 文件定义了 struct S { int y; };,则链接它们的程序的行为是未定义的。这通常通过未命名的命名空间来解决。
[编辑] 命名一个实体
如果表达式是一个表示变量的标识符表达式,则该变量被表达式命名。
在以下情况下,函数被表达式或转换命名
- 函数名称作为表达式或转换出现(包括具名函数、重载运算符、用户定义的转换、operator new 的用户定义放置形式、非默认初始化),如果它被重载决议选中,则该函数被该表达式命名,除非它是非限定纯虚成员函数或指向纯虚函数的成员指针。
- 类的分配或释放函数被出现在表达式中的new 表达式命名。
- 类的释放函数被出现在表达式中的delete 表达式命名。
- 被选择用于复制或移动对象的构造函数被认为由表达式或转换命名,即使发生复制省略。在某些上下文中,使用纯右值不会复制或移动对象,请参阅强制省略。(自 C++17 起)
潜在求值表达式或转换如果命名了一个函数,则 odr-使用该函数。
潜在常量求值表达式或转换,如果命名了一个 constexpr 函数,则使其常量求值所必需,这将触发默认函数的定义或函数模板特化的实例化,即使表达式未求值。 |
(自 C++11 起) |
[编辑] 潜在结果
表达式 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 的潜在结果在潜在结果集中。
- 否则,该集合为空。
[编辑] ODR 使用(非正式定义)
如果对象的值被读取(除非它是编译时常量)或写入,或者它的地址被获取,或者引用绑定到它,则该对象是 odr-使用的。
如果引用被使用,并且它的指示对象在编译时未知,则该引用是 odr-使用的。
如果对函数的函数调用被执行,或者它的地址被获取,则该函数是 odr-使用的。
如果一个实体是 odr-使用的,则它的定义必须存在于程序中的某个地方;违反这一点通常会导致链接时错误。
struct S { static const int x = 0; // static data member // a definition outside of class is required if it is odr-used }; const int& f(const int& r); int n = b ? (1, S::x) // S::x is not odr-used here : f(S::x); // S::x is odr-used here: a definition is required
[编辑] ODR 使用(正式定义)
变量 x,如果它被出现在点 P
的潜在求值表达式 expr 命名,则被 expr odr-使用,除非满足以下任何条件
- x 是一个在
P
点可在常量表达式中使用的引用。 - x 不是引用且(直到 C++26)expr 是表达式 E 的潜在结果集的一个元素,并且满足以下任何条件
- E 是一个弃值表达式,并且没有对其应用左值到右值转换。
- x 是一个非 volatile(自 C++26 起)对象,它在
P
点可在常量表达式中使用,并且没有 mutable 子对象,并且满足以下任何条件
(自 C++26 起) |
- E 具有非 volatile 限定的非类类型,并且对其应用了左值到右值转换。
struct S { static const int x = 1; }; // applying lvalue-to-rvalue conversion // to S::x yields a constant expression int f() { S::x; // discarded-value expression does not odr-use S::x return S::x; // expression where lvalue-to-rvalue conversion // applies does not odr-use S::x }
如果 this 作为潜在求值表达式出现(包括非静态成员函数调用表达式中的隐式 this),则 *this 是 odr-使用的。
如果结构化绑定作为潜在求值表达式出现,则它是 odr-使用的。 |
(自 C++17 起) |
在以下情况下,函数是 odr-使用的
- 如果函数被(见下文)潜在求值表达式或转换命名,则该函数是 odr-使用的。
- 虚成员函数,如果它不是纯虚成员函数,则是 odr-使用的(虚成员函数的地址需要构造虚函数表)。
- 类的非放置分配或释放函数被该类的构造函数的定义 odr-使用。
- 类的非放置释放函数被该类的析构函数的定义 odr-使用,或者被在虚析构函数的定义点查找时选中 odr-使用。
- 类
T
中的赋值运算符,如果是另一个类U
的成员或基类,则被U
的隐式定义的复制赋值或移动赋值函数 odr-使用。 - 类的构造函数(包括默认构造函数)被选择它的初始化 odr-使用。
- 类的析构函数,如果是潜在调用的,则是 odr-使用的。
本节不完整 原因:使 odr-使用产生影响的所有情况的列表 |
[编辑] 缺陷报告
以下行为变更的缺陷报告被追溯应用于先前发布的 C++ 标准。
DR | 应用于 | 已发布行为 | 正确行为 |
---|---|---|---|
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++23 标准 (ISO/IEC 14882:2024)
- 6.3 单一定义规则 [basic.def.odr]
- C++20 标准 (ISO/IEC 14882:2020)
- 6.3 单一定义规则 [basic.def.odr]
- C++17 标准 (ISO/IEC 14882:2017)
- 6.2 单一定义规则 [basic.def.odr]
- C++14 标准 (ISO/IEC 14882:2014)
- 3.2 单一定义规则 [basic.def.odr]
- C++11 标准 (ISO/IEC 14882:2011)
- 3.2 单一定义规则 [basic.def.odr]
- C++03 标准 (ISO/IEC 14882:2003)
- 3.2 单一定义规则 [basic.def.odr]
- C++98 标准 (ISO/IEC 14882:1998)
- 3.2 单一定义规则 [basic.def.odr]