【问题标题】:How can implement dynamic function call with C++11 and C++14?如何用 C++11 和 C++14 实现动态函数调用?
【发布时间】:2015-08-11 03:46: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]);
}

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

【问题讨论】:

  • 伪代码表明您想要某种“变体”类型。为此,请查找 Boost 变体和其他变体。标题表明您想要脚本语言的接口之类的东西。为此,请查找例如Boost 绑定到 Python,尤其是 Google 绑定到他们的 Javascript 引擎。除了这些指示(不一定是不相容的,而是不同的)之外,您还不清楚您在问什么。也许你可以勾勒出更高层次的目标。
  • 既然你使用的是 const 向量并添加类型注释,那么元组不是更合适吗?
  • 使用这些向量作为输入是预期的用例吗?我猜你不想把自己限制在这两种类型?答案是肯定的,但是会涉及大量的写作。
  • 这不是动态调用。
  • 等等,你想支持2种以上的类型吗?如果不通过某种枚举,您希望如何定义类型和向量之间的联系?

标签: c++ templates c++11 c++14 variadic-templates


【解决方案1】:

我会分两步完成。

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

这比你的Arg在这一层理解要简单一些,所以我可能会将Arg翻译成MyArg:

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; }
};

我们将void(*)(Ts...) 映射到std::function&lt;void(MyArg, MyArg, MyArg)&gt;,如下所示:

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...);
  };
}

现在剩下的就是计算函数参数计数与向量大小计数,并将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 );
}

其中index_sequence 和index_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 );
}

处理 throws 作为练习,处理返回值也是如此。

此代码尚未编译或测试,但设计应该是合理的。它只支持调用函数指针——调用通用的可调用对象很棘手,因为计算它们想要多少个参数(类型为 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...>>{}>
{};

是一个类型的包。

这是我们如何存储缓冲区:

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;
  }
};

现在我们修改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...>{} );
}

这是C++14,但大多数组件都可以翻译成C++11。

这使用 O(1) 数组查找,没有映射。你负责用缓冲区填充buffers&lt;types&gt;,有点像这样:

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() } );

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

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

它变得一团糟。

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

【讨论】:

  • 你能解释(或指出解释)对 second_type 的需求吗?看起来 second_type 应该只是与 MyArg 相同的类型,所以我一定遗漏了一些微妙的东西。
  • @Speed8ump 我正在扩展second_type&lt;Ts,MyArg&gt;...——想象如果Ts...是{int, double, char},那么second_type&lt;Ts,MyArg&gt;...是{second_type&lt;int,MyArg&gt;, second_type&lt;double,MyArg&gt;, second_type&lt;char,MyArg&gt;},也就是{MyArg, MyArg, MyArg}——相等数量的@ 987654349@ 为sizeof...(Ts)。 ({} 仅作说明)
【解决方案2】:

我有一个部分解决方案,使用 C++11 语法。

首先我创建一个函数重载器,接受仲裁器类型的参数

template< typename Function >
struct overloader : Function
{
    overloader( Function const& func ) : Function{ func } {}
    void operator()(...) const {}
};

template< typename Function >
overloader<Function> make_overloader( Function const& func )
{
    return overloader<Function>{ func };
}

那么,使用重载器来欺骗编译器相信下面的代码(在 switch-case 块中)是合法的:

template <typename F>
void Call( F const& f, const vector<Arg>& args )
{
    struct converter
    {
        Arg const& arg;
        operator double&() const
        {
            assert( arg.type == Double );
            return doubles[arg.index];
        }
        operator int() const
        {
            assert( arg.type == Int );
            return ints[arg.index];
        }
        converter( Arg const& arg_ ): arg( arg_ ) {}
    };
    auto function_overloader = make_overloader( f );
    unsigned long const arg_length = args.size();
    switch (arg_length)
    {
        case 0 :
            function_overloader();
            break;
        case 1 :
            function_overloader( converter{args[0]} );
            break;
        case 2 :
            function_overloader( converter{args[0]}, converter{args[1]} );
            break;
        case 3 :
            function_overloader( converter{args[0]}, converter{args[1]}, converter{args[2]} );
            break;
        /*
        case 4 :
        .
        .
        .
        case 127 :
        */
    }
}

并以这种方式进行测试:

void test_1()
{
    Call( []( int a, double& b ){ b = a; }, vector<Arg>{ Arg{Int, 3}, Arg{Double, 2} } );
}

void test_2()
{
    Call( []( double& b ){ b = 3.14; }, vector<Arg>{ Arg{Double, 0} } );
}

void my_copy( int a, double& b, double& c )
{
    b = a;
    c = a+a;
}

void test_3()
{
    //Call( my_copy, vector<Arg>{ Arg{Int, 4}, Arg{Double, 3}, Arg{Double, 1} } ); // -- this one does not work
    Call( []( int a, double& b, double& c ){ my_copy(a, b, c); }, vector<Arg>{ Arg{Int, 4}, Arg{Double, 3}, Arg{Double, 1} } );
}

这个解决方案的问题是:

  1. g++5.2接受,clang++6.1不接受
  2. 当函数 Call 的参数不合法时,它保持沉默
  3. 函数 Call 的第一个参数不能是 C 风格的函数,必须将其包装到 lambda 对象中才能使其工作。

代码可在此处获得 - http://melpon.org/wandbox/permlink/CHZxVfLM92h1LACf - 供您使用。

【讨论】:

    【解决方案3】:

    首先,您需要一些机制来注册您的参数值,这些值稍后会被某种类型和索引引用:

    class argument_registry
    {
    public:
        // register a range of arguments of type T
        template <class T, class Iterator>
        void register_range(Iterator begin, Iterator end)
        {
            // enclose the range in a argument_range object and put it in our map
            m_registry.emplace(typeid(T), std::make_unique<argument_range<T, Iterator>>(begin, end));
        }
    
        template <class T>
        const T& get_argument(size_t idx) const
        {
            // check if we have a registered range for this type
            auto itr = m_registry.find(typeid(T));
            if (itr == m_registry.end())
            {
                throw std::invalid_argument("no arguments registered for this type");
            }
    
            // we are certain about the type, so downcast the argument_range object and query the argument
            auto range = static_cast<const argument_range_base1<T>*>(itr->second.get());
            return range->get(idx);
        }
    
    private:
        // base class so we can delete the range objects properly
        struct argument_range_base0
        {
            virtual ~argument_range_base0(){};
        };
    
        // interface for querying arguments
        template <class T>
        struct argument_range_base1 : argument_range_base0
        {
            virtual const T& get(size_t idx) const = 0;
        };
    
        // implements get by querying a registered range of arguments
        template <class T, class Iterator>
        struct argument_range : argument_range_base1<T>
        {
            argument_range(Iterator begin, Iterator end)
                : m_begin{ begin }, m_count{ size_t(std::distance(begin, end)) } {}
    
            const T& get(size_t idx) const override
            {
                if (idx >= m_count)
                    throw std::invalid_argument("argument index out of bounds");
    
                auto it = m_begin;
                std::advance(it, idx);
                return *it;
            }
    
            Iterator m_begin;
            size_t m_count;
        };
    
        std::map<std::type_index, std::unique_ptr<argument_range_base0>> m_registry;
    };
    

    比我们定义一个小类型来组合一个类型和一个数字索引来引用参数:

    typedef std::pair<std::type_index, size_t> argument_index;
    
    // helper function for creating an argument_index
    template <class T>
    argument_index arg(size_t idx)
    {
        return{ typeid(T), idx };
    }
    

    最后,我们需要一些模板递归来遍历函数的所有预期参数,检查用户是否传递了匹配类型的参数并从注册表中查询:

    // helper trait for call function; called when there are unhandled arguments left
    template <bool Done>
    struct call_helper
    {
        template <class FuncRet, class ArgTuple, size_t N, class F, class... ExpandedArgs>
        static FuncRet call(F func, const argument_registry& registry, const std::vector<argument_index>& args, ExpandedArgs&&... expanded_args)
        {
            // check if there are any arguments left in the passed vector
            if (N == args.size())
            {
                throw std::invalid_argument("not enough arguments");
            }
    
            // get the type of the Nth argument
            typedef typename std::tuple_element<N, ArgTuple>::type arg_type;
    
            // check if the type matches the argument_index from our vector
            if (std::type_index{ typeid(arg_type) } != args[N].first)
            {
                throw std::invalid_argument("argument of wrong type");
            }
    
            // query the argument from the registry
            auto& arg = registry.get_argument<arg_type>(args[N].second);
    
            // add the argument to the ExpandedArgs pack and continue the recursion with the next argument N + 1
            return call_helper<std::tuple_size<ArgTuple>::value == N + 1>::template call<FuncRet, ArgTuple, N + 1>(func, registry, args, std::forward<ExpandedArgs>(expanded_args)..., arg);
        }
    };
    
    // helper trait for call function; called when there are no arguments left
    template <>
    struct call_helper<true>
    {
        template <class FuncRet, class ArgTuple, size_t N, class F, class... ExpandedArgs>
        static FuncRet call(F func, const argument_registry&, const std::vector<argument_index>& args, ExpandedArgs&&... expanded_args)
        {
            if (N != args.size())
            {
                // unexpected arguments in the vector
                throw std::invalid_argument("too many arguments");
            }
    
            // call the function with all the expanded arguments
            return func(std::forward<ExpandedArgs>(expanded_args)...);
        }
    };
    
    // call function can only work on "real", plain functions
    // as you could never do dynamic overload resolution in C++
    template <class Ret, class... Args>
    Ret call(Ret(*func)(Args...), const argument_registry& registry, const std::vector<argument_index>& args)
    {
        // put the argument types into a tuple for easier handling
        typedef std::tuple<Args...> arg_tuple;
    
        // start the call_helper recursion
        return call_helper<sizeof...(Args) == 0>::template call<Ret, arg_tuple, 0>(func, registry, args);
    }
    

    现在你可以像这样使用它了:

    int foo(int i, const double& d, const char* str)
    {
        printf("called foo with %d, %f, %s", i, d, str);
        // return something
        return 0;
    }
    
    int main()
    {
        // prepare some arguments
        std::vector<int> ints = { 1, 2, 3 };
        std::vector<double> doubles = { 10., 20., 30. };
        std::vector<const char*> str = { "alpha", "bravo", "charlie" };
    
        // register them
        argument_registry registry;
        registry.register_range<int>(ints.begin(), ints.end());
        registry.register_range<double>(doubles.begin(), doubles.end());
        registry.register_range<const char*>(str.begin(), str.end());
    
        // call function foo with arguments from the registry
        return call(foo, registry, {arg<int>(2), arg<double>(0), arg<const char*>(1)});
    }
    

    现场示例:http://coliru.stacked-crooked.com/a/7350319f88d86c53

    这种设计应该对任何参数类型开放,而无需在某处列出所有支持的类型。

    正如代码 cmets 中所述,一般情况下,您不能像这样调用任何可调用对象,因为在 C++ 中永远无法在运行时完成重载解析。

    【讨论】:

    • OP 的示例通过非编译时间常数参数获取传递给call 的参数。你的通过 compile-time-constant 参数(即类型)来获取它。碰巧的是,实际上可以处理具有固定数量参数的任何可调用对象,这是不切实际的。
    • @Yakk Writing arg&lt;int&gt;(2) 与 {Int, 2} 一样的编译时间常数。两个表达式的结果类型都是存储类型信息的“抽象”类型,但该表达式本身的结果类型不依赖于参数类型,因此就像基于枚举的解决方案一样易于处理。但是使用真实类型作为参数来创建类型信息而不是编造一些标识符,这样您就不必在某处定义所有受支持类型的综合列表。
    • @Yakk 通常也无法处理任何可调用对象。想想带有模板或重载operator() 的函子。在运行时专门化/找到正确的重载是不可能的。当然,您可以添加对一些特殊情况的支持,例如 std::function-like 对象,但正如您所说,它会变得混乱,永远不会成为通用解决方案。
    • 您在编译时就知道可调用对象的类型。您知道可以在编译时调用它的一组可能参数。如果您可以处理固定的最大参数数量,则可以构建一个运行时表,该表将(给定这组参数)映射到(调用它,或抛出无效参数异常)(作为 n 维表或一系列条件)在编译时。然后在此表中查找解决了您说不可能的问题。 this 变得混乱:(几乎)一般地解决问题(使用上限 arg-count)。
    【解决方案4】:

    您没有按照我的要求澄清问题,而是提出了赏金。除非这确实是问题所在,即没有用例的家庭作业,只是让你练习一般的基本编程,除了那只有纯粹的运气才能给你一个真正问题的答案:人必须猜测要解决的问题是什么。这就是为什么没有人打扰,即使有赏金,也没有人为你提出的“何时纠正明显错误”这个极其微不足道的问题提出解决方案,即如何做到这一点:

    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]);
    }
    

    在 C++11 及更高版本中,一种非常直接的方式是:

    #include <assert.h>
    #include <vector>
    using std::vector;
    
    namespace g {
        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)
    {
        // Was TODO: 
        //  - First assert that count and types or arguments of <f> agree with <args>.
        assert( args.size() == 2 );
        assert( args[0].type == Arg::Int );
        assert( int( g::ints.size() ) > args[0].index );
        assert( args[1].type == Arg::Double );
        assert( int( g::doubles.size() ) > args[1].index );
    
        //  - Call "f(args)"
        f( g::ints[args[0].index], g::doubles[args[1].index] );
    }
    
    // Example:
    
    void copy(int a, double& b)
    {
        b = a;
    }
    
    auto test()
    {
        Call(copy, {{Arg::Int, 3}, {Arg::Double, 2}}); // copy(ints[3], double[2]);
    }
    
    namespace h {}
    
    auto main()
        -> int
    {
        g::ints = {000, 100, 200, 300};
        g::doubles = {1.62, 2.72, 3.14};
        test();
        assert( g::doubles[2] == 300 );
    }
    

    C++14 中没有特别相关的新特性。

    【讨论】:

      【解决方案5】:

      我在对您的问题发表评论后提出这个答案。看到需求,你说:

      最好不要要求我们创建一个结构 枚举我们想要支持的所有类型。

      这可能表明您希望摆脱 Arg 结构中的 type 枚举器。然后,只剩下值:那为什么不直接使用纯 C++ 类型,而不是包装它们呢?

      假设您在编译时知道所有参数类型
      (这个假设可能是非常错误的,但我没有看到您的问题中有任何要求阻止它。如果您提供更多详细信息,我很乐意重写我的答案)。

      C++11 可变参数模板解决方案

      现在解决方案,使用 C++11 可变参数模板和完美转发。在文件Call.h中:

      template <class F, class... T_Args>
      void Call(F f, T_Args &&... args)
      {
          f(std::forward<T_Args>(args)...);
      }
      

      解决方案属性

      这种方法似乎可以满足您的所有明确要求:

      • 适用于 C++11 标准
      • 使用args 检查f agress 的计数和类型或参数。
        • 它实际上是在编译时尽早完成的,而不是可能的运行时方法。
      • 无需手动枚举接受的类型(实际上适用于任何 C++ 类型,无论是原生的还是用户定义的)

      不符合您的要求,但很高兴拥有:

      • 非常紧凑,因为它利用了 C++11 中引入的原生特性。
      • 接受任意数量的参数
      • 参数的类型和对应的f 参数的类型不必完全匹配,但必须兼容(完全,就像普通的 C++ 函数调用一样)。

      示例用法

      您可以在一个简单的 main.cpp 文件中对其进行测试:

      #include "Call.h"
      #include <iostream>
      
      void copy(int a, double& b)
      {
        b = a;
      }
      
      void main()
      {
          int a = 5;
          double b = 6.2;
      
          std::cout << "b before: " << b << std::endl;
          Call(copy, a, b);
          std::cout << "b now: " << b << std::endl;
      }
      

      哪个会打印:

      b before: 6.2
      b now: 5
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2017-08-02
        • 1970-01-01
        • 1970-01-01
        • 2014-06-29
        • 2018-10-29
        • 1970-01-01
        • 1970-01-01
        • 2021-01-29
        相关资源
        最近更新 更多