從一個 C++ class 自動生成另外一個 adaptor class

Share on:

本文內容採用創用 CC 姓名標示-非商業性-相同方式分享 3.0 台灣 授權條款授權.

程式設計師常常會用到 code generator,code generator 可以只根據簡短的描述,自動產生出具有許多額外功能的 class 程式碼。舉筆者常用的 code generator 為例,protobuf 可以自動生成「把 JSON 解析成 C++ 中的 STL 物件」的 class(object serialization);verilator 可以把 Verilog 轉換成「可以用 C++/SystemC 進行模擬」的 class。

例如 verilator 可以使用這樣的 Verilog 的定義

1input               a, // 1 bit
2output       [30:0] b, // 31 bits
3output              c, // 1 bit
4input signed [14:0] d  // 15 bits signed

自動生成這樣的 C++ 程式碼:

1struct MyClass {
2    sc_core::sc_in <bool>         a;
3    sc_core::sc_out<unsigned int> b;
4    sc_core::sc_out<bool>         c;
5    sc_core::sc_in <short>        d;
6private:
7    // Lots of simulation code here
8};

用 verilator 舉例的話能可能大家不是很好理解,為了方便說明,我用 STL 來舉例的話,可以類比成 unique_ptr

1struct MyClass {
2    std::unique_ptr<bool>         a;
3    std::unique_ptr<unsigned int> b;
4    std::unique_ptr<bool>         c;
5    std::unique_ptr<short>        d;
6};

像上面這種 struct 因為是 code generator 產生出來的,對於程式設計師來說不好去改他。在這個例子中,假設產生出來是使用 unique_ptr,也知道這樣會一直有 new/delete 的問題,但是也沒版法改這個程式。

相信很多人看到這個情形,第一個想法就是套個 adaptor pattern 就好了。

 1struct MyClassAdaptor {
 2    bool         a;
 3    unsigned int b;
 4    bool         c;
 5    short        d;
 6    void Read(const MyClass& rhs) {
 7        a = *rhs.a;
 8        b = *rhs.b;
 9        c = *rhs.c;
10        d = *rhs.d;
11    }
12    void Write(MyClass& rhs) {
13        *rhs.a = a;
14        *rhs.b = b;
15        *rhs.c = c;
16        *rhs.d = d;
17    }
18};

這樣只要經過一次轉換,之後都只用 MyClassAdaptor 就能解決前述的效能問題了。

自動生成 Adaptor

但是這個產出一個新的問題,假設 code generator 產出另外一個 class,就要在寫一次 adaptor。偏偏每次的 adaptor 寫法又都長一樣,寫起來非常瑣碎,必須把這個步驟自動化。為此,我有以下考量:

  1. 自己再寫一個 code generator,但這個會讓編譯流程變複雜,而且自己寫的 code generator 可能也要額外的 debug。(否決)
  2. Boost prepocessor 應該可以解決這個問題,但是沒事的話其實不想加入額外的 dependency。此外,其實 boost prepocessor 能生成的 member 數量有上限也是個問題?(否決)
  3. 只用到 C++11 跟 prepocessor,有沒有辦法呢?(聽起來很有趣,就這個吧)

在我合理的想像中,最終應該要可以簡單到可以這樣定義出兩個 adaptor class:

1struct Adaptor1 {
2    DEFINE_ADAPTOR_FOR(MyClass, a, b, c)
3};
4struct Adaptor2 {
5    DEFINE_ADAPTOR_FOR(MyClass, d)
6};

可是只用單一 prepocessor 時,最大長度就很容易被限制住。用 varadic template 只能傳 class 當參數,傳 a, b, c 這些字眼進去應該不適用。此外,大多數的情形下,a, b, c 需要穿插出現在 class 的不同位置,提高了設計 prepocessor 的難度。

我的解法

單純使用 preprocessor 會出現的問題就是,我們希望展開之後的程式碼可以讓 a, b, c 在程式碼的多個地方、以不同的形式出現:

1// typename decltype(MyClass::X)::element_type X; for a, b, c
2void Read(const MyClass& rhs) {
3    // X = *rhs.X; for a, b, c
4}
5void Write(MyClass& rhs) {
6    // *rhs.X = X; for a, b, c
7}

這個跟我知道的 preprocessor 不太一樣,於是我想到的方法是把上面這段程式直接當作一個 template include 進來:

1struct Adaptor1 {
2#define TARGET_ADAPTOR_FOR MyClass
3#define TARGET_MEMBER USE(a) USE(b) USE(c)
4#include "magic_template.h"
5};

magic_template.h 內容如下:

 1#define USE(X) typename decltype(TARGET_ADAPTOR_FOR::X)::element_type X;
 2    TARGET_MEMBER
 3#undef USE
 4
 5void Read(const TARGET_ADAPTOR_FOR& rhs) {
 6#define USE(X) X = *rhs.X;
 7    TARGET_MEMBER
 8#undef USE
 9}
10
11void Write(TARGET_ADAPTOR_FOR& rhs) {
12#define USE(X) *rhs.X = X;
13    TARGET_MEMBER
14#undef USE
15}
16
17#undef TARGET_ADAPTOR_FOR
18#undef TARGET_MEMBER

這個作法中,TARGET_MEMBER 這個 preprocessor macro 必須要求程式設計師把每個要用到的 member 都包上 USE,template 中只要偷偷換掉這個 USE,呼叫 TARGET_MEMBER 就會自己展開成正確的程式碼了。

注意 magic_template.h 不要用 include guard 保護,雖然要 include 一個獨立的檔案這方法很詭異,但是至少蠻可用的。


  1. 從一個 C++ class 自動生成另外一個 adaptor class
  2. 從一個 C++ class 自動生成另外一個 adaptor class(二)