C++ POD类型
什么是POD类型?
POD是Plain Old Data,是C++定义的一类数据结构,如char,int,float,double等都是POD类型。Plain顾名思义,表明POD是个普通类型,Old代表是旧的,与C语言兼容,意味着可以用古老的memcpy()进行复制,memset()进行初始化等。也就是说,POD特征的类或结构体,通过二进制拷贝后,仍然能保持数据结构(内存布局)不必。
C++11中,将POD划分为2个基本概念的合集:平凡的(trivial)和标准布局的(standard layout)。
而一个POD类型,就是指平凡的类型。
先看平凡的定义,一个平凡的类或结构体应该符合以下定义:
1)拥有平凡的默认构造函数(trivial constructor)和析构函数(trivial destructor)
通常,不定义类的构造函数,编译器就会生成一个平凡的默认构造函数。而一旦定义了构造函数,即使构造函数不含参数,函数体也没有任何代码,那么该构造函数也不再是“平凡”的。
比如:
struct NoTrivial {
NoTrivial();
};
这里NoTrivial构造函数就是非平凡的,而下面的构造函数就是平凡的,因为是编译器默认合成的。
struct NoTrivial {
NoTrivial() = default;
};
2)拥有平凡的拷贝构造函数(trivial copy constructor)和移动构造函数(trivial move constructor)。
平凡的copy构造函数等同于用memcpy进行类型的构造。同平凡的默认构造函数一样,不声明copy构造函数的话,编译器会帮程序员自动地生成平凡的默认copy构造函数。
平凡的move构造函数与平凡的copy构造函数类似。
3)拥有平凡的拷贝赋值运算符(trivial assignment operator)和移动赋值运算符(trivial move operator)。
与平凡的copy构造函数和平凡的move构造函数类似。
4)不能包含虚函数和虚基类。
因为编译器会为包含虚函数/虚基类的类生成一个隐含的指向虚函数表的指针vptr,而要构造。
如何判断一个类型是否为平凡的类型?
除了看类和结构是否包含上面提到的4类定义,最简单方式,当然是看 类和数据结构的内置数据是否都是内置的基本类型(int、float等平凡类型),数组的元素是否为平凡类型。
C++11中,还可以通过辅助的类模板帮助判断:
template<typename T> struct std::is_trivial;
is_trivial的成员value可以用于判断T类型是否是一个平凡的类型。除了类和结构体,is_trivial也可以对内置的标量数据类型(int、float等)以及数组类型(元素类型是平凡的数组总是平凡的)进行判断。
示例:is_trivial判断指定类型是否为平凡的。
// 用支持C++11标准的编译器编译
#include <iostream>
#include <type_traits>
using namespace std;
struct Trivial1 { };
struct Trivial2 {
public:
int a;
private:
int b;
};
struct Trivial3 {
Trivial1 a;
Trivial2 b;
};
struct Trivial4 {
Trivial2 a[23];
};
struct Trivial5 {
int x;
static int y;
};
struct NonTrivial1 {
NonTrivial1() : z(42) { }
int z;
};
struct NonTrivial2 {
NonTrivial2() = default;
int y;
};
struct NonTrivial3 {
NonTrivial3();
int w;
};
NonTrivial3::NonTrivial3() = default;
struct NonTrivial4 {
Trivial5 c;
virtual void f();
};
int main()
{
cout << is_trivial<Trivial1>::value << endl; // 1
cout << is_trivial<Trivial2>::value << endl; // 1
cout << is_trivial<Trivial3>::value << endl; // 1
cout << is_trivial<Trivial4>::value << endl; // 1
cout << is_trivial<Trivial5>::value << endl; // 1
cout << is_trivial<NonTrivial1>::value << endl; // 0
cout << is_trivial<NonTrivial2>::value << endl; // 1
cout << is_trivial<NonTrivial3>::value << endl; // 0
cout << is_trivial<NonTrivial4>::value << endl; // 0
return 0;
}
注意:NonTrivial2和NonTrivial3的构造函数,虽然都是用的编译器生成的默认构造函数,但后者是相当于自定义了构造函数,不过用编译器生成的版本。
什么是标准布局?
POD包含另外一个概念,即标准布局。标准布局的类或结构体应该符合以下定义:
1)所有非静态成员有相同的访问权限(public,private,protected)。
// 非标准布局
struct {
public:
int a;
private:
int b;
};
// 标准布局
struct {
public:
int a;
int b;
};
2)在类或结构体继承时,满足下面两种情况之一:
- 派生类中有非静态成员,且只有一个仅包含静态成员的基类。
- 基类有非静态成员,而派生类没有非静态成员。
// 派生类有非静态成员d, 且只有一个仅包含静态成员的基类B1
struct B1 { static int a; };
struct D1 : B1 { int d; };
// 基类B2有非静态成员a, 派生类D2没有非静态成员
struct B2 { int a; };
struct D2 : B2 { static int d; };
struct D3 : B2, B1 { static int d; }; // D3有1个基类B1包含静态成员, 1个基类B2包含非静态成员
struct D4 : B2 { int d; }; // D4的基类B2包含非静态成员, 派生类D4也包含非静态成员
struct D5 : B2, D1 { }; // D5有2个基类B2, D1, 都包含了非静态成员, 但多重继承会导致类型布局的变化
D1, D2, D3都是标准布局的,D4, D5则不属于标准布局。实际上,非静态成员只要同时出现在派生类和基类间,就不属于标准布局。而多重继承也会导致类型布局的一些变化,所以一旦非静态成员出现在多个基类中,派生类也不属于标准布局。
3)类中第一个非静态成员的类型与其基类不同。
形如:
struct A : B { B b; };
这里A类型不是一个标准布局的类型,因为第一个非静态成员变量b的类型跟A所继承的类型B相同。而形如:
struct C : B {
int a;
B b;
};
这里C是一个标准布局的类型。
struct B1 {};
struct B2 {};
struct D1 : B1 {
B1 b; // 第一个成员仍然是基类
int i;
};
struct D2 : B1 {
B2 b; // 第一个成员并非基类
int i;
};
int main()
{
D1 d1;
D2 d2;
cout << hex;
// 7ffcdf314d00
cout << reinterpret_cast<long long>(&d1) << endl;
// 7ffcdf314d01
cout << reinterpret_cast<long long>(&d1.b) << endl;
// 7ffcdf314d04
cout << reinterpret_cast<long long>(&d1.i) << endl;
// 7ffcdf314d10
cout << reinterpret_cast<long long>(&d2) << endl;
// 7ffcdf314d10
cout << reinterpret_cast<long long>(&d2.b) << endl;
// 7ffcdf314d14
cout << reinterpret_cast<long long>(&d2.i) << endl;
return 0;
}
上面代码声明了4个类,其中2个没有成员的基类B1和B2,以及2个派生于B1的派生类D1和D2。D1,D2唯一区别是第一个非静态成员的类型。在D1中,第一个非静态成员的类型是B1,跟它的基类相同;而在D2中,第一个非静态成员的类型则是B2。
直观地看,D1和D2应该是“布局相同”的,程序员应该可以用memcpy这样的函数,在这2种类型间进行拷贝,但实际上并不是这样。
可以看到main函数中,打印出的D1类型变量d1和D2类型变量d2的地址,以及其成员的地址,是不一样的。
d1.b地址 = d1地址 +1,而d2.b地址 = d2地址。
在C++标准中,如果基类没有成员,则允许派生类的第一个成员与基类共享地址。因为派生类的地址总是“堆叠”在基类之上的,所以这样的地址共享,表明了基类没有占据任何的实际空间(节省一点数据空间)。但如果派生类第一个成员仍然是基类(如D1第一个成员B1 b),编译器仍会为该成员分配1byte空间。分配1byte空间,是因为C++标准要求类型相同的对象必须地址不同,即基类地址与派生类的成员b地址必须不同。
也就是说,在标准布局中,C++11标准强制要求派生类的第一个非静态成员的类型必须不同于基类。
下图表示代码示例中,2个基类地址与派生类地址第一个非静态成员地址关系:
可以看到D1类型对象d1中,从基类B1继承而来的数据地址跟d1相同,占1byte,跟D1自定义的B1类型对象b地址d1.b并不相同。
而在D2类型对象d2中,从基类B1继承而来的数据地址,跟D2自定义的B2类型对象b地址d2.b重叠,地址完全相同。
如何判断类型是一个标准布局的类型?
类似于用std::is_trivial判断一个类型是否为平凡的,C++11中,可以用std::is_standard_layout模板来判断类型是否是一个标准布局的类型。
例,用 std::is_standard_layout
#include <iostream>
#include <type_traits>
using namespace std;
struct SLayout1 { }; // 空类是标准布局
struct SLayout2 { // 所有非静态成员有相同访问属性, 成员类型是基本类型(不存在基类), 是标准布局
private:
int x;
int y;
};
struct SLayout3 : SLayout1 { // 第一个非静态成员x, 并非基类成员, 是标准布局
int x;
int y;
void f();
};
struct SLayout4 : SLayout1 { // 第一个非静态成员x, 并非基类成员, 是标准布局
int x;
SLayout1 y;
};
struct SLayout5 : /*SLayout1,*/ SLayout3 { }; // 第一个非静态成员(不存在), 并非基类成员, 是标准布局
struct SLayout6 { static int y; }; // 没有非静态成员, 是标准布局
struct SLayout7 : SLayout6 { int x; }; // 第一个非静态成员x, 并非基类成员, 是标准布局
struct NonSLayout1 : SLayout1 { // // 第一个非静态成员x, 是基类成员, 不是标准布局
SLayout1 x;
int i;
};
struct NonSLayout2 : SLayout2 { int z; }; // 非静态成员同时出现在派生类和基类中, 不是标准布局
struct NonSLayout3 : NonSLayout2 {}; // 非标准布局类型的派生类也不是标准布局
struct NonSLayout4 { // 非静态数据成员访问属性不同, 不是标准布局
public:
int x;
private:
int y;
};
int main()
{
cout << is_standard_layout<SLayout1>::value << endl; // 1
cout << is_standard_layout<SLayout2>::value << endl; // 1
cout << is_standard_layout<SLayout3>::value << endl; // 1
cout << is_standard_layout<SLayout4>::value << endl; // 1
cout << is_standard_layout<SLayout5>::value << endl; // 1
cout << is_standard_layout<SLayout6>::value << endl; // 1
cout << is_standard_layout<SLayout7>::value << endl; // 1
cout << is_standard_layout<NonSLayout1>::value << endl; // 0
cout << is_standard_layout<NonSLayout2>::value << endl; // 0
cout << is_standard_layout<NonSLayout3>::value << endl; // 0
cout << is_standard_layout<NonSLayout4>::value << endl; // 0
return 0;
}
参考
[1]MichaelWon, IBM XL编译器中国开发团队. 深入理解C++11:C++11新特性解析与应用[M]. 机械工业出版社, 2013.