c++ trivial, standard layout和POD型別解析

不曉得叫什麼 發表於 2022-11-29
C++

1. trivial型別

佔用一片連續的記憶體,編譯器可以重排成員變數的順序或者增加一些padding(為了對齊),因此,可以放心的使用memcpy等函式
但是,在c程式碼裡面使用可能會出問題(因為可能會被重排),有如下特點:

  • 沒有虛擬函式和虛基類
  • 基類也必須保證沒有non-trivial的構造/析構/operator函式
  • 成員變數的型別也必須保證沒有non-trivial的構造/析構/operator函式
  • 成員變數可以有public/protected/private修飾,且可以出現多個。
  • 構造/operator/析構可以有自定義的,但是必須有預設的

可以看出來,trivial主要是針對於 構造/operator/析構 三種函式限制的,普通函式只要不是虛擬函式,就沒有任何限制。
題外話,關於重排序,來看一個例子:

struct Foo {
          A a;
          B b;
          C c;
private:
          D d;
          E e;
          F f;
};

一般來講,abc的順序是不會重排的,def的順序是不會重排的,但是,因為abc和def擁有著不同的訪問控制符,是有可能def重排到abc前面的。官方解釋
如下:

Nonstatic data members of a (non-union) class declared without an intervening access-specifier are allocated
so that later members have higher addresses within a class object.
The order of allocation of nonstatic data members separated by an access-specifier is unspecified (11.1)

注意,靜態成員變數不影響

好了,定義部分講完了,來看幾個例子:

 //trivial型別,同時也是pod型別(後面會解釋)
class TrivialClass{
    int a_;
    bool flag_;
    //bool flag2_{false}; //這種就不是trivial了
};

//不是trivial型別
class NonTrivialClass{
    int a_;
    bool flag_;
    bool flag2_{false}; //比上面就多了一個初始化賦值,就不是trivial了,但是這個是stdlayout(後續會介紹)
};

class NonTrivialClass2{
    int a_;
    bool flag_;
    bool flag2_ = false; //同NonTrivialClass
};
//不是trivial型別
class NonTrivialClass3{
    NonTrivialClass3() {} //和NonTrivialClass3() = default的寫法是不一樣的,決定是否為trivial型別
    int a_;
    bool flag_;
    bool flag2_ = false; //同NonTrivialClass
};

//下面的也滿足trivial要求
class TrivialClass2{
public:
    //注意,靜態型別是不影響判斷的,因為靜態成員變數不會儲存在結構體/類裡面
    static std::string static_member;
    //也可以有建構函式,但是必須有預設的建構函式才行
    TrivialClass2(int a, int b): a_(a), b_(b) {}
    TrivialClass2() = default;

    void foo() {}
    int a_;
private:
    int b_;
    bool flag_;
};

雖然舉的例子都是沒有繼承的簡單類,但是如果有繼承,也有可能是trivial型別,需要保證基類也是trivial型別

2. standard layout型別

standard layout型別(以下簡稱stdlayout):不包含任何c語言裡沒有的功能,可以使用memcpy等函式並且可以在c程式碼裡放心使用這點不同於trvial的型別,同時,stdlayout型別也能擁有自己的函式,有如下特點:

  • 沒有虛擬函式和虛基類
  • 所有的非靜態成員變數必須擁有完全一樣的public/protected/private訪問控制符, 比如下面的TrivialClass2就不是stdlayout型別
  • 所有非靜態成員變數也必須是stdlayout型別
  • 所有的基類也必須是stdlayout
  • 不能把基類型別作為第一個非靜態成員變數
  • 下面的條件滿足一個即可(總結就是要麼子類有非靜態成員變數,要麼父類有):
    • 子類沒有非靜態成員變數並且只有一個基類有非靜態成員變數
    • 基類都沒有非靜態成員變數

注意,靜態成員變數不影響
可以看出來,stdlayout放寬了構造/operator/析構的限制,但是也收緊了訪問控制符的限制(只能有一種)
接下來看程式碼示例:

//滿足stdlayout的要求,但是因為沒有預設的建構函式,因此不滿足trivial的要求
class StdLayoutClass{
public:
    StdLayoutClass(int a, int b): a_(a), b_(b) {}
    int a_;
    int b_;
};

//和上面的結構體就多了一個private(許可權修飾符不一樣),這裡就不是stdlayout了
class ClassMixedAccess{
public:
    ClassMixedAccess(int a, int b): a_(a), b_(b) {}
    int a_;
private:
    int b_;
};

class StdLayoutBase{
public:
    StdLayoutBase() {} //和StdLayoutBase()=default的寫法是有區別的,決定了是否為trivial
    int a_;
    int b_;
};
//這個不是standard layout,因為基類有非靜態成員變數,也不是trivial,因為積累的建構函式不是default(注意,和=default的寫法是有區別的)
class StdLayoutDerived : public StdLayoutBase {
    int c_;
    int d_;
};

class StdLayoutBase2{
    StdLayoutBase2() = default;
};
//這個就滿足stdlayout的要求了,因為必須滿足子類或者基類只能有一個有非靜態成員變數
class StdLayoutDerived2 : public StdLayoutBase2 {
    StdLayoutDerived2() = default;
    int c_;
    int d_;

    //可以擁有自己的函式
    void test_func(){
        printf("hello world");
    }
};

3. 集大成者,POD(Plain Old Data)型別

POD型別:當一個類既是trivial又是stdlayout型別的時候,這個時候它就是pod型別,因此,pod型別的特點如下:

  • 擁有著連續的記憶體
  • 每一個成員變數的地址都高於其前一個成員變數(也就是沒有進行重排序操作)
  • 同樣的,也有巢狀要求,非靜態成員變數必須也是POD型別

綜上,POD型別可以在I/O操作中放心的進行copy和恢復, 基本型別比如int,char,引用等都是POD型別。
注意,靜態成員變數不影響。
示例程式碼如下:

class ClassWithVirtual{
 public:
     virtual bool foo() {}
 };
 //出現了虛擬函式,因此,trivial和stdlayout都不是
 class ClassWithVirtualDerived : public ClassWithVirtual {
 public:
     int a_;
     int b_;
     virtual bool foo() override{}
 };

 //有不同的訪問限定符,因此,屬於trivial但是不屬於stdlayout
 class ClassDiffAccess{
 public:
     int a_;
 private:
     int b_;
 };
 //擁有著自定義的建構函式(但是沒有顯示指定default),因此不屬於trivial
 class ClassWithoutDefaultConstructor{
 public:
     ClassWithoutDefaultConstructor() {};
     int a_;
     int b_;
 };
//集大成者,既屬於trivial也屬於stdlayout,也就是POD型別
 class PODClass {
     int a_;
     int b_;
 };

4. 測試程式碼

上述類,可以透過如下程式碼測試

#define BOOL_2_STRING(val) ((val) ? "true" : "false")

/**
* tool function
* @tparam T
*/
template<class T>
void test_trivial_stdlayout_pod_type_internal(const char *type_name) {
    bool is_trivial = std::is_trivial<T>::value;
    bool is_std_layout = std::is_standard_layout<T>::value;
    bool is_pod = std::is_pod<T>::value;
    printf("%s: is_trivial[%s]  is_standard_layout[%s]  is_pod[%s]\n",
           type_name, BOOL_2_STRING(is_trivial),
           BOOL_2_STRING(is_std_layout), BOOL_2_STRING(is_pod));
}

/**
 * 測試三大型別,trivial,standard layout, pod
 */
void test_trivial_stdlayout_pod_type() {
    test_trivial_stdlayout_pod_type_internal<TrivialClass>("TrivialClass");
    test_trivial_stdlayout_pod_type_internal<NonTrivialClass>("NonTrivialClass");
    test_trivial_stdlayout_pod_type_internal<NonTrivialClass2>("NonTrivialClass2");
    test_trivial_stdlayout_pod_type_internal<NonTrivialClass3>("NonTrivialClass3");
    test_trivial_stdlayout_pod_type_internal<TrivialClass2>("TrivialClass2");
    test_trivial_stdlayout_pod_type_internal<StdLayoutClass>("StdLayoutClass");
    test_trivial_stdlayout_pod_type_internal<ClassMixedAccess>("ClassMixedAccess");
    test_trivial_stdlayout_pod_type_internal<StdLayoutDerived>("StdLayoutDerived");
    test_trivial_stdlayout_pod_type_internal<StdLayoutDerived2>("StdLayoutDerived2");
    test_trivial_stdlayout_pod_type_internal<ClassWithVirtualDerived>("ClassWithVirtualDerived");
    test_trivial_stdlayout_pod_type_internal<ClassDiffAccess>("ClassDiffAccess");
    test_trivial_stdlayout_pod_type_internal<ClassWithoutDefaultConstructor>("ClassWithoutDefaultConstructor");
    test_trivial_stdlayout_pod_type_internal<PODClass>("PODClass");
}

/*
輸出如下:
TrivialClass: is_trivial[true]  is_standard_layout[true]  is_pod[true]
NonTrivialClass: is_trivial[false]  is_standard_layout[true]  is_pod[false]
NonTrivialClass2: is_trivial[false]  is_standard_layout[true]  is_pod[false]
NonTrivialClass3: is_trivial[false]  is_standard_layout[true]  is_pod[false]
TrivialClass2: is_trivial[true]  is_standard_layout[false]  is_pod[false]
StdLayoutClass: is_trivial[false]  is_standard_layout[true]  is_pod[false]
ClassMixedAccess: is_trivial[false]  is_standard_layout[false]  is_pod[false]
StdLayoutDerived: is_trivial[false]  is_standard_layout[false]  is_pod[false]
StdLayoutDerived2: is_trivial[true]  is_standard_layout[true]  is_pod[true]
ClassWithVirtualDerived: is_trivial[false]  is_standard_layout[false]  is_pod[false]
ClassDiffAccess: is_trivial[true]  is_standard_layout[false]  is_pod[false]
ClassWithoutDefaultConstructor: is_trivial[false]  is_standard_layout[true]  is_pod[false]
PODClass: is_trivial[true]  is_standard_layout[true]  is_pod[true]
*/