命名空间
变体
操作

定义与 ODR (单一定义规则)

来自 cppreference.cn
< cpp‎ | language
 
 
C++ 语言
通用主题
流程控制
条件执行语句
if
迭代语句(循环)
for
范围 for (C++11)
跳转语句
函数
函数声明
Lambda 函数表达式
inline 说明符
动态异常规范 (在 C++17* 中弃用)
noexcept 说明符 (C++11)
异常
命名空间
类型
说明符
const/volatile
decltype (C++11)
auto (C++11)
constexpr (C++11)
consteval (C++20)
constinit (C++20)
存储期说明符
初始化
 
 

定义声明,它完整地定义了声明引入的实体。每个声明都是一个定义,除了以下情况

  • 没有函数体的函数声明
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
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
  • (已弃用) 命名空间作用域中,使用 constexpr 说明符在类内定义的静态数据成员的声明
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 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-使用的,并且在每个定义中具有相同的值。
  • Lambda 表达式,如果不在默认实参中或默认模板实参中(自 C++20 起),则通过用于定义它们的记号序列唯一标识。
(自 C++11 起)
  • 重载运算符,包括转换、分配和释放函数,从每个定义中引用相同的函数(除非引用在定义内定义的函数)。
  • 相应的实体在每个定义中具有相同的语言连接(例如,include 文件不在 extern "C" 块内)。
  • 如果一个 const 对象在任何定义中被常量初始化,则它在每个定义中都是常量初始化的。
  • 上述规则适用于每个定义中使用的每个默认实参。
  • 如果定义是针对具有隐式声明的构造函数的类,则 odr-使用它的每个翻译单元都必须为基类和成员调用相同的构造函数。
  • 如果定义是针对具有默认三路比较的类,则 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.E2E1.template E2 的类成员访问表达式,命名一个非静态数据成员,则 E1 的潜在结果包含在该集合中。
  • 如果 E 是命名静态数据成员的类成员访问表达式,则指定该数据成员的标识符表达式包含在该集合中。
  • 如果 E 是形式为 E1.*E2E1.*template E2 的成员指针访问表达式,其第二个操作数是常量表达式,则 E1 的潜在结果包含在该集合中。
  • 如果 E 是带括号的表达式((E1)),则 E1 的潜在结果包含在该集合中。
  • 如果 E 是左值条件表达式(E1 ? E2 : E3,其中 E2E3 是左值),则 E2E3 的潜在结果的并集都包含在该集合中。
  • 如果 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-使用的。

[编辑] 缺陷报告

以下行为变更的缺陷报告被追溯应用于先前发布的 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]