但凡阅读过源码,或者看过一些比较畅销的c++书籍,其都提到一个概念POD(Plain Old Data),当第一次遇到该概念的时候,忽略。然后正是因为忽略了该概念的真正含义,导致对某些问题或者原理理解的不是很透彻,当时研究了很久,才把这个概念彻底搞懂,今天借助本文,聊聊POD。
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为以下几个之一:
std::nullptr_t
type好了,从上面的内容中提到,一个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比较简单,所以在后面的内容中,将针对前两个概念进行详细分析。
这个概念比较抽象,乃至于很难用一句简单的话来描述。于是查阅了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
:
举例如下:
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<A>::value << std::endl; // print false
std::cout << std::is_trivial<B>::value << std::endl; // print true
std::cout << std::is_trivial<C>::value << std::endl; // print true
std::cout << std::is_trivial<Base>::value << std::endl; // print true
std::cout << std::is_trivial<Derived>::value << std::endl; // print true
return 0;
}
平凡类型具有如下属性:
针对上述最后一个属性,示例如下:
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这个访问权限,如果进行交互操作,可能会导致其他意想不到的问题。
布局指的是类、结构体或者联合(Union)的成员在内存中的排列。标准布局定义了这样一种类型,它不使用C中不存在的而在CPP中存在的某些功能或者特性。如果某个类是标准布局,那么可以通过memcpy进行复制,而且可以与C语言中定义的同种类型进行交互。一言以蔽之,具有标准布局类的类或者结构体等与C兼容,并行可以通过C的API进行交互。
既然符合标准布局的类只具有C语言中存在的功能或者特性,那么,很容易总结出来标准布局的条件:
现在我们结合示例代码进行分析:
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)不是标准类型。
看到这块,你是不是基本上了解了什么是POD类型以及怎样判断某个类型是不是POD?可惜的是,你懂了也没用,因为自C++20标准弃用了POD以及std::is_pod和std::is_pod_v的概念,这是因为一方面,POD等同于平凡的标准布局,另一方面,在大多数情况下,平凡类型和标准布局这两个就满足了,以下摘自于C++标准委员会对POD弃用的解释:
The term POD no longer serves a purpose in the standard, it is merely defined, and restrictions apply for when a few other types preserve this vestigial property. The is_pod trait should be deprecated, moving the definition of a POD type alongside the trait in Annex D, and any remaining wording referring to POD should be struck, or revised to clearly state intent (usually triviality) without mentioning PODs.
今天的文章就到这,我们下期见!