靈活的使用C ++中的反射系統

dl小寶 發佈 2020-04-21T06:29:49+00:00

例如,這個實現TypeDescriptor_StdString::dumpconst override { std::cout << "std::string{\"" << * obj << "\"}";

本文會展示一個小巧靈活的系統,用於使用C ++ 11語言功能進行運行時反射。這是一個為C ++類型生成元數據的系統。元數據採用TypeDescriptor在運行時創建的對象的形式,該對象描述其他運行時對象的結構。

我將這些對象稱為類型描述符。我寫這個反射系統的最初動機是為了支持序列化用於一個C ++遊戲引擎,因為有非常具體的需求,一旦可行,我也開始將運行時反射用於其他引擎功能:

  • 3D渲染:每次遊戲引擎使用OpenGL ES進行繪製時,它都會使用反射來傳遞統一的參數並向API描述頂點格式,這樣會使圖形編程更加高效!
  • 導入JSON:引擎的管道具備通用例程,可以從JSON文件和類型描述符合成C ++對象。它用於導入3D模型,關卡定義和其他應用。

該反射系統基於預處理器宏和模板。至少以當前形式,C ++並非旨在使運行時反射變得容易。眾所周知,編寫一個易於使用,易於擴展且切實可行的反射系統非常困難。在安頓我今天擁有的系統之前,我被模糊的語言規則,初始化順序的錯誤和極端的情況困擾了很多次。

為了說明其工作原理,我在GitHub上發布了一個示例項目:(代碼貼在文末處)

該示例實際上並未使用我的遊戲引擎的反射系統。而是使用了自帶的微型反射系統,但類型描述符的創建結構化尋找方式幾乎相同。這就是我將在這篇文章中重點討論的部分。

本文供那些對如何開發運行時反射系統感興趣的程式設計師,而不僅僅是使用涉及C ++的高級功能,但是示例項目只有242行代碼,因此希望能夠持續疊代,任何C ++程式設計師都提交代碼,如果您對使用現有解決方案更感興趣,請查看RTTR。

示例

在中main.cpp,示例項目定義了一個名為的結構Node。該REFLECT()宏告訴系統,以使這種類型的反射。

struct Node {
    std::string key;
    int value;
    std::vector<Node> children;

    REFLECT()      // 此類型啟用反射
};

在運行時,該示例創建一個類型為的對象Node。

//創建類型為Node的對象 
Node node = {"apple", 3, {{"banana", 7, {}}, {"cherry", 11, {}}}};

在內存中,Node對象看起來像這樣:

接下來,該示例找到Node的類型描述符。為此,必須將以下宏放在main.cpp文件中的某個位置。我將它們放在中Main.cpp,但也可將它們放在Node可見其定義的任何文件中。

//定義節點的類型描述符
REFLECT_STRUCT_BEGIN(Node)
REFLECT_STRUCT_MEMBER(key)
REFLECT_STRUCT_MEMBER(value)
REFLECT_STRUCT_MEMBER(children)
REFLECT_STRUCT_END()

Node現在據說可以反映其成員變量

Node可以通過調用獲得指向類型描述符的指針reflect::TypeResolver<Node>::get():

//查找Node的類型描述符
reflect::TypeDescriptor* typeDesc = reflect::TypeResolver<Node>::get();

找到類型描述符後,該示例將其用於將Node對象的描述轉儲到控制台。

//將Node對象的描述轉儲到控制台
typeDesc->dump(&node);

運行的輸出為:

宏的實現方式

將REFLECT()宏添加到結構或類時,它會聲明兩個其他靜態成員:Reflection,結構的類型描述符和initReflection初始化它的函數。實際上,展開宏後,完整的Node結構如下所示:

struct Node {
    std::string key;
    int value;
    std::vector<Node> children;

    // Declare the struct's type descriptor:
    static reflect::TypeDescriptor_Struct Reflection;

    // Declare a function to initialize it:
    static void initReflection(reflect::TypeDescriptor_Struct*);
};

同樣,展開後的REFLECT_STRUCT_*()宏塊main.cpp如下所示:

//定義結構類型描述符: 
reflect::TypeDescriptor_Struct Node::Reflection{Node::initReflection};

//初始化它的函數的定義:
void Node::initReflection(reflect::TypeDescriptor_Struct* typeDesc) {
    using T = Node;
    typeDesc->name = "Node";
    typeDesc->size = sizeof(T);
    typeDesc->members = {
        {"key", offsetof(T, key), reflect::TypeResolver<decltype(T::key)>::get()},
        {"value", offsetof(T, value), reflect::TypeResolver<decltype(T::value)>::get()},
        {"children", offsetof(T, children), reflect::TypeResolver<decltype(T::children)>::get()},
    };
}

現在,由於Node::Reflection是靜態成員變量,因此initReflection()在程序啟動時會自動調用其構造函數,該構造函數接受指向的指針。您可能想知道:為什麼將函數指針傳遞給構造函數?為什麼不通過初始化列表呢?答案是因為函數的主體為我們提供了聲明C ++ 11 類型別名的位置:using T = Node。沒有類型別名,所以我們必須將標識符Node作為額外的參數傳遞給每個REFLECT_STRUCT_MEMBER()宏。

如上所示,在函數內部還有另外三個對的調用reflect::TypeResolver<>::get()。每個人都會找到的反映成員的類型描述符Node。這些調用使用C ++ 11的decltype說明符自動將正確的類型傳遞給TypeResolver模板。

查找類型描述符

(請注意,本節中的所有內容都在reflect名稱空間中定義。)

TypeResolver是一個類模板。當你調用TypeResolver<T>::get()特定類型T,編譯器實例化相應的回報功能TypeDescriptor的T。它適用於反射結構以及這些結構的每個反射成員。默認情況下,這是通過主模板進行的,如下所示。

默認情況下,如果T是包含REFLECT()宏之類的結構(或類),如Node中get()將返回指向該結構Reflection成員的指針-這就是我們想要的。對於其他所有類型T,get()則調用getPrimitiveDescriptor<T>處理原始類型的函數模板,例如intstd::string

//聲明處理原始類型(例如int,std :: string等)的函數模板:
template <typename T>
TypeDescriptor* getPrimitiveDescriptor();

//以不同方式查找TypeDescriptor的幫助器類:
struct DefaultResolver {
    ...

    //如果T具有名為「 Reflection」的靜態成員變量,則調用此版本:
    template <typename T, /* 在SFINAE 處 */>
    static TypeDescriptor* get() {
        return &T::Reflection;
    }

    //否則稱為此版本:
    template <typename T, /* 在SFINAE 處 */>
    static TypeDescriptor* get() {
        return getPrimitiveDescriptor<T>();
    }
};

//這是查找所有TypeDescriptor的主要類模板:
template <typename T>
struct TypeResolver {
    static TypeDescriptor* get() {
        return DefaultResolver::get<T>();
    }
};

T使用SFINAE可以實現這一點的編譯時邏輯:根據是否存在靜態成員變量生成不同的代碼。我從上面的代碼段中省略了SFINAE代碼,坦率來講,看上去雖然很醜陋,但是可以在原始碼中檢查實際的實現。使用可以更優雅地重寫其中的一部分if constexpr,但是我的目標是C ++ 11。即使這樣,T至少可以在C ++採用靜態反射之前,檢測是否具有特定成員變量的部分。

TypeDescriptor的結構

在示例項目中,每個項目TypeDescriptor都有一個名稱,大小和幾個虛擬函數:

struct TypeDescriptor {
    const char* name;
    size_t size;

    TypeDescriptor(const char* name, size_t size) : name{name}, size{size} {}
    virtual ~TypeDescriptor() {}
    virtual std::string getFullName() const { return name; }
    virtual void dump(const void* obj, int indentLevel = 0) const = 0;
};

示例項目永遠不會TypeDescriptor直接創建對象。而是由系統創建從派生的類型的對象TypeDescriptor。這樣一來,每個類型描述符可以根據容納額外的信息。

例如,返回的對象的實際類型TypeResolver<Node>::get()為TypeDescriptor_Struct。它有一個附加的成員變量,members其中包含有關的每個反映成員的信息Node。對於每個反映成員,都有一個指向另一個成員的指針TypeDescriptor,整個過程在內存中就是這樣子。我在TypeDescriptor用紅色圈出了各個子類:

在運行時,可以通過調用getFullName()其類型描述符來獲取任何類型的全名。大多數子類僅使用getFullName()返回的基類實現TypeDescriptor::name。在此示例中,唯一的例外是採用TypeDescriptor_StdVector描述std::vector<>專業化的子類。為了返回完整的類型名稱,例如"std::vector<Node>",它保留了指向其項目類型的類型描述符的指針。我們可以在上面的內存圖中看到這一點:有一個TypeDescriptor_StdVector對象,其itemType成員一直指向的類型描述符Node。

當然,類型描述符僅描述類型。為了對運行時對象進行完整的描述,我們既需要類型描述符,也需要指向對象本身的指針。

請注意,TypeDescriptor::dump()該對象接受指向的指針const void*。這是因為抽象TypeDescriptor接口要能處理任何對象類型,子類實現只需要確定其所需的類型即可。例如,這個實現TypeDescriptor_StdString::dump()。將轉換const void*為const std::string*。

virtual void dump(const void* obj, int /*unused*/) const override {
    std::cout << "std::string{\"" << *(const std::string*) obj << "\"}";
}

至於,以void這種方式強制轉換指針是否安全。顯然,如果傳入了無效的指針,則程序很可能崩潰。這就是為什麼在我的遊戲引擎中,void指針所代表的對象總是與它們的類型描述符成對出現。通過以這種方式表示對象,可以編寫多種通用算法。

在示例項目中,是將對象轉儲到控制台輸出實現功能的,其實我們還能實現將對象類型描述符運用作二進位格式的序列化框架中。

傳送門

閱讀詳細代碼,請移步:https://github.com/preshing/FlexibleReflection/tree/part1

關鍵字: