编译期反射实践
自古以来,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()
办法。
template <typename T, typename... Args>
struct has_member_before {
private:
template <typename U>
static auto Check(int)
-> decltype(
std::declval<U>().before(std::declval<Args>()...),std::true_type() //1
);
template <typename U> static std::false_type Check(...); //2
public:
enum {
value = std::is_same<decltype(Check<T>(0)), std::true_type>::value //3
};
};
先讲下上述代码界说后怎么运用吧,比如现在有个 Student 类型,咱们来判别是否具有 before 成员函数,则只需求写下下面的代码:
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是否具有该办法的信息也就清楚了。
终究咱们能够把该段代码提取为宏作为通用代码:
#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
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
来完成更为简练的代码而无需独自写三个函数。
如下:
#define ST_ASSERT \
static_assert( \
has_member_before<T, Args...>::value || \
has_member_after<T, Args...>::value, \
"class need T::before(args...) or T::after(args...) member function!");
template <typename T, typename... Args>
std::pair<std::function<void(Args...)>, std::function<void(Args...)>>
GetMemberFunc() {
ST_ASSERT // 确保至少before after有其一
auto fun = std::make_shared<std::decay_t<T>>();
if constexpr (has_member_before<T, Args...>::value &&
has_member_after<T, Args...>::value) { // 有before和after
return std::make_pair(
[self = fun](Args &&...args) {
self->before(std::forward<Args>(args)...);
},
[self = fun](Args &&...args) {
self->after(std::forward<Args>(args)...);
});
} else if constexpr (has_member_before<T, Args...>::value &&
!has_member_after<T, Args...>::value) { // 有before
return std::make_pair(
[self = fun](Args &&...args) {
self->before(std::forward<Args>(args)...);
},
nullptr);
} else { // 只要after
return std::make_pair(nullptr, [self = fun](Args &&...args) {
self->after(std::forward<Args>(args)...);
});
}
}
下面我简略解说下代码:
- 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
来完成了):
/*以下是截取的一个类的两个办法*/
// 递归的止境
template <typename T> void Invoke(Args &&...args, T &&aspect) {
ST_ASSERT
if constexpr (has_member_Before<T, Args...>::value &&
has_member_After<T, Args...>::value) {
aspect.Before(std::forward<Args>(args)...); // 中心逻辑之前的切面逻辑
m_func(std::forward<Args>(args)...); // 中心逻辑
aspect.After(std::forward<Args>(args)...); // 中心逻辑之后的切面逻辑
} else if constexpr (has_member_Before<T, Args...>::value &&
!has_member_After<T, Args...>::value) {
aspect.Before(std::forward<Args>(args)...); // 中心逻辑之前的切面逻辑
m_func(std::forward<Args>(args)...); // 中心逻辑
} else {
m_func(std::forward<Args>(args)...); // 中心逻辑
aspect.After(std::forward<Args>(args)...); // 中心逻辑之后的切面逻辑
}
}
// 变参模板递归
template <typename T, typename... Tail>
void Invoke(Args &&...args, T &&headAspect, Tail &&...tailAspect) {
ST_ASSERT
if constexpr (has_member_Before<T, Args...>::value &&
has_member_After<T, Args...>::value) {
headAspect.Before(std::forward<Args>(args)...);
Invoke(std::forward<Args>(args)..., std::forward<Tail>(tailAspect)...);
headAspect.After(std::forward<Args>(args)...);
} else if constexpr (has_member_Before<T, Args...>::value &&
!has_member_After<T, Args...>::value) {
headAspect.Before(std::forward<Args>(args)...);
Invoke(std::forward<Args>(args)..., std::forward<Tail>(tailAspect)...);
} else {
Invoke(std::forward<Args>(args)..., std::forward<Tail>(tailAspect)...);
headAspect.After(std::forward<Args>(args)...); // 中心逻辑之后的切面逻辑
}
}
上述完好代码:aspect.hpp
上述代码是依据C++变参模板完成的通用性操作,能够一起增加多个切片 ,他们都是Aspect类的两个办法,详细完成逻辑便是:经过之前得到的编译期常量( has_member_Before<T,Args...>::value
)判别 T 是否具有Before或许After办法,分三种状况:
-
一起又Before和After:运用中序进行递归。
-
只要Before:运用前序进行递归。
-
只要After:运用后序进行递归。
为了简化调用进程,持续封装如下:
终究记得界说一个终止模板递归的终究形态。
template <typename T> using identity_t = T;
// AOP的辅佐函数,简化调用
template <typename... AP, typename... Args, typename Func>
void Invoke(Func &&f, Args &&...args) {
Aspect<Func, Args...> asp(std::forward<Func>(f));
asp.Invoke(std::forward<Args>(args)..., identity_t<AP>()...);
}
终究假如像最开端讲的要拓展s1、s2、s3的办法上去,那么简略的运用如下代码即可:
Invoke<s1,s2,s3>(&foo,args); //s1,s2,s3为拓展逻辑,foo为事务逻辑
一致转function存储并完成AOP调用次序
一致转function存储
将任意类的before和after办法团体装箱为function的关键代码逻辑如下,完好代码请看:
void Get() {} //空的func,用于结束模板的递归实例化
template <typename T, typename... Tail> void Get(T &&head, Tail &&...tails) {
ST_ASSERT
auto &&p = details::GetMemberFunc<T, Args...>();
m_output.push_back(p);
Get(tails...);
}
由于一切的获取before和after的逻辑在前面获取类的办法现已讲到,所以单个类型直接调用 GetMemberFunc
函数即可得出成果,并放入vector中,终究经过模板实例化的递归将一切的类型都装箱。
详细的运用方法也很简略,如下代码:
#include"reflect_util.hpp"
using func_t = reflect::MemberFunc<int>::func_t;
using func_pair_t = reflect::MemberFunc<int>::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<func_pair_t> out;
// 获取before和after办法,并经过function进行包装
reflect::MemberFunc<int>(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的调用次序完成
// 依据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框架