Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can implement dynamic function call with C++11 and C++14?

Here is code that I hope explains what I want to achieve.

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

Can this be done in C++11 ?
If yes, can the solution be simplified in C++14 ?

like image 524
Łukasz Lew Avatar asked Aug 11 '15 03:08

Łukasz Lew


3 Answers

I'd do this in two steps.

First, I'd wrap f in an object able to understand Arg-like parameters, and generate errors on failure. For simplicity, suppose we throw.

This is a bit simpler than your Arg to be understood at this layer, so I might translate Arg into 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; }
};

We map void(*)(Ts...) to std::function<void(MyArg, MyArg, MyArg)> like this:

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

now all that is left is counting function parameter count vs vector size count, and unpacking the std::vector into a function call.

The last looks like:

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

where index_sequence and index_sequence_for are C++14, but equivalents can be implemented in C++11 (there are many implementations on stack overflow).

So we end up with something like:

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

Dealing with the throws is left as an exercise, as is handling return values.

This code has not been compiled or tested, but the design should be sound. It only supports calling function pointers -- calling generalized callable objects is tricky, because counting how many arguments they want (of type int or double) is tricky. If you passed in how many arguments they want as a compile-time constant, it is easy. You could also build a magic switch that handles counts up to some constant (10, 20, 1000, whatever), and dispatch the runtime length of the vector into a compile time constant that throws on a argument length mismatch.

This is trickier.


The hard coded pointers sort of suck.

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

is a package of types.

Here is how we can store buffers:

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

now we modify 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...>{} );
}

which is C++14, but most components can be translated to C++11.

This uses O(1) array lookups, no maps. You are responsible for populating buffers<types> with the buffers, sort of like this:

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

parameter mismatch counts and index out of range generate thrown errors. It only works with raw function pointers -- making it work with anything with a fixed (non-template) signature is easy (like a std::function).

Making it work with an object with no signature is harder. Basically instead of relying on the function called for the arguments, you instead build the cross product of the types<Ts...> up to some fixed size. You build a (large) table of which of these are valid calls to the passed in call target (at compile time), then at run time walk that table and determine if the arguments passed in are valid to call the object with.

It gets messy.

This is why my above version simply asks for indexes, and deduces the types from the object being called.

like image 145
Yakk - Adam Nevraumont Avatar answered Oct 23 '22 08:10

Yakk - Adam Nevraumont


I have a partial solution, using C++11 grammar.

First I make a function overloader accepting arbitrator kinds of arguments

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

then, using the overloader to deceive the compiler into believing the following code ( in switch-case block )is legal:

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 :
        */
    }
}

and test it this way:

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

the problems with this solution is:

  1. g++5.2 accept it, clang++6.1 doesn's
  2. when the argument(s) of function Call is/are not legal, it remains silent
  3. the first argument of function Call cannot be a C-style function, one must wrap that into a lambda object to make it work.

the code is available here - http://melpon.org/wandbox/permlink/CHZxVfLM92h1LACf -- for you to play with.

like image 28
Feng Wang Avatar answered Oct 23 '22 07:10

Feng Wang


First of all, you need some mechanism to register your argument values that are later referenced by some type and an index:

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

Than we define a small type to combine a type and a numerical index for referencing arguments:

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

Finally, we need some template recursion to go through all expected arguments of a function, check if the user passed an argument of matching type and query it from the registry:

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

Now you can use it like this:

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

Live example: http://coliru.stacked-crooked.com/a/7350319f88d86c53

This design should be open for any argument type without the need to list all the supported types somewhere.

As noted in the code comments, you cannot call any callable object like this in general, because overload resolution could never be done at runtime in C++.

like image 1
Horstling Avatar answered Oct 23 '22 06:10

Horstling