從一個 C++ class 自動生成另外一個 adaptor class(二)
程式設計師常常會用到 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 的方法很類似,但是實際用起來其實也有不少缺點:
- 因為每一個 member 都要生成一個 class,導致編譯器產生的 class 比較多,加上用了 parameter pack 跟繼承,稍微測試了一下發現 gcc 記憶體的用量大了不少。
- 同上,大概編譯也會比較慢,但是我沒有作詳細的測試。
- 同上,編譯錯誤訊息很難讀。
- 因為 C++17 才引入用 operator 展開 parameter pack 的方法,如果想支援不是用逗點展開的計算,在 C++11 沒有太簡單的方法能用。相比之下 macro 的方法大部分都是 C++03 的功能,支援度好很多。
- 此方法打的字還是多一點。
大致上都是缺點,除非真的很討厭上一篇 include 另外一段程式這個方法的人、或是覺得 C++ template 很酷的人,我認為應該沒有一定要用此方法的必要。
系列文連結
-
從一個 C++ class 自動生成另外一個 adaptor class
-
從一個 C++ class 自動生成另外一個 adaptor class(二)
- 從一個 C++ class 自動生成另外一個 adaptor class
- 從一個 C++ class 自動生成另外一個 adaptor class(二)