命名空间
变体
操作

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

来自 cppreference.com
< cpp‎ | 语言
 
 
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 doesn't define f
extern const int a;     // declares, but doesn't define a
extern const int b = 1; // defines b
struct S
{
    int n;               // defines S::n
    static int i;        // declares, but doesn't 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 起)
  • 类名的声明(通过 前向声明 或在另一个声明中使用 elaborated 类型说明符)
struct S;             // declares, but doesn't define S
 
class Y f(class T p); // declares, but doesn't define Y and T (and also f and p)
enum Color : int; // declares, but doesn't define Color
(自 C++11 起)
template<typename T> // declares, but doesn't define T
  • 函数声明(非定义)中的参数声明
int f(int x); // declares, but doesn't define f and x
 
int f(int x)  // defines f and x
{
    return x + a;
}
typedef S S2; // declares, but doesn't define S2 (S may be incomplete)
using S2 = S; // declares, but doesn't define S2 (S may be incomplete)
(自 C++11 起)
using N::d; // declares, but doesn't define d
(自 C++17 起)
(自 C++11 起)
extern template
f<int, char>; // declares, but doesn't define f<int, char>
(自 C++11 起)
template<>
struct A<int>; // declares, but doesn't define A<int>

asm 声明 不定义任何实体,但它被归类为定义。

在必要时,编译器可以隐式定义 默认构造函数复制构造函数移动构造函数复制赋值运算符移动赋值运算符析构函数

如果任何对象的定义导致 不完整类型抽象类类型 的对象,则程序是格式错误的。

内容

[编辑] 单一定义规则

在任何一个翻译单元中,只允许对任何变量、函数、类类型、枚举类型概念(自 C++20 起) 或模板进行一次定义(其中一些可能有多个声明,但只允许一个定义)。

在整个程序(包括任何标准和用户定义的库)中,需要出现对每个被 odr-used(见下文)的非 内联 函数或变量的一个且只有一个定义。编译器不需要诊断此违规行为,但违反此规则的程序的行为是未定义的。

对于内联函数 或内联变量(自 C++17 起),在每个 odr-used 它的翻译单元中都需要一个定义。

对于类,在任何需要类为 完整 的地方都需要一个定义。

程序中可以有多个以下各项的定义:类类型、枚举类型、内联函数、内联变量(自 C++17 起)模板化实体(模板或模板的成员,但不是完整的 模板特化),只要以下所有条件都为真

  • 每个定义都出现在不同的翻译单元中
(自 C++20 起)
  • 每个定义都由相同的标记序列组成(通常出现在同一个头文件中)
  • 从每个定义内部进行名称查找都会找到相同的实体(在重载解析之后),除了
  • 具有内部或无链接的常量可以引用不同的对象,只要它们不被 odr-used 并且在每个定义中具有相同的值
  • 不在默认参数 或默认模板参数(自 C++20 起) 中的 lambda 表达式由用于定义它们的标记序列唯一标识
(自 C++11 起)
  • 重载运算符,包括转换、分配和释放函数,在每个定义中都引用相同的函数(除非引用定义中定义的函数)
  • 相应的实体在每个定义中都具有相同的语言链接(例如,包含文件不在 extern "C" 块内)
  • 如果 const 对象在任何定义中被 常量初始化,则它在每个定义中都被常量初始化
  • 以上规则适用于每个定义中使用的每个默认参数
  • 如果定义是针对具有隐式声明的构造函数的类,则每个 odr-used 它的翻译单元都必须为基类和成员调用相同的构造函数
  • 如果定义是针对具有默认 三路比较 的类,则每个 odr-used 它的翻译单元都必须为基类和成员调用相同的比较运算符
(自 C++20 起)
  • 如果定义是针对模板的,则所有这些要求都适用于定义点的名称和实例化点的依赖名称

如果满足所有这些要求,则程序的行为就像整个程序中只有一个定义一样。否则,程序格式错误,不需要诊断。

注意:在 C 语言中,类型没有程序范围的 ODR,甚至在不同翻译单元中对同一变量的 extern 声明也可能具有不同的类型,只要它们是兼容的。在 C++ 中,用于声明同一类型的源代码标记必须与上述描述相同:如果一个 .cpp 文件定义了 struct S { int x; };,而另一个 .cpp 文件定义了 struct S { int y; };,则将它们链接在一起的程序的行为是未定义的。这通常使用 未命名命名空间 来解决。

[编辑] 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

正式地说:

1)可能被求值的表达式 ex 中的变量 x 被 ODR 使用,除非以下两项都成立
  • x 应用左值到右值转换会产生一个不调用非平凡函数的常量表达式
  • x 不是对象(即 x 是引用),或者,如果 x 是对象,它是较大表达式 e潜在结果之一,其中该较大表达式要么是 丢弃值表达式,要么对其应用了左值到右值转换
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
}
2) 如果 this 作为可能被求值的表达式出现(包括非静态成员函数调用表达式中的隐式 this),则 *this 被 ODR 使用
3) 如果 结构化绑定 作为可能被求值的表达式出现,则它被 ODR 使用。
(自 C++17 起)

表达式 E 的一组潜在结果是出现在 E 中的 id 表达式(可能为空)的集合,组合如下

  • 如果 Eid 表达式,则表达式 E 是其唯一的潜在结果。
  • 如果 E 是下标表达式 (E1[E2]),其中一个操作数是数组,则该操作数的潜在结果包含在集合中。
  • 如果 E 是命名非静态数据成员的类成员访问表达式,形式为 E1.E2E1.template E2,则 E1 的潜在结果包含在集合中。
  • 如果 E 是命名静态数据成员的类成员访问表达式,则指定数据成员的 id 表达式包含在集合中。
  • 如果 E 是指向成员的访问表达式,形式为 E1.*E2E1.*template E2,其第二个操作数是常量表达式,则 E1 的潜在结果包含在集合中。
  • 如果 E 是括号中的表达式 ((E1)),则 E1 的潜在结果包含在集合中。
  • 如果 E 是左值条件表达式 (E1 ? E2 : E3,其中 E2E3 是左值),则 E2E3 的潜在结果的并集都包含在集合中。
  • 如果 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 is a part of the subexpression "x" (to the left of ?),
    // which applies lvalue-to-rvalue conversion, but applying that conversion to x
    // does not yield a constant expression, so x is odr-used
    // S::a and S::b are lvalues, and carry over as "potential results"
    // to the result of the glvalue conditional
    // That result is then subject to lvalue-to-rvalue conversion requested
    // to copy-initialize the return value, therefore S::a and S::b are not odr-used
}
4) 在以下情况下,函数被 ODR 使用
  • 如果函数由可能被求值的表达式或转换命名(见下文),则该函数被 ODR 使用。
  • 如果 虚成员函数 不是纯虚成员函数(需要虚成员函数的地址来构造虚函数表),则它被 ODR 使用。
  • 类的非放置分配或释放函数由该类的构造函数的定义 ODR 使用。
  • 类的非放置释放函数由该类的析构函数的定义 ODR 使用,或者由在虚析构函数的定义点进行的查找选择 ODR 使用。
  • T 中的赋值运算符(它是另一个类 U 的成员或基类)由 U 的隐式定义的复制赋值或移动赋值函数 ODR 使用。
  • 类的构造函数(包括默认构造函数)由选择它的 初始化 ODR 使用。
  • 如果类的析构函数可能被调用,则它被 ODR 使用。

[编辑] 命名函数

在以下情况下,函数由表达式或转换命名

  • 如果函数的名称作为表达式或转换出现(包括命名函数、重载运算符、用户定义的转换、用户定义的放置形式的 operator new、非默认初始化),并且它被重载决议选择,则该函数由该表达式命名,除非它是不合格的纯虚成员函数或指向纯虚成员函数的成员指针。
  • 类的 分配释放 函数由表达式中出现的 new 表达式 命名。
  • 类的释放函数由表达式中出现的 delete 表达式 命名。
  • 即使发生 复制省略,选择用于复制或移动对象的构造函数也被认为是由表达式或转换命名的。在某些情况下使用纯右值不会复制或移动对象,请参阅 强制省略(自 C++17 起)

如果一个可能求值的表达式或转换命名了一个函数,则它会 ODR 式使用该函数。

命名 constexpr 函数的可能常量求值表达式或转换会使其成为 常量求值所需,这会触发默认函数的定义或函数模板特化的实例化,即使表达式未求值也是如此。

(自 C++11 起)

[编辑] 缺陷报告

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