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个基类地址与派生类地址第一个非静态成员地址关系:
C++ POD类型

可以看到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::value 打印出类型的标准布局属性。

#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.

原文链接: https://www.cnblogs.com/fortunely/p/16069895.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    C++ POD类型

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/402687

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月21日 上午11:10
下一篇 2023年4月21日 上午11:10

相关推荐