C++編譯期反射實踐——以AOP實現為例
編譯期反射實踐
自古以來,C++就一直缺少一個程式語言的重要特性——反射,但如果熟悉C++元模板程式設計的同學,就知道以C++的風格,肯定是不會在標準庫中新增執行時的反射支援的,從最新的C++版本演進來看,倒是編譯期反射可能得到更好的支援。C++11 -> C++14 -> C++17 -> C++20... 不斷讓元模板程式設計變得更簡單,更規範。
本次的編譯期反射實踐,程式碼要求的最低C++版本為14,因為用到了 make_shared、decay_t。
本次實踐的完整程式碼倉庫:MyUtil/tree/master/aop
獲取類的方法
判斷類是否具有某方法
我們如何判斷某個類是否具有某個方法呢?
要想在編譯期間實現這樣一個判斷,我們的思路可以是這樣:寫兩個模板,如果這個型別具有這個方法,就匹配到返回 std::true_type()
的模板,如果不具備則匹配到返回 std::false_type()
的模板,最後通過 std::is_same
能夠判斷匹配結果,也就是實現了在編譯期間判斷類是否有這個方法。
上述過程,利用 SFINAE 的原理可以輕鬆實現,如果不瞭解 SFINAE 以及對應的 enable_if 的運用,可以看看這篇文章:C++模板進階指南:SFINAE。
我們現在就開始動手實現上述程式碼,假設我們需要判斷一個型別是否有 before()
方法。
```cpp
template
private:
template
static auto Check(int)
-> decltype(
std::declval().before(std::declval
template
public:
enum {
value = std::is_same
};
};
```
先講下上述程式碼定義後如何使用吧,比如現在有個 Student 型別,我們來判斷是否具有 before 成員函式,則只需要寫下下面的程式碼:
cpp
has_member_before<Student,int>::value //判斷Student類是否有Student::before(int)方法
上面的程式碼重點有三段,已經作為標記1、2、3:
程式碼1處,利用了 std::declval
在編譯期建立型別U的例項,並呼叫其 before
方法,這是在元模板中判斷一個型別是否具有某個方法的常有手段,因為 SFINAE 的存在,即便該處替換出錯,編譯器會去繼續尋找下一個替換是否能夠正確,直到所有的替換都出錯。
很明顯這裡是一定會替換成功的,因為程式碼2有一個包容性很強的過載,這個過載的引數不能和程式碼1處的過載引數一致,否則會算作重複定義,當然如果你使用 std::enable_if
對引數一致的模板引數進行唯一性的限制,那麼重複定義的錯誤也可以避免。但是寫成 C 的可變引數是最快的解決方式。
程式碼1處,有個逗號表示式的細節,如果成功被程式碼1處替換,那麼返回值型別將會是 decltype()
中的表示式型別,也就是逗號表示式最後的結果 std::true_type
。
程式碼3是利用enum型別在編譯期得到具體的常量值。具體是通過呼叫 Check<T>(0)
獲取該函式的返回值型別,這期間模板的匹配替換就會牽扯到前面的程式碼1、2。所以一旦模板被例項化,那麼該class是否具有該方法的資訊也就清楚了。
最後我們可以把該段程式碼提取為巨集作為通用程式碼:
cpp
#define HAS_MEMBER(member) \
template <typename T, typename... Args> struct has_member_##member { \
private: \
template <typename U> \
static auto Check(int) \
-> decltype(std::declval<U>().member(std::declval<Args>()...), \
std::true_type()); \
template <typename U> static std::false_type Check(...); \
\
public: \
enum { \
value = std::is_same<decltype(Check<T>(0)), std::true_type>::value \
}; \
};
如果想要生成判斷是否有before或者其他方法的程式碼,則只需要呼叫這個巨集。
HAS_MEMBER(before) //生成判斷是否有before的程式碼
HAS_MEMBER(after) //生成判斷是否有after的程式碼
將類方法轉為function儲存
直接上程式碼,再逐一講解:
以下程式碼是將該類的before和after方法包裝成一個function,並返回一個pair。完整程式碼:reflect_util.hpp
cpp
template <typename T, typename... Args>
typename std::enable_if< //1
has_member_before<T, Args...>::value && has_member_after<T, Args...>::value,
std::pair<std::function<void(Args...)>, std::function<void(Args...)>>>::type
GetMemberFunc() {
auto fun = std::make_shared<std::decay_t<T>>(); //2
return std::make_pair( //3
[self = fun](Args &&...args) {
self->before(std::forward<Args>(args)...);
},
[self = fun](Args &&...args) {
self->after(std::forward<Args>(args)...);
});
}
在程式碼段1中,通過 enable_if
確保在該型別有before和after方法,同時也可以保證寫其他版本的時候不會出現重複定義的錯誤。enable_if
第一個引數是需要滿足的條件,第二個引數是enable_if內部的type型別,預設為void。
程式碼段2中,建立一個T型別的例項,並用shread_ptr管理,原因在於before方法和after方法需要共用記憶體,而這兩個方法都要被提取為單獨的function,要保證記憶體安全,故需要使用智慧指標。其中 std::decay_t<T>
效果等同於 std::decay<T>::type
,作用是消除T型別的const修飾和引用修飾。因為make_shared<>中的模板引數不能為引用型別。
程式碼段3中,利用lamda表示式將fun拷貝一份到其中命名為self,最後返回pair即可。
當前寫的功能是不完整的,需要多幾個模板的過載來實現只有before方法、以及只有after方法的情況。寫法和上述程式碼一致,只不過 enable_if
中的條件稍作改變即可。前面也提到過enable_if千萬不能丟,否則會報重複定義的錯誤,當然如果你是C++17的版本,可以直接使用 if constexpr
來實現更為簡潔的程式碼而無需單獨寫三個函式。
如下:
```cpp
#define ST_ASSERT \
static_assert( \
has_member_before
template
下面我簡單解釋下程式碼:
- ST_ASSERT巨集的作用是,通過static_assert在編譯期丟擲提示,T型別必須有before或after兩個方法之一。
- 通過該型別擁有的情況不同,給出不同的返回值。
很明顯去除了enable_if後,我們程式碼清爽了許多。
AOP的實現
關於AOP,大家可以去搜一搜,這裡就不過多贅述。我的簡單理解就是一個事件回撥,可以嵌入到業務的執行前後,把這個事件的概念換成一個切面,把業務程式碼看作一個橫向座標軸上的面,那麼AOP就是在這個面的前後新增其他切面來實現常用的業務複用。比如使用者的身份驗證,可以在業務之前新增身份驗證的切面,比如需要測試該業務的效能,那麼可以在業務切面的前後新增開始計時和終止計時的邏輯。
Invoke呼叫實現AOP
根據上述對AOP的描述,我們要切入的程式碼主要是前和後兩個邏輯,故每個要切入的類可以規定他必須定義Before或者After方法。然後通過可變參模板遞迴實現任意個引數的切面呼叫。
可以把整個切面呼叫過程看作一個洋蔥圈層,比如新增s1型別的before和after作為切片,s2型別的before和after作為切片,s3型別的before作為切片。把業務程式碼邏輯作為foo函式。
則他們的呼叫過程如下:
s1->before => s2->before => s3->before => foo業務邏輯 => s1->after => s2->after。
如果稍微學過點資料結果,這個呼叫就能想到前中後序遍歷上去了。
程式碼實現如下(C++11需要使用eable_if來實現,程式碼量很多,所以這裡就直接用C++17的 if constexpr
來實現了):
```cpp /以下是擷取的一個類的兩個方法/
// 遞迴的盡頭
template
// 變參模板遞迴
template
上述完整程式碼:aspect.hpp
上述程式碼是根據C++變參模板實現的通用性操作,可以同時新增多個切片 ,他們都是Aspect類的兩個方法,具體實現邏輯就是:通過之前得到的編譯期常量( has_member_Before<T,Args...>::value
)判斷 T 是否具有Before或者After方法,分三種情況:
-
同時又Before和After:利用中序進行遞迴。
-
只有Before:利用前序進行遞迴。
-
只有After:利用後序進行遞迴。
為了簡化呼叫過程,繼續封裝如下:
最後記得定義一個終止模板遞迴的最終形態。
```cpp
template
// AOP的輔助函式,簡化呼叫
template
最終如果像最開始講的要拓展s1、s2、s3的方法上去,那麼簡單的使用如下程式碼即可:
cpp
Invoke<s1,s2,s3>(&foo,args); //s1,s2,s3為拓展邏輯,foo為業務邏輯
統一轉function儲存並實現AOP呼叫順序
統一轉function儲存
將任意類的before和after方法集體裝箱為function的關鍵程式碼邏輯如下,完整程式碼請看:
```cpp void Get() {} //空的func,用於結束模板的遞迴例項化
template
由於所有的獲取before和after的邏輯在前面獲取類的方法已經講到,所以單個型別直接呼叫 GetMemberFunc
函式即可得出結果,並放入vector中,最後通過模板例項化的遞迴將所有的型別都裝箱。
具體的使用方式也很簡單,如下程式碼:
```cpp
include"reflect_util.hpp"
using func_t = reflect::MemberFunc
struct LoginAspect {
void before(int i) { cout << "Login start " << i << endl; }
void after(int i) { cout << "after start " << i << endl; }
};
int main(){
vector
AOP的呼叫順序實現
cpp
// 根據AOP的順序存入out陣列
void AspectOrder(vector<func_t> &out, vector<func_pair_t> &v,
const func_t &func, int index) {
if (v.size() <= index) {
out.push_back(func);
return;
}
if (v[index].first) {
out.push_back(v[index].first);
}
AspectOrder(out, v, func, index + 1);
if (v[index].second) {
out.push_back(v[index].second);
}
}
完整測試程式碼:test_aspect.cc