命名空间
变体
操作

定义和 ODR (One Definition Rule)

来自 cppreference.cn
< cpp‎ | 语言
 
 
C++ 语言
 
 

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

  • 没有函数体的函数声明
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 起)或模板只允许一个定义(其中一些可能具有多个声明,但只允许一个定义)。

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

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

对于类,在任何以要求其为完整的方式使用该类的地方,都需要一个定义。

程序中可以有以下各项的多个定义:类类型、枚举类型、inline 函数、inline 变量(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; };,则将它们链接在一起的程序行为是未定义的。这通常通过匿名命名空间解决。

[编辑] 命名实体

如果表达式是表示变量的标识符表达式,则变量被表达式命名

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

  • 如果函数名作为表达式或转换出现(包括命名函数、重载运算符、用户定义转换operator new 的用户定义放置形式、非默认初始化),并且它被重载决议选中,则该函数被该表达式命名,除非它是非限定纯虚成员函数或指向纯虚函数的成员指针。
  • 类的一个分配解分配函数被表达式中出现的new 表达式命名。
  • 类的一个解分配函数被表达式中出现的delete 表达式命名。
  • 即使发生复制消除,为复制或移动对象而选择的构造函数也被表达式或转换命名。在某些上下文中使用 prvalue 不会复制或移动对象,参见强制消除(C++17 起)

潜在求值表达式或转换在命名函数时会 odr-use 它。

命名 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 是 glvalue 条件表达式(E1 ? E2 : E3,其中 E2E3 是 glvalue),则 E2E3 的潜在结果的并集都包含在集合中。
  • 如果 E 是逗号表达式(E1, E2),则 E2 的潜在结果在潜在结果集合中。
  • 否则,集合为空。

[编辑] ODR-use(非正式定义)

如果对象的的值被读取(除非它是编译时常量)或写入,其地址被获取,或一个引用绑定到它,则该对象被 odr-used。

如果引用被使用且其指向的实体在编译时未知,则该引用被 odr-used。

如果函数被调用或其地址被获取,则该函数被 odr-used。

如果一个实体被 odr-used,则其定义必须存在于程序中的某个位置;违反此规则通常会导致链接时错误。

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-use(正式定义)

变量 x 被在点 P 出现的潜在求值表达式 expr 命名,并且被 expr odr-used,除非满足以下任何条件:

  • x 是在 P可在常量表达式中使用的引用。
  • x 不是引用,并且(直至 C++26)expr 是表达式 E 的潜在结果集的一个元素,并且满足以下任何条件:
    • E 是一个被丢弃值的表达式,并且没有对其应用左值到右值转换。
    • x 是一个在 P 处可在常量表达式中使用的非 volatile(C++26 起)对象,且没有可变子对象,并且满足以下任何条件:
(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-used。

如果结构化绑定作为潜在求值表达式出现,则它被 odr-used。

(C++17 起)

函数在以下情况下被 odr-used:

  • 如果函数被潜在求值表达式或转换命名(见下文),则该函数被 odr-used。
  • 如果虚成员函数不是纯虚成员函数,则它被 odr-used(虚成员函数的地址是构建虚函数表所必需的)。
  • 类的一个非放置分配或解分配函数被该类的构造函数的定义 odr-used。
  • 类的一个非放置解分配函数被该类的析构函数的定义 odr-used,或者在虚析构函数的定义点通过查找被选中而 odr-used。
  • T 中作为另一个类 U 的成员或基类的赋值运算符被 U 的隐式定义复制赋值或移动赋值函数 odr-used。
  • 为类选择的构造函数(包括默认构造函数)被选择它的初始化 odr-used。
  • 如果类的析构函数可能被调用,则它被 odr-used。

[编辑] 缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

缺陷报告 应用于 发布时的行为 正确的行为
CWG 261 C++98 多态类的解分配函数
即使程序中没有相关的新建或删除表达式,也可能被 odr-used
在程序中没有相关新建或删除表达式的情况下,多态类的解分配函数也可能被 odr-used。
补充了
odr-use 案例以涵盖
构造函数和析构函数
CWG 678 C++98 一个实体可以有不同语言链接的定义
一个实体可以有
在这种情况下,行为是
未定义的
具有不同语言链接的定义 C++98 CWG 1472
满足常量表达式要求的引用变量即使立即应用了左值到右值转换也曾被 odr-used
即使立即应用了左值到右值转换,满足常量表达式要求的引用变量也会被 odr-used。
它们没有
在这种情况下被 odr-used
CWG 1614 C++98 获取纯虚函数的地址会 odr-use 它 该函数未被 odr-used
CWG 1741 C++98 在潜在求值表达式中立即进行左值到右值转换的常量对象曾经被 odr-used
在潜在求值表达式中,立即进行左值到右值转换的常量对象曾被 odr-used。
它们未被 odr-used
CWG 1926 C++98 数组下标表达式未传播潜在结果 它们传播
CWG 2242 C++98 不清楚只在其部分定义中常量初始化的 const 对象是否违反 ODR
不清楚只在部分定义中常量初始化的 const 对象是否违反 ODR
ODR 未被违反;对象在此情况下被常量初始化
在此情况下被常量初始化
CWG 2300 C++11 不同翻译单元中的 lambda 表达式
永远不能具有相同的闭包类型
闭包类型在“一个定义规则”下可以是相同的
闭包类型在
一个定义规则下可以是相同的 C++98 CWG 2353
静态数据成员不是访问它的成员访问表达式的潜在结果
它是
静态数据成员不是访问它的成员访问表达式的潜在结果。 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]