整理一下 C++ POD 类型的细节(一)
POD type(简旧数据类型)是 C++ 标准中对于对象类型的一些要求。
内容引用自 n4659 (C++17 工作草案)和 n4861 (C++20 工作草案),尽量不讨论古代 C++ 。
名称
POD 类型中的 POD 指的是 plain old data ,简旧数据。
POD 本意可能是直接与 C 库二进制交流的类型,但其中也有 C++ 独有的类型。
POD 类型的要求
POD 类型是以下类型的统称(n4659 6.9,9):
- 标量类型(scalar type)
- POD 类
- 以上类型的数组和 cv 限定版本
标量类型是以下类型的统称:
- 算术(整数、浮点)类型
- 枚举类型
- 指针类型
- 指向成员指针类型
-
std::nullptr_t类型 - 以上类型 cv 限定版本
(以下的类含 class/struct 和 union)
POD 类是以下类型的统称(n4659 12,10):
- POD struct
- POD union
POD struct 和 POD union 的要求:
- 平凡类(trivial class)
- 标准布局类(standard-layout class)
- 无非 POD 类类型或其数组的非静态数据成员
- 两者区别是一个是非联合类(class/struct),另一个是 union
标准布局类的要求
标准布局类要求(n4659 12,7):
- 无非标准布局类型的基类
- 无虚函数或虚基类
- 对于每个直接或间接基类,只有一个该基类的子对象
- 无非标准布局类型(或其数组)或引用类型的非静态数据成员
- 所有非静态数据成员拥有同一访问控制(public/protected/private)
- 该类和基类中的所有非静态数据成员、位域都声明于同一类中
- 基类的类型都与该类“零偏移成员类型集”中成员类型不同
对象类型 X 的“零偏移成员类型集” M(X) 定义为:
- 若 X 为非联合类且无(含继承的)非静态数据成员,则 M(X)=\emptyset
- 若 X 为非类、非数组类型,则 M(X)=\emptyset
- 若 X 为非空的非联合类类型,其首个(含继承的)非静态数据成员类型为 X_0 ,则 M(X)={X_0}\cup M(X_0)
- 若 X 为数组类型,其元素类型为 X_e ,则 M(X)=X_e\cup M(X_e)
- 若 X 为 union 类型,其所有静态数据成员类型为 X_i ,则 M(X)=\bigcup_{i}{X_i}
注意:零偏移成员类型集,是标准布局类的所有非基类子对象中,保证在零偏移位置的非基类子对象的类型集合。换言之,标准布局类强制空基类优化(empty base optimization, EBO) 。标准布局类的成员顺序完全由标准确定,而且布局中无编译器隐式维护的内存位置(虚函数或虚基类所必须的信息)。
平凡类的要求
平凡类要求(n4659 12,6):
- 可平凡复制类(trivially copyable class)
- 所有默认构造函数均为平凡或被删除,且至少有一个未被删除( C++20 中有改动 )
可平凡复制类的要求:
- 复制构造函数、移动构造函数、复制赋值运算符、移动赋值运算符均为平凡或被删除( C++20 中有改动 )
- 上述特殊成员函数至少有一个未被删除( 在 C++20 中有改动 )
- 拥有平凡且未被删除的析构函数( C++20 中此条无改动,但析构函数本身有改动 )
平凡默认构造函数要求(n4659 15.1,6):
-
该构造函数非用户提供(隐式定义或显式
=default) - 类无虚函数或虚基类
- 所有非静态数据成员均无默认成员初始化器
- 所有直接基类均拥有平凡默认构造函数
- 每个类类型成员(或类类型数组成员的元素)拥有平凡默认构造函数
平凡析构函数要求(n4659 15.4,6):
-
析构函数非用户提供(隐式定义或显式
=default) - 所有直接基类拥有平凡析构函数
- 每个类类型成员(或类类型数组成员的元素)拥有平凡析构函数
平凡复制/移动构造函数要求(n4659 15.8.1,11):
-
该构造函数非用户提供(隐式定义或显式
=default) - 类无虚函数或虚基类
- 为每个直接基类子对象选择的复制/移动构造函数为平凡
- 为每个类类型成员(或类类型数组成员的元素)选择的复制/移动构造函数为平凡
平凡复制/移动运算符要求(n4659 15.8.2,9)
-
该赋值运算符非用户提供(隐式定义或显式
=default) - 类无虚函数或虚基类
- 为每个直接基类子对象选择的复制/移动赋值运算符为平凡
- 为每个类类型成员(或类类型数组成员的元素)选择的复制/移动赋值运算符为平凡
注意:平凡的复制/移动构造函数/赋值运算符的行为等价于用
std::memmove
或
std::memcpy
复制对象表示。平凡的默认构造函数和析构函数的行为等价于无操作。
C++20 中的改动
C++20 中有些类模板或类模板的成员模板可以有多个同种类(n4861, 11.2)的特殊成员函数。它们有些是“合格(eligible)”的。
同种类指的是:
- 均为默认构造函数、复制构造函数、移动构造函数、复制赋值运算符、移动赋值运算符;
-
对于有形参的特殊成员函数,首个形参类型相同(如复制构造函数的首个形参可以是
const T&或比较奇特的T&,这两种不属于同一种类); - 对于赋值运算符,成员函数的 cv 及引用限定相同。
这些限制保证了同种类的特殊成员函数在不考虑访问、 C++20 中加入的关联约束(通过
requires
子句)和默认参数时,在重载决议中表现相同。
合格的特殊成员函数是:
- 未被删除;
-
关联约束(通过
requires子句引入)若存在则得到满足; - 没有更受约束的同种类特殊成员函数。
在 C++20 中可平凡复制类和平凡类的要求中,关于(预期的)析构函数外的特殊成员函数的“被删除”“未被删除”均被改成“合格”“不合格”。
析构函数的要求在字面上没有改动。但是 C++20 引入了预期的(prospected)析构函数(n4861, 11.4.7),它是
~T()
形式的特殊成员函数,也包括析构函数。 C++20 中一个类可以有多个预期的析构函数,但其中只有重载决议在类定义末尾选择的唯一一个才成为析构函数。
附注
标量类型(含 cv 限定版本)及其数组满足标准布局类型(standard-layout type)、可平凡复制类型(trivially copyable type)、平凡类型(trivial type)的要求。
标准布局类、可平凡复制类、平凡类的数组也分别是标准布局类型、可平凡复制类型、平凡类型。
可以简单地说 POD 类型就是同时满足标准布局类型和平凡类型的类型。
C++20 中, POD 类型这一用词被弃用,未来可能会被移除。