C++編譯期反射實踐——以AOP實現(xiàn)為例
編譯期反射實踐
自古以來,C++就一直缺少一個編程語言的重要特性——反射,但如果熟悉C++元模板編程的同學,就知道以C++的風格,肯定是不會在標準庫中添加運行時的反射支持的,從最新的C++版本演進來看,倒是編譯期反射可能得到更好的支持。C++11 -> C++14 -> C++17 -> C++20… 不斷讓元模板編程變得更簡單,更規(guī)范。
本次的編譯期反射實踐,代碼要求的最低C++版本為14,因為用到了 make_shared、decay_t。
本次實踐的完整代碼倉庫:https://github.com/ACking-you/MyUtil/tree/master/aop
獲取類的方法
判斷類是否具有某方法
我們?nèi)绾闻袛嗄硞€類是否具有某個方法呢?
要想在編譯期間實現(xiàn)這樣一個判斷,我們的思路可以是這樣:寫兩個模板,如果這個類型具有這個方法,就匹配到返回?std::true_type()
?的模板,如果不具備則匹配到返回?std::false_type()
?的模板,最后通過?std::is_same
?能夠判斷匹配結(jié)果,也就是實現(xiàn)了在編譯期間判斷類是否有這個方法。
上述過程,利用?SFINAE?的原理可以輕松實現(xiàn),如果不了解 SFINAE 以及對應(yīng)的 enable_if 的運用,可以看看這篇文章:https://zhuanlan.zhihu.com/p/21314708
我們現(xiàn)在就開始動手實現(xiàn)上述代碼,假設(shè)我們需要判斷一個類型是否有?before()
?方法。
先講下上述代碼定義后如何使用吧,比如現(xiàn)在有個 Student 類型,我們來判斷是否具有 before 成員函數(shù),則只需要寫下下面的代碼:
上面的代碼重點有三段,已經(jīng)作為標記1、2、3:
代碼1處,利用了?std::declval
?在編譯期創(chuàng)建類型U的實例,并調(diào)用其?before
?方法,這是在元模板中判斷一個類型是否具有某個方法的常有手段,因為 SFINAE 的存在,即便該處替換出錯,編譯器會去繼續(xù)尋找下一個替換是否能夠正確,直到所有的替換都出錯。
很明顯這里是一定會替換成功的,因為代碼2有一個包容性很強的重載,這個重載的參數(shù)不能和代碼1處的重載參數(shù)一致,否則會算作重復(fù)定義,當然如果你使用?std::enable_if
?對參數(shù)一致的模板參數(shù)進行唯一性的限制,那么重復(fù)定義的錯誤也可以避免。但是寫成 C 的可變參數(shù)是最快的解決方式。
代碼1處,有個逗號表達式的細節(jié),如果成功被代碼1處替換,那么返回值類型將會是?decltype()
?中的表達式類型,也就是逗號表達式最后的結(jié)果?std::true_type
。
代碼3是利用enum類型在編譯期得到具體的常量值。具體是通過調(diào)用?Check<T>(0)
?獲取該函數(shù)的返回值類型,這期間模板的匹配替換就會牽扯到前面的代碼1、2。所以一旦模板被實例化,那么該class是否具有該方法的信息也就清楚了。
最后我們可以把該段代碼提取為宏作為通用代碼:
如果想要生成判斷是否有before或者其他方法的代碼,則只需要調(diào)用這個宏。
將類方法轉(zhuǎn)為function保存
直接上代碼,再逐一講解:
以下代碼是將該類的before和after方法包裝成一個function,并返回一個pair。完整代碼:https://github.com/ACking-you/MyUtil/tree/master/aop/reflect_util.hpp
在代碼段1中,通過?enable_if
?確保在該類型有before和after方法,同時也可以保證寫其他版本的時候不會出現(xiàn)重復(fù)定義的錯誤。enable_if
?第一個參數(shù)是需要滿足的條件,第二個參數(shù)是enable_if內(nèi)部的type類型,默認為void。
代碼段2中,創(chuàng)建一個T類型的實例,并用shread_ptr管理,原因在于before方法和after方法需要共用內(nèi)存,而這兩個方法都要被提取為單獨的function,要保證內(nèi)存安全,故需要使用智能指針。其中?std::decay_t<T>
?效果等同于?std::decay<T>::type
,作用是消除T類型的const修飾和引用修飾。因為make_shared<>中的模板參數(shù)不能為引用類型。
代碼段3中,利用lamda表達式將fun拷貝一份到其中命名為self,最后返回pair即可。
當前寫的功能是不完整的,需要多幾個模板的重載來實現(xiàn)只有before方法、以及只有after方法的情況。寫法和上述代碼一致,只不過?enable_if
?中的條件稍作改變即可。前面也提到過enable_if千萬不能丟,否則會報重復(fù)定義的錯誤,當然如果你是C++17的版本,可以直接使用?if constexpr
?來實現(xiàn)更為簡潔的代碼而無需單獨寫三個函數(shù)。
如下:
下面我簡單解釋下代碼:
ST_ASSERT宏的作用是,通過static_assert在編譯期拋出提示,T類型必須有before或after兩個方法之一。
通過該類型擁有的情況不同,給出不同的返回值。
很明顯去除了enable_if后,我們代碼清爽了許多。
AOP的實現(xiàn)
關(guān)于AOP,大家可以去搜一搜,這里就不過多贅述。我的簡單理解就是一個事件回調(diào),可以嵌入到業(yè)務(wù)的執(zhí)行前后,把這個事件的概念換成一個切面,把業(yè)務(wù)代碼看作一個橫向坐標軸上的面,那么AOP就是在這個面的前后添加其他切面來實現(xiàn)常用的業(yè)務(wù)復(fù)用。比如用戶的身份驗證,可以在業(yè)務(wù)之前添加身份驗證的切面,比如需要測試該業(yè)務(wù)的性能,那么可以在業(yè)務(wù)切面的前后添加開始計時和終止計時的邏輯。
Invoke調(diào)用實現(xiàn)AOP
根據(jù)上述對AOP的描述,我們要切入的代碼主要是前和后兩個邏輯,故每個要切入的類可以規(guī)定他必須定義Before或者After方法。然后通過可變參模板遞歸實現(xiàn)任意個參數(shù)的切面調(diào)用。
可以把整個切面調(diào)用過程看作一個洋蔥圈層,比如添加s1類型的before和after作為切片,s2類型的before和after作為切片,s3類型的before作為切片。把業(yè)務(wù)代碼邏輯作為foo函數(shù)。
則他們的調(diào)用過程如下:
?s1->before =>?
s2->before =>?
s3->before =>
foo業(yè)務(wù)邏輯 =>?
s1->after =>?
s2->after。
如果稍微學過點數(shù)據(jù)結(jié)構(gòu),這個調(diào)用就能想到前中后序遍歷上去了。
代碼實現(xiàn)如下(C++11需要使用eable_if來實現(xiàn),代碼量很多,所以這里就直接用C++17的?if constexpr
?來實現(xiàn)了):
上述完整代碼:https://github.com/ACking-you/MyUtil/tree/master/aop/aspect.hpp
上述代碼是根據(jù)C++變參模板實現(xiàn)的通用性操作,可以同時添加多個切片 ,他們都是Aspect類的兩個方法,具體實現(xiàn)邏輯就是:通過之前得到的編譯期常量(?has_member_Before<T,Args...>::value
?)判斷 T 是否具有Before或者After方法,分三種情況:
同時又Before和After:利用中序進行遞歸。
只有Before:利用前序進行遞歸。
只有After:利用后序進行遞歸。
為了簡化調(diào)用過程,繼續(xù)封裝如下:
最后記得定義一個終止模板遞歸的最終形態(tài)。
最終如果像最開始講的要拓展s1、s2、s3的方法上去,那么簡單的使用如下代碼即可:
統(tǒng)一轉(zhuǎn)function存儲并實現(xiàn)AOP調(diào)用順序
統(tǒng)一轉(zhuǎn)function存儲
將任意類的before和after方法集體裝箱為function的關(guān)鍵代碼邏輯如下,完整代碼請看:
由于所有的獲取before和after的邏輯在前面獲取類的方法已經(jīng)講到,所以單個類型直接調(diào)用?GetMemberFunc
?函數(shù)即可得出結(jié)果,并放入vector中,最后通過模板實例化的遞歸將所有的類型都裝箱。
具體的使用方式也很簡單,如下代碼:
AOP的調(diào)用順序?qū)崿F(xiàn)
完整測試代碼:https://github.com/ACking-you/MyUtil/tree/master/aop/test_aspect.cc
參考鏈接:
C++模板進階指南:SFINAE:https://zhuanlan.zhihu.com/p/21314708SFINAE:https://en.cppreference.com/w/cpp/language/sfinae
C++11實現(xiàn)一個輕量級的AOP框架:https://www.cnblogs.com/qicosmos/p/4772389.html