從一個 C++ class 自動生成另外一個 adaptor class
程式設計師常常會用到 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 寫法又都長一樣,寫起來非常瑣碎,必須把這個步驟自動化。為此,我有以下考量:
- 自己再寫一個 code generator,但這個會讓編譯流程變複雜,而且自己寫的 code generator 可能也要額外的 debug。(否決)
- Boost prepocessor 應該可以解決這個問題,但是沒事的話其實不想加入額外的 dependency。此外,其實 boost prepocessor 能生成的 member 數量有上限也是個問題?(否決)
- 只用到 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 一個獨立的檔案這方法很詭異,但是至少蠻可用的。
系列文連結
-
從一個 C++ class 自動生成另外一個 adaptor class
-
從一個 C++ class 自動生成另外一個 adaptor class(二)
- 從一個 C++ class 自動生成另外一個 adaptor class
- 從一個 C++ class 自動生成另外一個 adaptor class(二)