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

来自cppreference.com
< cpp‎ | language

定义是完全定义了声明中所引入的实体的声明。除了以下情况外的声明都是定义:

  • 无函数体的函数声明:
int f(int); // 声明但不定义 f
extern const int a;     // 声明但不定义 a
extern const int b = 1; // 定义 b
struct S
{
    int n;               // 定义 S::n
    static int i;        // 声明但不定义 S::i
    inline static int x; // 定义 S::x
};                       // 定义 S
 
int S::i;                // 定义 S::i
  • (弃用) 已经在类中用 constexpr 说明符定义过的静态数据成员,在命名空间作用域中的声明:
struct S
{
    static constexpr int x = 42; // 隐含 inline,定义 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; // 声明但不定义 Color
(C++11 起)
template<typename T> // 声明但不定义 T
  • 并非定义的函数声明中的形参声明:
int f(int x); // 声明但不定义 f 和 x
 
int f(int x)  // 定义 f 和 x
{
    return x + a;
}
typedef S S2; // 声明但不定义 S2(S 可以是不完整类型)
using S2 = S; // 声明但不定义 S2(S 可以是不完整类型)
(C++11 起)
using N::d; // 声明但不定义 d
(C++17 起)
(C++11 起)
extern template
f<int, char>; // 声明但不定义 f<int, char>
(C++11 起)
template<>
struct A<int>; // 声明但不定义 A<int>

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

如果必要,编译器就会隐式定义默认构造函数复制构造函数移动构造函数复制赋值运算符移动赋值运算符析构函数

如果任何对象的定义导致出现具有不完整类型抽象类类型的对象,那么程序非良构。

单一定义规则(ODR)

任何变量、函数、类类型、枚举类型概念 (C++20 起)或模板,在每个翻译单元中都只允许有一个定义(其中部分可以有多个声明,但只允许有一个定义)。

在整个程序(包括所有的标准或用户定义的程序库)中,被 ODR 使用(见下文)的非 inline 函数或变量只允许有且仅有一个定义。不要求编译器诊断这条规则是否被违反,但违反它的程序的行为是未定义的。

对于 inline 函数或 inline 变量 (C++17 起)来说,在 ODR 使用了它的每个翻译单元中都需要一个定义。

在以需要将类作为完整类型的方式予以使用的每个翻译单元中,要求有且仅有该类的一个定义。

以下各种实体:类类型、枚举类型、inline 函数、inline 变量 (C++17 起)模板实体(模板和模板成员,但不含完全模板特化),在程序中可以出现多个定义,只要满足下列条件:

  • 每个定义在不同的翻译单元出现
(C++20 起)
  • 每个定义都由相同的记号序列构成(典型情况下是在同一个头文件中)
  • 每个定义内进行的名字查找(在重载决议后)都找到相同实体,除了
  • 具有内部链接或无链接的常量可以指代不同的对象,只要不 ODR 使用它们并在它们在各个定义中都具有相同的值
  • 不在默认实参或默认模板实参 (C++20 起)中的 lambda 表达式由定义它们的记号序列被唯一标识
(C++11 起)
  • 被重载的运算符(包括转换,分配和解分配函数),在各个定义中都代表相同的函数(除非它们代表的是在这个定义中所定义的函数)
  • 每个定义中对应的实体具有相同的语言链接(比如包含文件时并未处于某个 extern "C" 块之中)
  • 如果一个 const 对象在其中一个定义中以常量被初始化,那么它在每个定义中都会以常量被初始化
  • 以上规则同样适用于各个定义中的所有默认实参
  • 如果该定义是带有隐式声明的构造函数的类定义,那么在每个 ODR 使用它的翻译单元中必须为基类和成员调用相同的构造函数
  • 如果该定义是模板定义,那么所有这些要求一同适用于定义点的各个名字和实例化点的各个待决名

如果满足了所有这些要求,那么程序的行为如同在整个程序中只有一个定义。否则程序非良构,不要求诊断。

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

ODR 使用

非正式地说: 1. 一个对象在它的值被读取(除非它是编译时常量)或写入,或它的地址被取,或者被引用绑定时,这个对象被 ODR 使用。 2. 使用“所引用的对象在编译期未知”的引用时,这个引用被 ODR 使用。 3. 一个函数在被调用或它的地址被取时,被 ODR 使用。 如果一个对象、引用或函数被 ODR 使用,那么程序中必须有它的定义;否则通常会有链接时错误。

struct S
{
    static const int x = 0; // 静态数据成员
    // 如果 ODR 使用它,就需要一个类外的定义
};
 
const int& f(const int& r);
 
int n = b ? (1, S::x) // S::x 在此处未被 ODR 使用
          : f(S::x);  // S::x 在此处被 ODR 使用:需要一个定义

正式地说,

1) 潜在求值表达式 ex 中的变量 x 被 ODR 使用,除非同时满足以下两条:
  • x 进行左值到右值转换产生了一个没有调用任何非平凡函数的常量表达式
  • 或者 x 不是对象(即 x 是引用),或者当 x 是对象时,它是某个更大的表达式 e潜在结果 之一,而这个更大的表达式要么是弃值表达式,要么对它实施了左值到右值转换
struct S { static const int x = 1; }; // 对 S::x 实施左值到右值转换产生常量表达式
 
int f()
{ 
    S::x;        // 弃值表达式不会 ODR 使用 S::x
 
    return S::x; // 实施了左值到右值转换的表达式不会 ODR 使用 S::x
}
2) 如果 this 作为潜在求值表达式(包括非静态成员函数调用表达式中隐含的 this)出现,那么 *this 被 ODR 使用。
3) 如果结构化绑定作为潜在求值表达式出现,那么它被 ODR 使用。
(C++17 起)

表达式 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 : E3E2E3 都是泛左值)时,集合包含 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 是子表达式 "x"(? 的左边)的一部分,它应用了左值到右值转换,
    // 但对 x 实施这个转换不产生常量表达式,所以 x 被 ODR 使用
    // S::a 和 S::b 都是左值,并均作为泛左值条件表达式的结果的“潜在结果”
    // 该结果随即进行了为复制初始化返回值所要求的左值到右值转换,
    // 因此 S::a 和 S::b 未被 ODR 使用
}
4) 以下情况下函数被 ODR 使用:
  • 如果函数被潜在求值的表达式或转换指名(见后述),那么它被 ODR 使用。
  • 如果虚成员函数不是纯虚成员函数,那么它被 ODR 使用(需要虚函数的地址来构建虚表)。
  • 类的非布置分配或解分配函数,为这个类的构造函数的定义所 ODR 使用。
  • 类的非布置解分配函数,为这个类的析构函数的定义所 ODR 使用,或由虚析构函数的定义点所进行的查找所选择时被 ODR 使用。
  • 作为另一个类 U 的成员或基类的类 T 的赋值运算符,由 U 的隐式定义的复制赋值或移动赋值函数所 ODR 使用。
  • 类的构造函数(包括默认构造函数),由选择了它的初始化所 ODR 使用。
  • 类的析构函数,当它被潜在调用时即被 ODR 使用。

指名函数

函数在下列情况被表达式或转换指名:

  • 函数的名字在表达或转换式中出现(这包括具名函数,被重载的运算符,用户定义的转换,用户定义的布置形式的 new 运算符,以及非默认的初始化等情况),如果重载决议选择它,那么它被该表达式或转换指名,除非它是无限定的纯虚成员函数或指向纯虚函数的成员指针。
  • 类的分配解分配函数,由在表达式中出现的 new 表达式所指名。
  • 类的解分配函数,由在表达式中出现的 delete 表达式所指名。
  • 即便发生了复制消除,仍认为选择用于复制或移动对象的构造函数被该表达式或转换指名。在某些语境中使用纯右值不会复制或移动对象,见强制消除 (C++17 起)

如果潜在求值的表达式或转换指名函数,那么 ODR 使用它。

指名 constexpr 函数的潜在常量求值的表达式或转换使它需要用于常量求值,这会触发预置函数的定义或函数模板特化的实例化,即使该表达式不被求值。

(C++11 起)

缺陷报告

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

缺陷报告 应用于 出版时的行为 正确行为
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++20 标准(ISO/IEC 14882:2020):
  • 6.3 One definition rule [basic.def.odr]
  • C++17 标准(ISO/IEC 14882:2017):
  • 6.2 One definition rule [basic.def.odr]
  • C++14 标准(ISO/IEC 14882:2014):
  • 3.2 One definition rule [basic.def.odr]
  • C++11 标准(ISO/IEC 14882:2011):
  • 3.2 One definition rule [basic.def.odr]
  • C++03 标准(ISO/IEC 14882:2003):
  • 3.2 One definition rule [basic.def.odr]
  • C++98 标准(ISO/IEC 14882:1998):
  • 3.2 One definition rule [basic.def.odr]