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

< 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" 块之中)
  • 以上三条规则同样适用于各个定义中的所有默认实参
  • 若该定义是带有隐式声明的构造函数的类定义,则在每个 ODR 式使用它的翻译单元中必须为基类和成员调用相同的构造函数
  • 若该定义是模板定义,则所有这些要求一同适用于定义点的各个名字和实例化点的各个待决名

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

注意:在 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; // 静态数据成员
    // 如果 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 起)

在以上各项定义中,潜在求值的含义是,其表达式并非诸如 sizeof 这样的不求值表达式(或其子表达式)的操作数。而表达式 e潜在结果集合e 中所出现的标识表达式的(可能为空的)集合。组合起来有:

  • e 为数组下标表达式(e1[e2])且其中操作数之一为数组时,该操作数的潜在结果包含于集合中
(C++17 起)
  • e 是类成员访问表达式(e1.e2e1->e2)时,对象表达式 e1 的潜在结果包含于集合中
  • e 是成员指针访问表达式(e1.*e2e1->*e2),且其第二个操作数为常量表达式时,对象表达式 e1 的潜在结果包含于集合中
  • e 是带有括号的表达式((e1))时,e1 的潜在结果包含于集合中
  • e 泛左值条件表达式(e1?e2:e3 且 e2 和 e3 均为泛左值)时,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 式使用:
  • 函数的名字出现于潜在求值表达式之中(这包括具名函数,被重载的运算符,用户定义的转换,用户定义的布置形式的 new 运算符,以及非默认的初始化等情况),若它被重载决议所选择,则它被 ODR 式使用,除非它是无限定的纯虚成员函数或指向纯虚函数的成员指针 (C++17 起)
  • 如果虚成员函数不是纯虚成员函数,它就被 ODR 式使用(需要虚函数的地址来构建虚表)
  • 类的分配或解分配函数,由出现于潜在求值表达式中的 new 表达式所 ODR 式使用
  • 类的解分配函数,由出现于潜在求值表达式中的 delete 表达式所 ODR 式使用
  • 类的非布置分配或解分配函数,为这个类的构造函数的定义所 ODR 式使用
  • 类的非布置解分配函数,为这个类的析构函数的定义所 ODR 式使用,或由虚析构函数的定义点所进行的查找所选择时被 ODR 式使用
  • 作为另一个类 U 的成员或基类的类 T 的赋值运算符,由 U 的隐式定义的复制赋值或移动赋值函数所 ODR 式使用。
  • 类的构造函数(包括默认构造函数),由选择了它的初始化所 ODR 式使用
  • 类的析构函数,当其被潜在调用时即被 ODR 式使用

所有这些情况中,即便发生了复制消除,其所选择用于复制或移动一个对象的构造函数仍被 ODR 式使用。

引用

  • C++11 standard (ISO/IEC 14882:2011):
  • 3.2 One definition rule [basic.def.odr]