定义与单一定义规则(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 式使用它们,并在它们在各个定义中都具有相同的值。
  • 重载运算符(包括转换,分配和解分配函数),在各个定义中都代表相同的函数(除非它们代表的是在这个定义中所定义的函数)
  • 它们具有相同的语言链接(比如包含文件时并未处于某个 extern "C" 块之中)
  • 以上三条规则对每个定义中的每个默认实参同样适用
  • 若定义调用带前提条件( [[expects:]] )的函数,或是含有断言( [[assert:]] )或拥有契约条件( [[expects:]][[ensures:]] )的函数,则在何种条件下必须用同一构建等级和违规持续模式翻译所有定义,是实现定义的
(C++20 起)
  • 若定义是带有隐式声明的构造函数的类定义,则在每个 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) 潜在求值 (potentially-evaluated) 表达式 ex 中的变量 x 被 ODR 式使用,除非以下两条均为真:
  • x 进行左值向右值转换产生了不需要调用非平凡函数的常量表达式
  • x 不是对象(亦即 x 是引用),或者当 x 是对象时,它是某个更大的表达式 e潜在结果( potential result )之一,而这个更大的表达式要么是弃值表达式,或应用到它的左值到右值转换
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 中所出现的标识表达式(id-expression)的(可能为空的)集合。组合起来有:

  • 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 未被 ODR 式使用
  // S::a 和 S::b 都是左值,并作为泛左值条件表达式
  // 的结果的“潜在结果”
  // 这个结果随即根据对返回值进行复制初始化所要求的进行了
  // 左值向右值转换,因而 S::a 和 S::b 也未被 ODR 式使用
}
4) 以下情况下函数被 ODR 式使用
  • 函数的名字出现于潜在求值表达式之中(这包括具名函数,重载运算符,用户定义的转换,用户定义的放置式运算符 new,以及非默认的初始化等情况),若它被重载决议所选择,则它被 ODR 式使用,除非它是未被限定的纯虚函数或纯虚函数的成员指针 (C++17 起)
  • 如果虚成员函数不是纯虚成员函数,它就被 ODR 式使用(需要虚函数的地址来构建虚表)
  • 类的分配或解分配函数,由出现于潜在求值表达式中的 new 表达式所 ODR 式使用
  • 类的解分配函数,由出现于潜在求值表达式中的 delete 表达式所 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]