如何用C++ 11和C++ 14实现动态函数调用?

Łuk*_*Lew 7 c++ templates variadic-templates c++11 c++14

这是代码,我希望解释我想要实现的目标.

vector<int> ints;
vector<double> doubles;


struct Arg {
  enum Type {
    Int,
    Double
  };

  Type type;
  int index;
};

template <typename F> 
void Call(const F& f, const vector<Arg>& args) {
  // TODO: 
  //  - First assert that count and types or arguments of <f> agree with <args>.
  //  - Call "f(args)"
}

// Example:

void copy(int a, double& b) {
  b = a;
}

int test() {
  Call(copy, {{Int, 3}, {Double, 2}}); // copy(ints[3], double[2]);
}
Run Code Online (Sandbox Code Playgroud)

这可以在C++ 11中完成吗?
如果是,可以在C++ 14中简化解决方案吗?

Yak*_*ont 10

我分两步完成.

首先,我将包装f一个能够理解Arg类似参数的对象,并在失败时生成错误.为简单起见,假设我们抛出.

这比你Arg在这一层被理解的要简单一点,所以我可以翻译ArgMyArg:

struct MyArg {
  MyArg(MyArg const&)=default;
  MyArg(int* p):i(p){}
  MyArg(double* p):d(p){}
  MyArg(Arg a):MyArg(
    (a.type==Arg::Int)?
    MyArg(&ints.at(a.index)):
    MyArg(&doubles.at(a.index))
  ) {}
  int * i = nullptr;
  double* d = nullptr;
  operator int&(){ if (!i) throw std::invalid_argument(""); return *i; }
  operator double&(){ if (!d) throw std::invalid_argument(""); return *d; }
};
Run Code Online (Sandbox Code Playgroud)

我们映射void(*)(Ts...)std::function<void(MyArg, MyArg, MyArg)>这样:

template<class T0, class T1>using second_type = T1;

template<class...Ts>
std::function<void( second_type<Ts,MyArg>... )> // auto in C++14
my_wrap( void(*f)(Ts...) ) {
  return [f](second_type<Ts,MyArg>...args){
    f(args...);
  };
}
Run Code Online (Sandbox Code Playgroud)

现在剩下的就是计算函数参数计数与向量大小计数,并将其解包std::vector到函数调用中.

最后看起来像:

template<class...Ts, size_t...Is>
void call( std::function<void(Ts...)> f, std::index_sequence<Is...>, std::vector<Arg> const& v ) {
  f( v[Is]... );
}
template<class...Ts>
void call( std::function<void(Ts...)> f, std::vector<Arg> const& v ) {
  call( std::move(f), std::index_sequence_for<Ts...>{}, v );
}
Run Code Online (Sandbox Code Playgroud)

在哪里index_sequenceindex_sequence_for是C++ 14,但是等价物可以在C++ 11中实现(堆栈溢出有很多实现).

所以我们最终得到的结果如下:

template<class...Ts>
void Call( void(*pf)(Ts...), std::vector<Arg> const& v ) {
  if (sizeof...(Ts)>v.size())
    throw std::invalid_argument("");
  auto f = my_wrap(pf);
  call( std::move(f), v );
}
Run Code Online (Sandbox Code Playgroud)

处理投掷是一种练习,处理返回值也是如此.

此代码尚未编译或测试,但设计应该是合理的.它只支持调用函数指针 - 调用通用可调用对象很棘手,因为计算它们需要多少个参数(类型为int或double)是棘手的.如果你传递了他们想要多少个参数作为编译时常量,那很容易.您还可以构建一个魔术开关来处理一些常量(10,20,1000,无论如何),并将向量的运行时长度调度到编译时间常量,该常量会引发参数长度不匹配.

这比较棘手.


硬编码指针很糟糕.

template<class...Ts>struct types{using type=types;};
template<size_t I> using index=std::integral_constant<size_t, I>;
template<class T, class types> struct index_in;
template<class T, class...Ts>
struct index_in<T, types<T,Ts...>>:
  index<0>
{};
template<class T, class T0, class...Ts>
struct index_in<T, types<T0,Ts...>>:
  index<1+index_in<T, types<Ts...>>{}>
{};
Run Code Online (Sandbox Code Playgroud)

是一个类型的包.

以下是我们如何存储缓冲区:

template<class types>
struct buffers;
template<class...Ts>
struct buffers<types<Ts...>> {
  struct raw_view {
    void* start = 0;
    size_t length = 0;
  };
  template<class T>
  struct view {
    T* start = 0;
    T* finish = 0;
    view(T* s, T* f):start(s), finish(f) {}
    size_t size() const { return finish-start; }
    T& operator[](size_t i)const{
      if (i > size()) throw std::invalid_argument("");
      return start[i];
    }
  }
  std::array< raw_view, sizeof...(Ts) > views;
  template<size_t I>
  using T = std::tuple_element_t< std::tuple<Ts...>, I >;
  template<class T>
  using I = index_of<T, types<Ts...> >;

  template<size_t I>
  view<T<I>> get_view() const {
    raw_view raw = views[I];
    if (raw.length==0) { return {0,0}; }
    return { static_cast<T<I>*>(raw.start), raw.length/sizeof(T) };
  }
  template<class T>
  view<T> get_view() const {
    return get_view< I<T>{} >();
  }
  template<class T>
  void set_view( view<T> v ) {
    raw_view raw{ v.start, v.finish-v.start };
    buffers[ I<T>{} ] = raw;
  }
};
Run Code Online (Sandbox Code Playgroud)

现在我们修改Call:

template<class R, class...Args, size_t...Is, class types>
R internal_call( R(*f)(Args...), std::vector<size_t> const& indexes, buffers<types> const& views, std::index_sequence<Is...> ) {
  if (sizeof...(Args) != indexes.size()) throw std::invalid_argument("");
  return f( views.get_view<Args>()[indexes[Is]]... );
}
template<class R, class...Args, size_t...Is, class types>
R Call( R(*f)(Args...), std::vector<size_t> const& indexes, buffers<types> const& views ) {
  return internal_call( f, indexes, views, std::index_sequence_for<Args...>{} );
}
Run Code Online (Sandbox Code Playgroud)

这是C++ 14,但大多数组件都可以转换为C++ 11.

这使用O(1)数组查找,没有映射.您负责填充buffers<types>缓冲区,如下所示:

buffers<types<double, int>> bufs;
std::vector<double> d = {1.0, 3.14};
std::vector<int> i = {1,2,3};
bufs.set_view<int>( { i.data(), i.data()+i.size() } );
bufs.set_view<double>( { d.data(), d.data()+d.size() } );
Run Code Online (Sandbox Code Playgroud)

参数不匹配计数和索引超出范围会产生抛出的错误.它只适用于原始函数指针 - 使其适用于具有固定(非模板)签名的任何内容都很容易(如a std::function).

使其与没有签名的对象一起工作更难.基本上,您不是依赖于为参数调用的函数,而是构建types<Ts...>最多为某个固定大小的叉积.您构建一个(大)表,其中哪些是对传入的调用目标的有效调用(在编译时),然后在运行时遍历该表并确定传入的参数是否有效以调用该对象.

它变得凌乱.

这就是为什么我的上面的版本只是要求索引,并从被调用的对象中推断出类型.