Definitions and ODR (One Definition Rule)
定义 是指完全定义声明所引入实体的 声明 。每个声明都是定义,除了以下情况:
- 没有函数体的函数声明:
int f(int); // 声明但不定义函数 f
extern const int a; // 声明但不定义 a extern const int b = 1; // 定义 b
- 类定义内部 静态数据成员 的 非内联 (since 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; // 隐式内联,定义 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; // declares, but does not define Color |
(since 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; // declares, but does not define S2 (S may be incomplete) |
(since C++11) |
using N::d; // 声明但不定义 d
|
(since C++17) |
|
(since C++11) |
extern template f<int, char>; // declares, but does not define f<int, char> |
(since C++11) |
- 一个 显式特化 ,其声明不是定义:
template<> struct A<int>; // 声明但不定义 A<int>
一个 asm 声明 不定义任何实体,但被归类为定义。
在必要时,编译器可能会隐式定义 默认构造函数 、 复制构造函数 、 移动构造函数 、 复制赋值运算符 、 移动赋值运算符 以及 析构函数 。
如果任何对象的定义导致生成 不完整类型 或 抽象类类型 的对象,则程序是非法的。
目录 |
单一定义规则
在任何单个翻译单元中,只允许存在任意变量、函数、类类型、枚举类型 、概念 (C++20 起) 或模板的一个定义(其中某些实体可以拥有多个声明,但只允许存在一个定义)。
对于程序中每个被 odr-used (见下文)的非 inline 函数或变量,必须在整个程序(包括任何标准库和用户自定义库)中有且仅有一个定义。编译器不要求诊断此类违规,但违反此规定的程序行为是未定义的。
对于内联函数 或内联变量 (C++17起) ,在每一个发生 odr-use 的翻译单元中都必须提供其定义。
对于类而言,当使用方式要求其为 完整类型 时,必须在使用处提供定义。
在程序中以下各项允许存在多个定义:类类型、枚举类型、内联函数 、内联变量 (C++17 起) 、 模板化实体 (模板或模板成员,但不包括完整的 模板特化 ),只要满足以下所有条件:
- 每个定义出现在不同的翻译单元中。
|
(since C++20) |
-
- 具有内部链接或无链接的常量可以引用不同对象,只要它们未被odr使用且在每次定义中具有相同的值。
|
(C++11 起) |
- 重载运算符(包括转换函数、分配和释放函数)在每个定义中都引用相同的函数(除非引用的是定义内部定义的函数)。
- 对应实体在每个定义中具有相同的语言链接(例如包含文件不在 extern "C" 块内)。
-
若
const对象在任意定义中进行了 常量初始化 ,则其在每个定义中都必须进行常量初始化。 - 上述规则适用于每个定义中使用的所有默认实参。
- 若该定义针对具有隐式声明构造函数的类,则每个进行odr使用的翻译单元必须为基类和成员调用相同的构造函数。
|
(since C++20) |
- 若定义针对模板,则所有这些要求同时适用于定义点的名称和实例化点的依赖名称。
如果所有这些要求都得到满足,程序的行为就如同整个程序中只有一个定义。否则,程序是非良构的,无需诊断。
注意:在C语言中,类型没有程序级的单一定义规则(ODR),甚至不同翻译单元中同一变量的extern声明 只要类型兼容 就可以具有不同类型。在C++中,同一类型声明所使用的源码标记必须完全相同:如果一个.cpp文件定义了 struct S { int x ; } ; ,而另一个.cpp文件定义了 struct S { int y ; } ; ,将它们链接在一起的程序行为是未定义的。这一问题通常通过 无名命名空间 来解决。
命名实体
变量通过表达式被 命名 ,当该表达式是一个标识符表达式且指向该变量时。
在以下情况下,函数通过表达式或转换被 命名 :
- 若函数名作为表达式或转换出现(包括命名函数、重载运算符、 用户定义转换 、 operator new 的用户定义布置形式、非默认初始化),且该函数通过重载决议被选中,则称该表达式命名了该函数,除非它是非限定纯虚成员函数或指向纯虚函数的成员指针。
- 类的 分配函数 或 解分配函数 由出现在表达式中的 new表达式 命名。
- 类的解分配函数由出现在表达式中的 delete表达式 命名。
- 被选用于复制或移动对象的构造函数,即使发生 复制消除 ,仍视为被表达式或转换命名。 在某些上下文中使用纯右值不会复制或移动对象,参见 强制消除 。 (C++17 起)
一个可能被求值的表达式或转换如果命名了函数,就会对其进行odr使用。
|
命名了 constexpr 函数的潜在常量求值表达式或转换会使其 成为常量求值所必需 ,这将触发默认函数的定义或函数模板特化的实例化,即使该表达式未被求值。 |
(since 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-used,
当对函数进行函数调用或取其地址时,该函数即被odr使用。
如果某个实体被odr使用,则其定义必须存在于程序中的某处;违反此规则通常会导致链接时错误。
struct S { static const int x = 0; // 静态数据成员 // 如果被odr-used,需要在类外提供定义 }; const int& f(const int& r); int n = b ? (1, S::x) // 此处S::x未被odr-used : f(S::x); // 此处S::x被odr-used:需要提供定义
ODR使用(正式定义)
一个由
潜在求值表达式
expr
命名的变量
x
,在点
P
处出现时,会被
expr
进行 odr-use,除非满足以下任一条件:
-
x
是在
P处 可用于常量表达式 的引用。 -
x
不是引用且
(C++26 前)
expr
是表达式
E
的潜在结果集合中的元素,且满足以下任一条件:
- E 是 弃值表达式 ,且未对其应用左值到右值转换。
-
x
是在
P处可用于常量表达式的 非易失 (C++26 起) 对象且无可变子对象,并满足以下任一条件:
| (since C++26) |
-
-
- E 具有非 volatile 限定的非类类型,且对其应用了左值到右值的转换。
-
struct S { static const int x = 1; }; // 对 S::x 应用左值到右值转换 // 将产生常量表达式 int f() { S::x; // 弃值表达式不会对 S::x 进行 ODR 使用 return S::x; // 应用左值到右值转换的表达式 // 不会对 S::x 进行 ODR 使用 }
* this 在以下情况中被 odr-使用:当 this 作为潜在求值表达式出现时(包括非静态成员函数调用表达式中隐式的 this )。
|
当 结构化绑定 作为潜在求值表达式出现时,它被视作odr使用。 |
(since C++17) |
函数在以下情况下被odr使用:
- 若函数通过(见下文)潜在求值表达式或转换被指名,则该函数被odr使用。
- 若 虚成员函数 不是纯虚成员函数,则其被odr使用(构造虚函数表需要虚成员函数的地址)。
- 类的非布置分配或释放函数会被该类的构造函数定义所odr使用。
- 类的非布置释放函数会被该类的析构函数定义所odr使用,或在虚析构函数定义点通过查找被选中时被odr使用。
-
作为另一个类
U的成员或基类的类T中的赋值运算符,会被U的隐式定义的复制赋值或移动赋值函数所odr使用。 - 类的构造函数(包括默认构造函数)会被选择它的 初始化 过程所odr使用。
- 若类的析构函数 可能被调用 ,则其被odr使用。
|
此章节内容不完整
原因:需列出所有ODR使用产生影响的具体情形 |
缺陷报告
以下行为变更缺陷报告被追溯应用于先前发布的C++标准。
| 缺陷报告 | 适用标准 | 发布时行为 | 正确行为 |
|---|---|---|---|
| CWG 261 | C++98 | 多态类的释放函数可能被odr-used,即使程序中没有相关的new或delete表达式 | 补充了odr-use场景以涵盖构造函数和析构函数 |
| CWG 678 | C++98 | 实体可能存在具有不同语言链接的定义 | 此情况下行为未定义 |
| CWG 1472 | C++98 | 满足常量表达式要求的引用变量即使立即应用左值到右值转换也会被odr-used | 此情况下不会被odr-used |
| CWG 1614 | C++98 | 获取纯虚函数地址会odr-use该函数 | 该函数不会被odr-used |
| CWG 1741 | C++98 | 在可能求值的表达式中立即进行左值到右值转换的常量对象会被odr-used | 不会被odr-used |
| 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]