C++ 参考手册
- C++11
- C++14
- C++17
- C++20
- C++ 编译器支持情况表
- 独立与宿主实现
- C++ 语言
- 变量模板(C++14 起)
- 整数字面量
- 聚合初始化
- 比较运算符
- 默认比较(C++20 起)
- 转义序列
- for 循环
- while 循环
- 用户定义转换
- SFINAE
- 主函数
- ASCII 码表
- 标识符
- 类型
- 内存模型
- 对象
- 基本概念
- 表达式
- 声明
- 初始化
- 函数
- 语句
- 类
- 运算符重载
- 模板
- 异常
- 事务性内存
- 占位符类型说明符 (C++11 起)
- decltype 说明符
- 函数声明
- final 说明符 (C++11 起)
- override 说明符(C++11 起)
- 引用声明
- 移动构造函数
- 移动赋值运算符
- 枚举声明
- constexpr 说明符(C++11 起)
- 列表初始化 (C++11 起)
- 构造函数与成员初始化器列表
- using 声明
- nullptr,指针字面量
- 基础类型
- 类型别名,别名模版 (C++11 起)
- 形参包
- 联合体声明
- 字符串字面量
- 用户定义字面量 (C++11 起)
- 属性说明符序列(C++11 起)
- Lambda 表达式 (C++11 起)
- noexcept 说明符 (C++11 起)
- noexcept 运算符 (C++11 起)
- alignof 运算符(C++11 起)
- alignas 说明符 (C++11 起)
- 存储类说明符
- 基于范围的 for 循环 (C++11 起)
- static_assert 声明
- 隐式转换
- 代用运算符表示
- 自增/自减运算符
- 折叠表达式(C++17 起)
- 类模板实参推导(C++17 起)
- 模板形参与模板实参
- if 语句
- inline 说明符
- 结构化绑定声明 (C++17 起)
- switch 语句
- 字符字面量
- 命名空间
- 求值顺序
- 复制消除
- consteval 说明符 (C++20 起)
- constinit 说明符 (C++20 起)
- 协程 (C++20)
- 模块 (C++20 起)
- 约束与概念 (C++20 起)
- new 表达式
- do-while 循环
- continue 语句
- break 语句
- goto 语句
- return 语句
- 动态异常说明
- throw 表达式
- try 块
- 命名空间别名
- 类声明
- cv(const 与 volatile)类型限定符
- 默认初始化
- 值初始化(C++03 起)
- 零初始化
- 复制初始化
- 直接初始化
- 常量初始化
- 引用初始化
- 值类别
- C++ 运算符优先级
- 布尔字面量
- 浮点字面量
- typedef 说明符
- 显式类型转换
- static_cast 转换
- dynamic_cast 转换
- const_cast 转换
- reinterpret_cast 转换
- delete 表达式
- 构造函数与成员初始化器列表
- this 指针
- 访问说明符
- 友元声明
- virtual 函数说明符
- explicit 说明符
- 静态成员
- 默认构造函数
- 复制构造函数
- 复制赋值运算符
- 析构函数
- 类模板
- 函数模板
- 显式(全)模板特化
- 汇编声明
- C++ 的历史
- 作用域
- 生存期
- 定义与单一定义规则(ODR)
- 名字查找
- 有限定的名字查找
- 无限定的名字查找
- 如同规则
- 未定义行为
- 翻译阶段
- 常量表达式
- 赋值运算符
- 算术运算符
- 逻辑运算符
- 成员访问运算符
- 其他运算符
- sizeof 运算符
- typeid 运算符
- 指针声明
- 数组声明
- 语言链接
- 详述类型说明符
- 默认实参
- 变长实参
- 实参依赖查找
- 重载决议
- 重载函数的地址
- 注入类名
- 非静态数据成员
- 非静态成员函数
- 嵌套类
- 派生类
- 空基类优化
- 抽象类
- 位域
- 转换构造函数
- 成员模板
- 模板实参推导
- 部分模板特化
- sizeof... 运算符
- 待决名
- 函数 try 块
- 扩充命名空间 std
- 字母缩写
- RAII
- 三/五/零之法则
- PImpl
- 零开销原则
- 类型
- 隐式转换
- 注释
- C++ 关键词
- 预处理器
- C++ 标准库头文件
- 具名要求
- 功能特性测试 (C++20)
- 工具库
- 类型支持(基本类型、RTTI、类型特性)
- 概念库 (C++20)
- 错误处理
- 动态内存管理
- 日期和时间工具
- 字符串库
- 容器库
- 迭代器库
- 范围库 (C++20)
- 算法库
- 数值库
- 输入/输出库
- 文件系统库
- 本地化库
- 正则表达式库
- 原子操作库
- 线程支持库
- 实验性 C++ 特性
- 有用的资源
- 索引
- std 符号索引
- 协程支持 (C++20)
- C++ 关键词
定义与单一定义规则(ODR)
定义是完全定义了声明中所引入的实体的声明。除了以下情况外的声明都是定义:
- 无函数体的函数声明
int f(int); // 声明但不定义 f
extern const int a; // 声明但不定义 a extern const int b = 1; // 定义 b
- 在类的定义中的非 inline (C++17 起) 静态数据成员的声明
struct S { int n; // 定义 S::n static int i; // 声明但不定义 S::i inline static int x; // 定义 S::x }; // 定义 S int S::i; // 定义 S::i
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 声明
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 起),在程序中可以出现多个定义,只要满足下列条件:
- 每个定义出现于不同翻译单元
|
(C++20 起) |
- 每个定义都由相同的记号(token)序列构成(典型情况下是在同一个头文件中)
- 每个定义内进行的名字查找(在重载决议后)都找到相同实体,除了
- 具有内部链接或无链接的常量可以指代不同的对象,只要不 ODR 式使用它们,并在它们在各个定义中都具有相同的值
|
(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 式使用:需要一个定义
正式地说,
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 }
3) 若结构化绑定作为潜在求值表达式出现,则它被 ODR 使用。
|
(C++17 起) |
在以上各项定义中,潜在求值的含义是,其表达式并非诸如 sizeof 这样的不求值表达式(或其子表达式)的操作数。而表达式 e
的潜在结果(potential result)集合是 e
中所出现的标识表达式(id-expression)的(可能为空的)集合。组合起来有:
- 当
e
是标识表达式时,表达式e
就是其唯一的潜在结果
|
(C++17 起) |
- 当
e
是类成员访问表达式 e1.e2 或 e1->e2 时,对象表达式 e1 的潜在结果就包括在集合中 - 当
e
是成员指针访问表达式 e1.*e2 或 e1->*e2,且其第二个操作数为常量表达式时,对象表达式 e1 的潜在结果就包括在集合中 - 当
e
是带有括号的表达式 (e1) 时,e1
的潜在结果被包括在集合中 - 当
e
泛左值条件表达式 e1?e2:e3,其中 e2 和 e3 均为泛左值时,e2
和e3
的潜在结果的并集都包括在集合中 - 当
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 式使用 }
- 函数的名字出现于潜在求值表达式之中(这包括具名函数,重载运算符,用户定义的转换,用户定义的布置形式的 new 运算符,以及非默认的初始化等情况),若它被重载决议所选择,则它被 ODR 式使用,除非它是无限定的纯虚成员函数或纯虚函数的成员指针 (C++17 起)。
- 如果虚成员函数不是纯虚成员函数,它就被 ODR 式使用(需要虚函数的地址来构建虚表)
- 类的分配或解分配函数,由出现于潜在求值表达式中的 new 表达式所 ODR 式使用
- 类的解分配函数,由出现于潜在求值表达式中的 delete 表达式所 ODR 式使用
- 类的非布置分配或解分配函数,为这个类的构造函数的定义所 ODR 式使用
- 类的非布置解分配函数,为这个类的析构函数的定义所 ODR 式使用,或由虚析构函数的定义点所进行的查找所选择时它被 ODR 式使用
- 作为另一个类
U
的成员或基类的类T
的赋值运算符,由U
的隐式定义的复制赋值或移动赋值函数所 ODR 式使用。 - 类的构造函数(也包括默认构造函数),由选择了它的初始化所 ODR 式使用
- 类的析构函数,当其被潜在调用时即被 ODR 式使用
所有这些情况中,即便发生了复制消除,其所选择用于复制或移动一个对象的构造函数仍被 ODR 式使用。
本节未完成 原因:列出所有 ODR 式使用产生差异的情形 |
引用
- C++11 standard (ISO/IEC 14882:2011):
- 3.2 One definition rule [basic.def.odr]