概述
POD,即Plain Old Data的缩写,plain代表普通,Old代表旧,从字面意思看是老的、普通的数据类型。这个概念由C++引入主要是为了与C兼容,或者说POD就是与C兼容的那边部分数据类型。在C++对POD类型进行序列化生成二进制后,在C语言中可以对该二进制进行解析成功。如果对于一个非POD类型,假如包含虚函数的class,大家知道编译器在操作的时候会加入虚函数指针,但是虚函数这个概念在C语言中不存在,遇到这种数据编译器就不认识了,或者说对于一个非POD类型的数据,C语言是不识别的,于是C++就提出了POD数据类型的概念。
POD的一种常见用法是跨系统或者跨语言进行通讯,比如与C/.NET等编写的代码进行通讯。
概念
struct A { int x; int y; }; struct B { private: int x; public: int y; }; struct C { int a; int b; C(int x, int y) :a{ x }, b{ y } {} };
那么,问题来了,上述三个类型A、B和C,哪个是POD类型?
如果我们不清楚POD的判断标准的话,只能靠猜来回答该问题,幸运的是Modern Cpp提供了接口来进行判断(PS:下面的trivial和standard layout用来辅助判断):
C++11 | C++17 | 描述 |
std::is_pod | std::is_pod_v | 通过其value是否为ture来表示是否为POD类型(std::is_pod::value) |
std::is_trivial | std::is_trivial_v | 通过其value是否为ture来表示是否为平凡类型(std::is_trivial::value) |
std::is_standard_layout | std::is_standard_layout_v | 通过其value是否为ture来表示是否为标准布局(std::is_standard_layout::value) |
如果使用上述接口对前面例子中的对象A、B和C进行判断的话,结果如下:
类型 | Trivial(平凡类型) | Standard layout(标准布局) | POD |
A | 是 | 是 | 是 |
B | 是 | 否 | 否 |
C | 否 | 是 | 否 |
从上述结果可以看出,B是平凡类型,C是标准布局,A既是平凡类型,又是标准布局,同时也是POD类型,这就引出了POD的定义:
A POD type is a type that is both trivial and standard-layout. This definition must hold recursively for all its non-static data members.
通过上述定义可以看出,POD类型既是平凡类型又是标准布局,反过来可以理解为如果一个类型既是平凡类型又是标准布局,且其内部非静态成员变量也满足该条件(既是平凡类型又是标准布局),那么这个类型就是POD类型。
标准对POD定义如下:
A POD class is a class that is both a trivial class and a standard-layout class, and has no non-static data members of type non-POD class (or array thereof). A POD type is a scalar type, a POD class, an array of such a type, or a cv-qualified version of one of these types.
与前一个定义相比,新增了一个类型scalar type,cppference中提到,scalar type为以下几个之一:
• an arithmetic type
• an enumeration type
• a pointer type
• a pointer-to-member type
• the std::nullptr_t type
• cv-qualified versions of the above types
好了,从上面的内容中提到,一个POD类型的类,其非静态成员变量也必须是POD的,对静态成员变量和成员函数则没有这个要求,如下这个类D,其仍然是POD:
struct D { int a; int b; static std::string s; int get(){ return a; } };
在本小节中,我们提到了三个概念:Trivial(平凡类型)、Standard layout(标准布局)以及Scalar type,对于最后一个Scalar type比较简单,所以在后面的内容中,将针对前两个概念进行详细分析。
Trivial
这个概念比较抽象,乃至于很难用一句简单的话来描述。于是查阅了cppreference,显示标准对这块也没有一个完整的定义:
Note: the standard doesn't define a named requirement with this name. This is a type category defined by the core language. It is included here as a named requirement only for consistency.
于是搜索了相关资料,微软官网对这块的定义如下:
When a class or struct in C++ has compiler-provided or explicitly defaulted special member functions, then it is a trivial type. It occupies a contiguous memory area. It can have members with different access specifiers. In C++, the compiler is free to choose how to order members in this situation.
也就是说,当一个类型(class/struct )同时满足以下几个条件时,它就是 trivial type:
• 没有虚函数或虚基类。
• 由编译器生成(使用=default或者=delete)默认的特殊成员函数,包括默认构造函数、拷贝构造函数、移动构造函数、赋值运算符、移动赋值运算符和析构函数。
• 数据成员同样需要满足条件 1 和 2。
举例如下:
class A { public: A(int i) : n(i) {} A() {} private: int n; }; class B { public: B(int i) : n(i) {} B() = default; private: int n; }; class C { public: C(int i) : n(i) {} C() = delete; private: int n; }; struct Base { int x; int y; }; struct Derived : public Base { private: int z; }; int main() { std::cout << std::is_trivial::value << std::endl; // print false std::cout << std::is_trivial::value << std::endl; // print true std::cout << std::is_trivial::value << std::endl; // print true std::cout << std::is_trivial ::value << std::endl; // print true std::cout << std::is_trivial ::value << std::endl; // print true return 0; }
平凡类型具有如下属性:
• 占据一块连续的内存区域
• 由于对齐要求,成员变量之间可以填充对齐字节(padding)
• 可以使用 memcpy进行对象拷贝
• 可以将一个平凡的类型通过memcpy()放入char或者unsigned char数组,然后可以把数组内的内容重新组装成一个该类型对象
• 允许有多个不同的访问控制符,但是,在这种情况下,编译器有可能对其进行重排
针对上述最后一个属性,示例如下:
struct C { public: int x; private: int y; public: int z; };
编译器可以将其重新排序为如下这种:
struct C { public: int x; int z; private: int y; };
正是因为如上原因(访问权限和编译器重排),普通类型不能安全的与其他语言编写的代码进行交互操作。我们以C语言为例,编译器的重排导致不能不能与C语言兼容(或者说C语言解析失败),因为C语言不识别private这个访问权限,如果进行交互操作,可能会导致其他意想不到的问题。
Standard layout
布局指的是类、结构体或者联合(Union)的成员在内存中的排列。标准布局定义了这样一种类型,它不使用C中不存在的而在CPP中存在的某些功能或者特性。如果某个类是标准布局,那么可以通过memcpy进行复制,而且可以与C语言中定义的同种类型进行交互。一言以蔽之,具有标准布局类的类或者结构体等与C兼容,并行可以通过C的API进行交互。
既然符合标准布局的类只具有C语言中存在的功能或者特性,那么,很容易总结出来标准布局的条件:
1. 没有虚函数或者虚基类
2. 没有引用类型的非静态成员变量
3. 所有的非静态成员变量具有相同的访问控制权限
4. 所有的非静态成员变量和基类都是标准布局
5. 没有多重继承导致的菱形问题
6. 子类中的第一个非静态成员的类型与其基类不同
7. 在class或者struct继承时,满足以下两种情况之一(总结就是要么子类有非静态成员变量,要么父类有):
• 派生类中有非静态成员,且只有一个仅包含静态成员的基类
• 基类有非静态成员,而派生类没有非静态成员
现在我们结合示例代码进行分析:
struct A{ }; struct B { A a; double b; }; struct C { void foo() {} }; struct D : public C { int x; int y; };
依据前面标准布局的要求,上述几个类A、B、C和D都是标准布局。现在我们构造稍微复杂点的例子:
struct E { int x; }; struct F : public E { int y; }; struct G { int x; private: virtual void foo() {}; }; struct H {}; struct X : public H {}; struct Y : public H {}; struct K : public X, Y {}; struct L { int x; private: int y; };
上面这些例子中,E是标准布局,G不属于标准布局(虚函数,不满足条件1),K不属于标准布局(菱形继承,不满足条件5),L不属于标准布局(不同的访问权限,不满足条件3)
接着我们看下前面条件中比较难理解的一个子类中的第一个非静态成员的类型与其基类不同,示例如下:
struct M { int x; }; struct N : public M { M m; int a; }; struct X { int x; }; struct Y : public X{ }; struct Z : public X { int y; };
在上述例子中,M、X和Y是标准布局,而N(不满足条件6)和Z(不满足条件7)不是标准类型。
审核编辑:刘清
全部0条评论
快来发表一下你的评论吧 !