C++編譯期反射實踐——以AOP實現為例

語言: CN / TW / HK

編譯期反射實踐

自古以來,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 struct has_member_before {
private:
template
static auto Check(int) -> decltype( std::declval().before(std::declval()...),std::true_type() //1 );
template static std::false_type Check(...); //2

public:
enum {
value = std::is_same(0)), std::true_type>::value //3
};
}; ```

先講下上述程式碼定義後如何使用吧,比如現在有個 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::value || \ has_member_after::value, \ "class need T::before(args...) or T::after(args...) member function!");

template std::pair, std::function\> GetMemberFunc() { ST_ASSERT // 確保至少before after有其一 auto fun = std::make_shared>(); if constexpr (has_member_before::value && has_member_after::value) { // 有before和after return std::make_pair( self = fun { self->before(std::forward(args)...); }, self = fun { self->after(std::forward(args)...); }); } else if constexpr (has_member_before::value && !has_member_after::value) { // 有before return std::make_pair( self = fun { self->before(std::forward(args)...); }, nullptr); } else { // 只有after return std::make_pair(nullptr, self = fun { self->after(std::forward(args)...); }); } } ```

下面我簡單解釋下程式碼:

  1. ST_ASSERT巨集的作用是,通過static_assert在編譯期丟擲提示,T型別必須有before或after兩個方法之一。
  2. 通過該型別擁有的情況不同,給出不同的返回值。

很明顯去除了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 void Invoke(Args &&...args, T &&aspect) { ST_ASSERT if constexpr (has_member_Before::value && has_member_After::value) { aspect.Before(std::forward(args)...); // 核心邏輯之前的切面邏輯 m_func(std::forward(args)...); // 核心邏輯 aspect.After(std::forward(args)...); // 核心邏輯之後的切面邏輯 } else if constexpr (has_member_Before::value && !has_member_After::value) { aspect.Before(std::forward(args)...); // 核心邏輯之前的切面邏輯 m_func(std::forward(args)...); // 核心邏輯 } else { m_func(std::forward(args)...); // 核心邏輯 aspect.After(std::forward(args)...); // 核心邏輯之後的切面邏輯 } }

// 變參模板遞迴 template void Invoke(Args &&...args, T &&headAspect, Tail &&...tailAspect) { ST_ASSERT if constexpr (has_member_Before::value && has_member_After::value) { headAspect.Before(std::forward(args)...); Invoke(std::forward(args)..., std::forward(tailAspect)...); headAspect.After(std::forward(args)...); } else if constexpr (has_member_Before::value && !has_member_After::value) { headAspect.Before(std::forward(args)...); Invoke(std::forward(args)..., std::forward(tailAspect)...); } else { Invoke(std::forward(args)..., std::forward(tailAspect)...); headAspect.After(std::forward(args)...); // 核心邏輯之後的切面邏輯 } } ```

上述完整程式碼:aspect.hpp

上述程式碼是根據C++變參模板實現的通用性操作,可以同時新增多個切片 ,他們都是Aspect類的兩個方法,具體實現邏輯就是:通過之前得到的編譯期常量( has_member_Before<T,Args...>::value )判斷 T 是否具有Before或者After方法,分三種情況:

  1. 同時又Before和After:利用中序進行遞迴。

  2. 只有Before:利用前序進行遞迴。

  3. 只有After:利用後序進行遞迴。

為了簡化呼叫過程,繼續封裝如下:

最後記得定義一個終止模板遞迴的最終形態。

```cpp template using identity_t = T;

// AOP的輔助函式,簡化呼叫 template void Invoke(Func &&f, Args &&...args) { Aspect asp(std::forward(f)); asp.Invoke(std::forward(args)..., identity_t()...); } ```

最終如果像最開始講的要拓展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 void Get(T &&head, Tail &&...tails) { ST_ASSERT auto &&p = details::GetMemberFunc(); m_output.push_back(p); Get(tails...); } ```

由於所有的獲取before和after的邏輯在前面獲取類的方法已經講到,所以單個型別直接呼叫 GetMemberFunc 函式即可得出結果,並放入vector中,最後通過模板例項化的遞迴將所有的型別都裝箱。

具體的使用方式也很簡單,如下程式碼:

```cpp

include"reflect_util.hpp"

using func_t = reflect::MemberFunc::func_t; using func_pair_t = reflect::MemberFunc::func_pair_t;

struct LoginAspect { void before(int i) { cout << "Login start " << i << endl; } void after(int i) { cout << "after start " << i << endl; } }; int main(){ vector out; // 獲取before和after方法,並通過function進行包裝 reflect::MemberFunc(out).Get( TimeElapsedAspect{}, LoggingAspect{}, LoginAspect{} ); //將三個型別的before和after方法分離成function後以pair的形式儲存在out中 for(auto&& p : out){ if(p.first){//如果before存在則呼叫 p.first(0); } if(p.second){//如果after存在則呼叫 p.second(1); } } } ```

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

參考連結: C++模板進階指南:SFINAE SFINAE C++11實現一個輕量級的AOP框架