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

Share on:

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

程式設計師常常會用到 code generator,code generator 可以只根據簡短的描述,自動產生出具有許多額外功能的 class 程式碼。上一篇中提到了,程式設計師有時會想要替生成程式中的 class 的,加入新的功能,這時 adaptor pattern 就很好用了。

快速回顧

舉例而言,生成程式產生了有數個 unique_ptr 的 struct。

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 產生出來的,對於程式設計師來說不好去改他。想替 MyClass 加入一些功能時,相信很多人第一個想法就是套個 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};

但是這件事對每個 class 都要作一次,偏偏每個的 adaptor 寫法又都長一樣,寫起來非常瑣碎,必須把這個步驟自動化。上一篇文章筆者可以用每個 class 都 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};
 6struct Adaptor2 {
 7#define TARGET_ADAPTOR_FOR MyClass
 8#define TARGET_MEMBER USE(d)
 9#include "magic_template.h"
10};

上面的程式碼很快速的對 MyClass::a/b/c 自動生成一個型別正確的 Adaptor1,也對 MyClass::d 自動生成一個型別正確的 Adaptor2

自動生成 Adaptor (v2!)

雖然用起來沒啥問題,應該也符合標準。但是這個方法實在太不 C++ 了,而且每一個 class 都要 include header 一遍,這個方法實在是有點……詭異:

考慮下面的程式碼(原來 C++ 允許 class 跟 member 同名……),他是一個只能處理 MyClass::a 的 adaptor class:

1struct a {
2    typename decltype(MyClass::a)::element_type a;
3    void Read(const MyClass& rhs) {
4        a = *rhs.a;
5    }
6    void Write(MyClass& rhs) {
7        *rhs.a = a;
8    }
9};

那麼,一個能處理 MyClass::a/b/c 的 adaptor class 可以由繼承多個類似的 class 得到,這點跟 C++11 tuple 的實做方式類似:

 1template<typename CLS, typename ...MBR>
 2struct Adaptor: public MBR... {
 3    void Read(const CLS& rhs) {
 4        int arr[]{(MBR::Read(rhs), 0)...};(void)arr;
 5    }
 6
 7    void Write(CLS& rhs) {
 8        int arr[]{(MBR::Write(rhs),0)...};(void)arr;
 9    }
10};
11typedef Adaptor<a, b, c> Adaptor1;

在 Adaptor 中的 Read/Write 是把 base class 的 Read/Write 都呼叫一次就好了,這是 C++11 的功能。至於為什麼要創一個 arr 包住呢?啊 C++11 就要這樣寫啊,我也沒辦法

最後的實做:多使用了 namespace

但是,這樣的 class 名稱太容易撞名了,最後在實做時我決定額外用 MyClass 當作 namespace。

1namespace MyClass {
2    struct a...;
3}
4namespace MyClass {
5    typedef Adaptor<a, b, c> Adaptor1;
6}
7using MyClass Adaptor1;

這樣的程式碼用 macro 就蠻好作的了,最後寫 adaptor 時,程式設計師真的要寫的程式如下:

1#include "class_helper.h"
2TARGET_CLASS(MyClass) (
3    USE_MEMBER(a);
4    USE_MEMBER(b);
5    USE_MEMBER(c);
6    USE_MEMBER(d);
7)
8CREATE_GROUP_CLASS(Adaptor1, MyClass, a, b, c)
9CREATE_GROUP_CLASS(Adaptor2, MyClass, d)

class_helper.h 的實做如下:

 1// class_helper.h
 2#define TARGET_CLASS2(PART2) PART2 }
 3#define TARGET_CLASS(CLS) namespace CLS##_ns { typedef CLS TargetClass; TARGET_CLASS2
 4#define USE_MEMBER(MBR)\
 5    struct MBR {\
 6        typename decltype(TargetClass::MBR)::element_type MBR;\
 7        void Read(const TargetClass& rhs) {\
 8            MBR = *rhs.MBR;\
 9        }\
10        void Write(TargetClass& rhs) {\
11            *rhs.MBR = MBR;\
12        }\
13    };
14#define CREATE_GROUP_CLASS(NAME, CLS, ...)\
15    namespace CLS##_ns { typedef Adaptor<TargetClass, __VA_ARGS__> NAME; }\
16    using CLS##_ns::NAME;
17
18template<typename CLS, typename ...MBR>
19struct Adaptor: public MBR... {
20    void Read(const CLS& rhs) {
21        int arr[]{(MBR::Read(rhs), 0)...};(void)arr;
22    }
23
24    void Write(CLS& rhs) {
25        int arr[]{(MBR::Write(rhs),0)...};(void)arr;
26    }
27}

結論

本方法使用繼承來自動生成 adaptor,這個實做的想法跟 C++11 tuple 的方法很類似,但是實際用起來其實也有不少缺點:

  1. 因為每一個 member 都要生成一個 class,導致編譯器產生的 class 比較多,加上用了 parameter pack 跟繼承,稍微測試了一下發現 gcc 記憶體的用量大了不少。
  2. 同上,大概編譯也會比較慢,但是我沒有作詳細的測試。
  3. 同上,編譯錯誤訊息很難讀。
  4. 因為 C++17 才引入用 operator 展開 parameter pack 的方法,如果想支援不是用逗點展開的計算,在 C++11 沒有太簡單的方法能用。相比之下 macro 的方法大部分都是 C++03 的功能,支援度好很多。
  5. 此方法打的字還是多一點。

大致上都是缺點,除非真的很討厭上一篇 include 另外一段程式這個方法的人、或是覺得 C++ template 很酷的人,我認為應該沒有一定要用此方法的必要。


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