c++ 聚合/POD/平凡/標準布局 介紹

前言

因為要整理近期學習的c++特性,特地出一篇來介紹POD類型和c++11引進的TrivialStandard-layout

聚合

聚合是以下類型之一:

  • 數組類型
  • 類類型(通常,struct或union)具有
沒有用戶聲明的構造函數 (直到 C++11)
沒有用戶提供的構造函數(允許顯式默認或刪除的構造函數) (C++11 起) (C++17 前)
沒有用戶提供的、繼承的或顯式(explicit,c++17特意新加)的構造函數(允許顯式默認或刪除的構造函數) (C++17 起) (C++20 前)
沒有用戶聲明或繼承的構造函數(相當於=default不行了 (C++20 起)
  • 沒有virtualprivateprotected (C++17 起)基類
  • 沒有虛擬成員函數
沒有默認的成員初始化器 (C++11 起) (C++14 前)

聚合初始化。它是列表初始化 (C++11 起)或直接初始化 (C++20 起)的一種形式

POD(Plain Old Data)

POD規範著對象的類型,主要是為了兼容C,C++可以直接使用C庫函數操作POD數據類型,擁有POD特徵的類或結構體通過直接位元組拷貝或二進位拷貝後依然能保持數據結構不變,POD類型在C和C++間的操作總是安全的。

特徵:

  • 標量類型(算術類型(整型/浮點型)、指針、成員指針、枚舉類型)
  • 類類型(class、struct、union)
    • 從c++11起
      • 是平凡(trivial)類型(後續介紹)
      • 是標準布局(standard-layout)類型(後續介紹)
      • 所有非靜態成員都是POD類型

特性:

  • 完全與C兼容,但是仍然可以有成員函數;POD類型標準到甚至可以與其他語言兼容;
  • 可以用std::memcpy拷貝(對於非POD類型,即使滿足TriviallyCopyable,用std::memcpy拷貝的行為也是未定義的)
  • 有更長的生命周期,從資源獲取到資源釋放,而非POD類型的是從構造函數結束到析構函數結束;
  • POD類型對象的前部沒有填充位元組,即對象指針與第一個成員的指針是相等的

來自cppreference:PODType

平凡類型(TrivialType)

要求

來自cppreference:TrivialType

平凡可複製(TrivialCopyable)

要求

以下類型統稱為平凡可複製類型

  • 標量類型
  • 可簡單複製的類,即滿足以下要求的類:
    • 至少一個拷貝構造函數、移動構造函數、複製賦值運算符或移動賦值運算符符合條件
    • 每個符合條件的拷貝構造函數(如果有的話)都是平凡的
    • 每個符合條件的移動構造函數(如果有)都是平凡的
    • 每個符合條件的複製賦值運算符(如果有)都是平凡的
    • 每個符合條件的移動賦值運算符(如果有)都是平凡的
    • 有一個平凡的的未刪除析構函數
      • 這裡有很多符合條件的要求,但大致如下
        • 如果未刪除函數(拷貝構造、移動構造、拷貝賦值、移動賦值 (標準c++20前)),則它是符合條件的。符合條件的函數(上述四者)的平凡性決定了該類是否是隱式生命周期類型,以及該類是否是可平凡複製的類型
      • 這裡有很多平凡的概念,但大致如下
        • 它不是用戶提供的(意思是,它是被隱式定義或默認的)
        • 所在類沒有虛擬,包括虛基類和虛成員函數
        • 對每個非靜態類型類型(或類類型數組)成員,遞歸該要求
  • 平凡可複製對象 的數組

這意味著一個平凡可複製的類沒有虛函數虛基類

來自cppreference:TriviallyCopyable

使用is_trivially_copyable(C++11)可判斷類型是否是平凡可複製的

std::is_trivially_copyable

對於某些函數的補充說明

平凡拷貝構造函數

平凡可複製對象可以通過手動複製其對象表示來複制,例如使用std::memmove。所有與 C 語言兼容的數據類型(POD 類型)都可以輕鬆複製。

符合條件的移動構造函數

平凡移動構造函數是執行與普通拷貝構造函數相同的操作的構造函數,也就是說,就像通過std::memmove一樣製作對象表示的副本。所有與 C 語言兼容的數據類型(POD 類型)都可以輕鬆移動。

符合條件的拷貝賦值運算符

平凡拷貝賦值運算符生成對象表示的副本,就像通過std::memmove一樣。所有與 C 語言兼容的數據類型(POD 類型)都可以簡單地拷貝分配。

符合條件的移動賦值運算符

平凡移動賦值運算符執行與平凡拷貝賦值運算符相同的操作,即生成對象表示的副本,就像std::memmove一樣。所有與C語言兼容的數據類型(POD類型)都可以簡單地移動賦值。

符合條件的析構函數

平凡析構函數是不執行任何操作的析構函數。具有普通析構函數的對象不需要刪除表達式,並且可以通過簡單地釋放它們的存儲來處理。所有與 C 語言兼容的數據類型(POD 類型)都可以輕鬆破壞。

標準布局類型(Standard-layout Type)

標準布局規範著對對象的布局。標準布局類型對於與用其他程式語言編寫的程式碼進行通訊很有用。
當類或結構不包含某些C++語言功能(例如無法在C語言中找到虛函數),並且所有成員都具有相同的訪問控制時,該類或結構為標準布局類型。可以在記憶體中對其進行複製,並且布局已經充分定義,可以由C程式使用。標準布局類具有用戶定義的特殊成員函數。此外有以下特徵

  • 所有非靜態數據成員具有相同的訪問控制
  • 沒有虛函數或虛基類
  • 沒有引用類型的非靜態數據成員
  • 類類型的所有非靜態成員和基類本身都是標準布局類型
  • 沒有與第一個非靜態數據成員類型相同的基類
  • 滿足以下條件之一:
    • 最底層派生類中沒有非靜態數據成員,並且具有非靜態數據成員的基類不超過一個(換言之繼承樹中最多只能有一個類有非靜態數據成員),或者
    • 沒有含非靜態數據成員的基類

舉兩個例子,Base類和Derived類中都有非靜態數據成員,因為當Derived繼承於Base,有std::is_standard_layout<Derived>為falsestd::is_standard_layout<Base>為true

struct Base
{
    int i;
    int j;
};

// std::is_standard_layout<Derived> == false!
struct Derived : public Base
{
    int x;
    int y;
};

Derived 是標準布局,因為 Base 沒有非靜態數據成員:

struct Base
{
    void Foo() {}
};

// std::is_standard_layout<<Derived> == true
struct Derived : public Base
{
    int x;
    int y;
};

標準布局兼容

涉及兩個或兩個以上滿足標準布局的數據結構兼容問題,概括起來有點複雜,先直接拋cppreference鏈接看吧(後續再細看),在標準布局內容下邊

例子

類A滿足POD類型要求,即可直接通過位元組拷貝 拷貝其數據,在此情形下,位元組拷貝效率是很快的

class A
{
public:
    int a;
    int b;
};

int main()
{
    A a1;
    a1.a = 10;
    a1.b = 20;
    
    char* p = new char[sizeof(A)];
    memcpy(p, &a1, sizeof(A));

    A* a2 = reinterpret_cast<A*>(p);
    cout << a2->a << "\n" << a2->b << "\n";
}

總結

POD概念在C98中被提出,在C++20被啟用,取而代之的是在C++11引入的TrivialStandard-layout類型,因本文所介紹內容在《深度探索C++對象模型》中會被重點介紹,待後續閱讀完此書籍後,再對本文進行更多補充

引用部落格

聚合類型與POD類型

C++中的POD類型

《深度探索C++對象模型》

Tags: