编译期反射实践

自古以来,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)...);
    });
  }
}

下面我简略解说下代码:

  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 来完成了):

 /*以下是截取的一个类的两个办法*/
// 递归的止境
  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办法,分三种状况:

  1. 一起又Before和After:运用中序进行递归。

  2. 只要Before:运用前序进行递归。

  3. 只要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框架