POD类型

发布时间 2023-12-08 15:22:52作者: Beasts777

文章参考:

爱编程的大丙 (subingwen.cn)

1. POD概述

1.1 意义

POD:是plain old data的缩写,即普通的旧数据。POD通常用于说明一个类型的属性,尤其是用户自定义类型的属性,具体来说它是指没有使用面向思想来设计的类/结构体。

POD含义为:

  • Plain:表示是一个普通的类型。(说明支持静态初始化)
  • Old:体现了与C地兼容性,支持标准C函数。(说明与C语言有一样的内存布局)

在C++11中将POD划分为两部分:

  • 平凡的trivial
  • 标准布局的standard layout

1.2 作用

  • 字节赋值:对于POD类型,我们可以安全的使用memsetmemcpy进行初始化和拷贝等操作。
  • 提供对C内存布局兼容:POD类型的数据在C和C++之间的操作总是安全的,因此C++程序可以和C函数进行相互操作。
  • 保证了静态初始化的安全有效:静态初始化在很多时候可以提高程序的性能,而POD类型的对象初始化往往更加简单。

2. "平凡"类型

一个平凡的类/结构体要同时满足以下几点要求:

  1. 拥有平凡的默认构造函数(trivial constructor)和平凡的默认析构函数(trivial destructor)。

    • 一般而言,如果不定义构造函数,那么编译器会为我们生成一个平凡的默认构造函数

      class Test{}		// 编译器生成平凡的默认构造函数
      
    • 一旦定义了构造函数,即使没有参数,函数体没有代码,那么该构造函数也不是“平凡”的。

      class Test{
          Test(){}		// 不是平凡的默认构造函数
      }
      
    • 例外在于:可以使用= default关键字,来显式地声明默认地构造函数,从而使类型恢复平凡化

      class Test{
      	Test() = default {}			// 显式声明平凡的构造函数
      }
      
    • 析构函数同理。

  2. 拥有平凡的拷贝构造函数(tivial copy constructor)和平凡的移动构造函数(trivial move constructor)

    • 平凡的拷贝构造函数基本上等同于使用memcpy进行类型的构造。
    • 同平凡的默认构造函数一样,在不声明拷贝构造函数地时候,编译器会自动帮程序员生成。
    • 如果定义了拷贝构造函数,那么可以使用=default来显式声明拷贝构造函数。
    • 移动构造函数和拷贝构造函数同理。
  3. 用于平凡的拷贝赋值运算符(trivial assignment operator)和平凡的移动赋值运算符(trivial assignment operator)。

    • 逻辑与第2条一致。
  4. 不包含虚函数和虚基类:

    • 虚函数:使用virtual修饰的函数叫做虚函数。
    • 虚基类:继承时使用virtual关键字修饰基类。

3. “标准布局”类型

标准布局主要指的是结构体地结构或者组合方式。

要成为标准布局,需要同时满足下列五点定义(前两条最重要):

  1. 所有非静态成员都有相同的访问权限。

    class Test{
    private:
        static int a;
    public:
        int b;
        int c;
        Test(){}
        void func_1();
    }
    
  2. 在类或者结构体继承时,继承树中只能有一个类有非静态成员变量。

    struct Base{static int a;};
    struct Child: public Base{int b;};		// ok
    struct Base1{int a;};
    struct Child1: public Base1{static int b;};		// ok
    struct Child2: public Base, public Base1{static int b;}		// ok
    struct Child3: public Base1{int b;};		// error
    struct Child4: public Base1, public Child{static int num;};		// error
    
  3. 子类中第一个非静态成员的类型不可以是基类类型。

    struct Base{};
    struct Child1: public Base{
        Base b;			// 这不是一个标准布局,因为第一个非静态成员是基类的类型
        int a;
    }
    struct Child2: public Base{
        static Base b1;
        int a;			// 是一个标准布局,子类的第一个非静态成员不是基类的类型
        Base b2;
    }
    

    注:这条规则主要是为了节约内存,提高数据的读取效率。对于上述两种继承情况,在基类没有成员的情况下,两个子类Child1Child2的内存结构是不一样的:

    • Child1:如果子类的第一个非静态成员仍是基类类型,C++标准要求类型相同的对象它们的地址必须不同,此时需要分配额外的地址空间将二者的地址错开。
    • Child2:如果子类的第一个非静态成员不是基类类型,C++标准允许标准布局类型派生类的第一个成员和基类这个类本身共享地址,此时基类变量实际上没有占据任何的实际空间
  4. 没有虚函数和虚基类。

  5. 所有非静态数据成员均符合标准布局类型,其基类也符合标准布局。(这是一个递归的定义)

4. 判断是否为POD类型

4.1 对“平凡“进行判断

C++11提供类模板std::is_trivial来检测一个类型是否trivial,定义如下:

template <class T>
struct std::is_trivial

通过模板类的成员value,可以获取类型T是否是平凡的,返回一个布尔值,语法如下:

std::is_trivial<T>::value;

EG:

  • 代码:

    #include <iostream>
    using namespace std;
    
    struct A{};
    struct B{
    public:
        B(){}
    };
    struct C: public A{};
    struct D: public B{};
    struct E: virtual public A{};
    struct F{
    public:
        virtual void func(){}
    };
    
    int main(void){
        cout << "A:" << is_trivial<A>::value << endl;
        cout << "B:" << is_trivial<B>::value << endl;
        cout << "C:" << is_trivial<C>::value << endl;
        cout << "D:" << is_trivial<D>::value << endl;
        cout << "E:" << is_trivial<E>::value << endl;
        cout << "F:" << is_trivial<F>::value << endl;
        cout << "int:" << is_trivial<int>::value << endl;
        using Arr = A[];
        cout << "A[]:" << is_trivial<Arr>::valur << endl;
        return 0;
    }
    
  • 输出:

    A:1
    B:0
    C:1
    D:0
    E:0
    F:0
    int:1
    A[]:1
    
  • 分析:

    • A:拥有默认的构造函数、析构函数,属于trivial类型。
    • B:自定义了构造函数,因此不属于trivial类型。
    • C:虽然继承了A,但A也不含不符合标准的内容,因此属于trivial类型。
    • D:基类自定义了构造函数,继承后D内也相当于有了构造函数,因此不属于trivial类型。
    • E:继承关系中有虚基类,因此不属于trivial类型。
    • F:类成员函数中有虚函数,因此不属于trivial类型。
    • int:标准内置数据类型,属于trivial
    • A[]:元素是平凡类型的数组总是平凡的,因此属于trivial类型。

4.2 对“标准布局”类型的判断

在C++11中,提供了模板类std::is_standard_layout来判断一个类型是否是为标准布局,定义如下:

template <typename T>
struct std::is_standard_layout;

通过模板类的成员value,可以获取类型T是否为标准布局,返回一个布尔值,语法如下:

std::is_standard_layout<T>::value

EG

  • 代码:

    #include <iostream>
    using namespace std;
    
    struct A{};
    struct B: public A{int a;};
    struct C{
    private:
        int a;
    public:
        int b;
    };
    struct D1{static int i;};
    struct D2{int i;};
    struct E1{static int i;};
    struct E2{int i;};
    struct D: public D1, public E1{int a;};
    struct E: public D1, public E2{int a;};
    struct F: public D2, public E2{static int a;};
    struct G: public A{
        int foo;
        A a;
    };
    struct H: public A{
        A a;
        int foo;
    };
    
    int main(void){
        cout << "A:" << is_standard_layout<A>::value << endl;
        cout << "B:" << is_standard_layout<B>::value << endl;
        cout << "C:" << is_standard_layout<C>::value << endl;
        cout << "D:" << is_standard_layout<D>::value << endl;
        cout << "E:" << is_standard_layout<E>::value << endl;
        cout << "F:" << is_standard_layout<F>::value << endl;
        cout << "G:" << is_standard_layout<G>::value << endl;
        cout << "H:" << is_standard_layout<H>::value << endl;
        return 0;
    }
    
  • 编译执行:

    g++ test.cpp -o test -std==c++11
    ./test
    
  • 输出:

    A:1
    B:1
    C:0
    D:1
    E:0
    F:0
    G:1
    H:0
    
  • 分析:

    • A:符合五条要求,是标准布局类型。
    • B:符合五条要求,是标准布局类型。
    • C:违反了第一条:所有非静态成员应该有相同的访问权限,因此不是标准布局类型。
    • D:符合五条要求,是标准布局类型。(虽然有继承,但继承树中只有一个类有非静态成员变量,因此不违反要求)
    • E:违反了第二条:在类或者结构体继承时,继承树中只能有一个类有非静态成员变量,因此不是标准布局类型。
    • F:违反了第二条:在类或者结构体继承时,继承树中只能有一个类有非静态成员变量,因此不是标准布局类型。
    • G:符合五条要求,是标准布局类型。
    • H:违反了第三条规则:子类中第一个非静态成员的类型不可以是基类类型,因此不是标准布局类型。