0%

C++对象模型(三)POD

注:本节不讨论union

Scalar Type

第一个概念,Scalar Type,即标量类型。

所谓标量,就是一个数字,而标量类型,就是可以表示为一个数字的类型。

C++的标量类型为:

  • 各种整数/浮点类型,如int8_tuint32_tcharfloat等,可满足std::is_arithmetic<T>为true。
  • 枚举类型,可满足std::is_enum<T>为true。
  • 各种指针类型,包括std::nullptr_t,可满足std::is_pointer<T>std::is_member_pointer<T>为true。

以上类型都属于标量类型,都可满足std::is_scalar<T>::value为true。

Aggregate Type

第二个概念,Aggregate Type,即聚合类型。

所谓聚合类型,就是可以使用= {v1, v2, v3}这样语法(注意不是C++11的std::initializer_list特性)进行初始化或赋值的类型,对应C的数组和struct。

C++的聚合类型为:

  • 所有数组类型。
  • 满足以下条件的类(包括classstruct):
    • 所有非静态成员变量的访问权限都是public
    • 没有用户自定义的构造函数(但允许使用= default来显式使用编译器合成的构造函数,或使用= delete来显式禁止某种构造函数)。
    • 没有基类(C++17后允许有public的非虚基类)。
    • 没有虚函数。
    • 成员变量没有默认初始化式(不在构造函数里那种)(C++11新增,但似乎C++14又去掉了此限制)。

根据上面的定义,聚合类型还有下面的几个特点:

  • 不要求其所有非静态成员变量均为聚合类型。
  • 对静态成员没有任何限制。
  • 只对构造函数有限制,对析构函数、赋值函数等无限制。
  • 非聚合类型的数组也是聚合类型。

当我们写

1
Type a[m] = {b0, b1, ..., bn-1};

时:

  • 若m == n,则会发生b0到a[0]、b1到a[1]等等n次复制初始化。
  • 若m < n,则报错。
  • 若m > n,则a[0]-a[n-1]发生复制初始化,而a[n]-a[m-1]则发生默认初始化。
  • 若m为空,则a的长度会被设定为n,同样发生n次复制初始化。

对于下面的聚合类型

1
2
3
4
5
struct S {
TypeA a;
TypeB b;
TypeC c;
};

当我们写

1
S s = {a', b', c'};

时:

  • {}内元素数量与S中非静态成员变量数量相等,会按a’->a、b’->b、c’->c的方式进行复制初始化。
  • {}内元素数量更多,则报错。
  • S中非静态成员变量数量更多,则后面的成员发生默认初始化。

在初始化时:

  • 若发生复制初始化,则会调用相应类型的复制构造函数或赋值函数。
  • 若列表中某项为表达式,则复制/赋值时允许发生隐式转换(C++11开始要求不能是narrow转换)。
  • 若列表中某项本身也是个{}列表,则要求对应的数组元素/非静态成员变量也是聚合类型,尝试递归聚合赋值。
  • 标量类型的默认初始化会将其初始化为0、0.0、false等。
  • 引用类型的默认初始化会报错。

在对聚合类型(非数组)做列表赋值时,我们还可以指定成员的名字,如:

1
2
3
4
5
6
7
struct A {
int x;
int y;
int z;
};

A a = {.x = 1, .z = 2};

它有以下特点:

  • 列表中名字顺序必须符合成员顺序,即{.z = 2, .x = 1}是不行的。(注意,C允许乱序,还允许其它多种初始化方式,但C++不允许)
  • 列表中元素数量可以少于成员数量,未在列表中出现的成员发生默认初始化,即上例中a.y为0。

Trivial Type

第三个概念,Trivial Type,即平凡类型。

所谓平凡类型,可以认为是有bitwise语义的类型,即可以直接按字节复制的类型。C中的所有类型都是Trivial Type。

Trivial Type有两个标准:

  • 能trivial静态构造,即要有一个trivial的默认构造函数。
  • 能trivial拷贝,即满足Trivial Copyable标准。

Trivial Copyable类型即是满足std::is_trivially_copyable<T>::value为true的类型,它要求:

  • 所有复制构造、赋值函数要么是trivial的,要么是deleted。
  • 所有移动构造、赋值函数要么是trivial的,要么是deleted。
  • 至少有一个非deleted复制或移动的构造或赋值函数。
  • 析构函数为trivial,且非deleted。

构造函数、析构函数、复制构造/赋值函数、移动构造/赋值函数的trivial是指:

  • 满足bitwise语义。
  • 要么是编译器隐式合成的版本。
  • 要么通过= default显式使用编译器的合成版本。

以上条件也就意味着一个Trivial Type:

  • 不能有虚函数(会导致构造函数等失去bitwise语义)。
  • 不能有虚基类(同上)。
  • 如果有基类,基类也要是Trivial Type。
  • 不能有自定义的构造、析构、复制、移动函数。
  • 不能有非Trivial Type类型的非静态成员变量。

标量类型、Trivial Type的数组也是Trivial Type。

Trivial Type是用来区分那些可以像C一样通过memsetmemcpy等函数直接构造和复制的类型,C++11中增加了std::is_trivial模板来判断一个类型是否是trivial的。

注意,Trivial Type还有以下特点:

  • 不限制成员变量的访问限制,即publicprotectedprivate都可以。
  • 只要求默认构造函数是trivial的,对其它构造函数没有要求。

Standard Layout Type

第四个概念,Standard Layout Type,即标准布局类型。

标准布局的目的是定义一种与C兼容的内存布局,满足标准布局的类型即为标准布局类型,Standard Layout Type。

关于不同C++类型的内存布局,可以见上一篇文章C++对象模型(二)struct/class的内存布局

C++的Standard Layout Type要求:

  • 所有非静态成员变量有着相同的访问权限。

  • 没有虚函数或虚基类。

  • 没有引用类型的非静态成员变量。

  • 所有基类和非静态成员变量本身也是Standard Layout Type。

  • 该类型与其所有基类中,最多只能有一个类型有非静态成员变量(其它类型都需要是空类型),即所有非静态成员变量都在一个类型中。

  • 第一个非静态成员变量(包括继承自基类的成员)其类型不能与任一空基类相同(影响空基类优化)。

  • 该类型的继承树中同一类型不能出现多次。

    例子:

    1
    2
    3
    4
    struct Q {};
    struct S: Q {};
    struct T: Q {};
    struct U: S, T {};

    U的继承树中Q出现了两次,则QST都是Standard Layout Type,但U不是。

相同访问权限的原因:标准只规定了同一个section内成员的顺序,未规定不同section之间的顺序,因此若非静态成员变量分布在不同section下,无法给出一个确定的布局。

所有非静态成员变量都在一个类型中的原因:标准未规定基类子对象的位置,因此不同基类、或基类与子类的非静态成员变量间的顺序是未定义的。

没有虚函数或虚基类的原因:虚函数和虚基类会影响类的内存布局,但标准未规定其实现方式,因此有虚函数或虚基类的类型无法给出一个确定的布局。

第一个非静态成员变量不能与空基类类型相同的原因:标准规定同时存在的两个变量不能有相同地址,若应用空基类优化,则第一个非静态成员变量的地址与对象地址相同,也与所有空基类地址相同,若其中有相同类型,则导致该地址同时对应了多个变量。

C++11新增了std::is_standard_layout来判断一个类型是不是Standard Layout Type。

标准布局

C++的标准布局实际就是C中struct的布局,对于一个标准布局类的对象:

  • 其本身的地址与其所有基类子对象的地址相同,即基类子对象的地址无偏移。
  • 其各个非静态成员变量的位置按声明顺序从对象地址开始由低到高排列。
  • 其第一个非静态成员变量的地址与对象地址相同。
  • 其各个非静态成员变量的地址均满足对齐要求。

POD Type

最后一个概念,POD Type,即Plain Old Data Type,即可导出,可跨语言使用的类型(通常也意味着与C二进制兼容)。

一个POD类型为:

  • 标量类型。
  • 满足以下条件的自定义类型:
    • C++11之前:
      • 聚合类型。
      • 没有非POD类型的非静态成员变量。
      • 没有引用类型的非静态成员变量。
      • 没有自定义的构造函数或析构函数。
    • C++11之后:
      • 是平凡类。
      • 是标准布局类。
      • 没有非POD类型的非静态成员变量。
  • POD类型的数组。

可以看到POD的标准在C++11前后发生了很大的变化。C++11里放宽了对POD的限制,且根据这些限制的目的,提出了平凡类和标准布局类这两个更清晰的概念。在C++20后POD这个概念本身都会被去掉,而是在不同场合直接使用平凡类、标准布局类等概念。

一个类型可以只是平凡类或只是标准布局类:

  • 如果是平凡类,则意味着它可以直接通过memcpymemset等函数来操作。
  • 如果是标准布局类,则意味着它的布局是确定的,可以与其它语言交互。

可以用is_pod来判断一个类型是不是POD类型。

POD的用途

平凡类的用途:

  • 平凡类的对象可以与字节流之间安全转换,即:
    • 若要将对象转为字节流,直接取其地址即可。
    • 若要将字节流转为对象,直接将该地址cast为对象指针即可。
    • 直接通过复制字节的方式复制对象。
  • 安全的静态初始化。
    • C++11的thread_local变量可以是非平凡类型,但在某些编译器下会有比较大的性能开销。gcc扩展的__thread只能使用POD类型。

标准布局类的用途:

  • 跨进程、跨语言使用。