5

I have a collection of delegates using std::function that point to functions with different signatures. I want to be able to retrieve those delegates at run time using string keys. I can't seem to use a map because they point to functions with different signatures. Can I have such functionality without a switch statement?

For example, a real world use case is an RPC system. Do they really just make the methods have the same signature or use code generation?

E:useful links related to the selected answer which will take a fair amount of time to grasp

http://en.cppreference.com/w/cpp/utility/functional/function

http://en.cppreference.com/w/cpp/utility/forward

http://en.cppreference.com/w/cpp/utility/integer_sequence

http://en.cppreference.com/w/cpp/types/result_of

http://en.cppreference.com/w/cpp/types/remove_reference

http://www.boost.org/doc/libs/1_57_0/doc/html/any.html

FPGA
  • 3,525
  • 10
  • 44
  • 73
  • 1
    How would you call a function you retrieved from the map? – user253751 Mar 09 '15 at 23:15
  • 1
    C++ is statically typed, you can't call a function whose signature you don't know. You might experiment with `boost::any` but I think your RPC might need one generic signature for all functions. – Kos Mar 09 '15 at 23:16
  • @immibis with a wrapper that has a variadic template, and i also have a command struct that has a template that contains the return type, the instance type and another variadic for method parameters, it works fine – FPGA Mar 09 '15 at 23:23
  • @FPGA how are you going to call the wrapper? – user253751 Mar 09 '15 at 23:25
  • @immibis it has a `RT call(const T& instance,PT params...)` method – FPGA Mar 09 '15 at 23:30
  • 3
    @FPGA but what I meant was: how are you going to call `call`? – user253751 Mar 09 '15 at 23:33
  • @immibis the command has an array of parameters, and my remotely accessable instances inherit from a struct called RemotelyAccessible those are registered in a map, the command also contains the name of the instance, how would i call call? i was imagining something like map["methodame"].call(this,command.params...) from within an instance – FPGA Mar 09 '15 at 23:44
  • 1
    Since you asked: a typical RPC implementation (e.g DCE/RPC or Microsoft DCOM) would have you describe your external interface using a special language (Interface Definition Language, IDL), which is then processed by a special tool (IDL compiler). This tool generates proxy and stub functions - functions with the right signature that handle serializing and deserializing their parameters. Client calls a proxy, which magically calls a stub on the other end, which in turn calls the server's implementation. – Igor Tandetnik Mar 09 '15 at 23:47
  • @IgorTandetnik thats sick! – FPGA Mar 09 '15 at 23:49
  • What type is `command.params`, then? – Igor Tandetnik Mar 09 '15 at 23:49
  • std::array params; Param is `template struct Param{ explicit Param(std::string k,T v):key(k),value(v){} std::string key; const T value; };` – FPGA Mar 09 '15 at 23:50
  • 2
    You can't have `std::array`, what with `Param` being a template. You can only have `std::array>`, for some concrete type `SomeType`. – Igor Tandetnik Mar 09 '15 at 23:57
  • @IgorTandetnik same problem will try to use boost::any or kill myself – FPGA Mar 10 '15 at 00:02
  • Where are you getting the params to do the call from and their types? How are they stored initially? Type tagged binary data? Binary data and deserialization function ids? You need to type erase earlier, not later, I think. You do not need an idl: c++ is powerful enough without it -- but you do need a better understanding of type erasure and serialization. – Yakk - Adam Nevraumont Mar 10 '15 at 00:41
  • @Yakk i have not reached that far yet but i will either use protobuf or json strings, as to my real world c++ experience its non existent so any suggestions are appreciated – FPGA Mar 10 '15 at 00:53
  • 1
    There's [this old question](https://stackoverflow.com/questions/8476975/call-function-with-parameters-extracted-from-string) which basically asks the same thing, with a working answer – Tom Knapen Mar 10 '15 at 01:15
  • @FPGA: If your functions take an array of parameters, then it sounds like they **do** all have the same signature. –  Mar 10 '15 at 10:04
  • I think a lot of this discussion in the comments could have been avoided if you posted a code snippet demonstrating how you would *use* the functionality you're trying to implement. –  Mar 10 '15 at 10:06
  • @Hurkyl code was a bit too long and messy and barely functional to post – FPGA Mar 10 '15 at 10:27

2 Answers2

4

Start with a type bundle:

template<class...Ts>struct types{
  using type=types;
  enum{count = sizeof...(Ts)};
};
template<class T> struct tag{using type=T;};

Now we define all of our supported types in one global types bundle.

The index into that global types bundle is sent over the wire, and used to look up the deserialization code.

There should be a function boost::any read( wire_data, T* unused ) defined in your protocol namespace (for basic types and std types) and in the namespace of T (for other types) that reads wire data into a boost::any. wire_data is just a placeholder for whatever stuff you get off the wire that you turn into the T.

We turn the type index into a call to read via the magic switch technique:

template<size_t n> using index=std::integral_constant<size_t, n>;

template<class types, class T>
struct index_in;
template<class...Ts, class T>
struct index_in<types<T, Ts...>, T>:index<0> {};
template<class T0, class...Ts, class T1>
struct index_in<types<T0, Ts...>, T1>:index<
  index_in<types<Ts...>, T1>::value+1
> {};

gives us the offset of a type T in types<Ts...>. Use that on the sending side to map your type to an index into the list.

On the other side, we have:

template<class types, size_t n>
struct type_at;
template<class types, size_t n>
using type_at_t=typename type_at<types,n>::type;
template<class T0, class...Ts>
struct type_at<types<T0, Ts...>,0>: tag<T0> {};
template<class T0, class...Ts, size_t n>
struct type_at<types<T0, Ts...>,n>:
  type_at<types<Ts...>, n-1>
{};

which takes a types<Ts...> and an index and returns a type.

template<class types>
struct to_any {
  template<size_t n>
  struct worker {
    boost::any operator()( wire_data w )const{
      using protocol_ns::read;
      return read( w, (type_at_t<types,n>*)nullptr );
    }
  };
};

which dispatches to read using ADL.

Now we write our quick magic switch:

namespace details {
  template<template<size_t>class action, class indexes>
  struct magic_switch;
  template<template<size_t>class action, size_t... Is>
  struct magic_switch<action, std::index_sequences<Is...>>
  {
    template<class...Ts, class R=std::result_of_t< action<max>(Ts...) >>
    R operator()(size_t i, Ts&&... ts)const {
      using entry = R(*)(std::remove_reference<Ts>*...);
      entry table[] = {
        [](std::remove_reference<Ts>*...args)->R{
          return action<Is>{}( std::forward<Ts>(*args)... );
        }...
      };
      if (i > sizeof(table)/sizeof(entry))
        throw std::out_of_range("i");
      return table[i]( (&ts)... );
    }
  };
}
template<template<size_t>class action, size_t max>
struct magic_switch:
  details::magic_switch<action,std::make_index_sequence<max>>
{};

Then

magic_switch<
  to_any<all_types_supported>::template worker, 
  all_types_supported::count
>

is the type of a stateless function object that, when passed n and wire_data, will call the appropriate read function for that type and return a boost::any.

Ok, now we are half way there.

The second half involves taking our function of signature Z(Args...), and writing a type eraser that takes a std::vector<boost::any> storing Args... and returns a boost::any storing a Z.

std::function<boost::any(std::vector<boost::any>)> erased_func_t;

template<class... Args, class F>
erased_func_t erase_func(F&& f) {
  // TODO
}

Once we have written that, we can store a map from string to an erased_func_t for our table of functions.

We lookup the erased_func_t. We use the above deserialization infrastructure to generate a std::vector<boost::any> from the passed in parameters. We invoke it, having an exception thrown if it fails.

And bob is your uncle.

If you want to send the answer back, you'll need to type-erase going back to the wire format, and change erased_func_t to return the wire_data required to send it back over the wire instead of a boost::any. That would probably be best.

None of the above code has been tested. Some of it requires C++14 (not that much, mostly _t aliases), and some compilers who claim to support C++11 don't support the magic_switch implementation I wrote (it is almost pure C++11, except the _t aliases, I believe). But an equivalent one can be written, if more verbose.

Finally, like many things, it is usually not a good idea to write a RPC protocol from scratch. Odds are I missed an important step above.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • `std::index_sequences` to `std::index_sequence` – FPGA Mar 10 '15 at 08:39
  • Very nice answer ! In the fifth snippet though, what is `max` ? – Quentin Mar 10 '15 at 10:10
  • 1
    @quen it builds a jump table for efficient switching: `max` is the size of the jump table. The valid `i` values (that get turned into compile-time constants) are the half-open interval `[0,max)`. In the next snippet, we set `max` to the number of types in our pack of types. – Yakk - Adam Nevraumont Mar 10 '15 at 11:09
2

The problem with lookup tables and std::map is that all the function pointers must have the same signature.

However, there is a complex solution around this. It involves function objects (functors) and base classes.

Let us define the base functor class:

struct Base_Parameters;

struct Base_Functor
{
  virtual void execute(Base_Parameter * p_parameters) = 0;
};

This solves the issue of how to declare the std::map:

typedef std::map<std::string, Base_Functor *> Function_Key_Container;

Any function would be derived from the base function structure: struct My_Functor : Base_Functor { void execute(Base_Parameters * p_parameters) { //... } };

You would add the function to the map, something like this:

Function_Key_Container function_map;
function_map["Apple"] = new My_Functor;

The issue now is passing parameters to the function.
I haven't fully worked this one out yet, but here are some suggestions:

  • Use boost::variant -- a variant record.
  • Create a child from Base_Parameters and in the function object, use dynamic_cast to convert the Base_Parameters pointer to a pointer to the child parameters.
Thomas Matthews
  • 56,849
  • 17
  • 98
  • 154
  • I would look for a way to "late bind" the parameters to the function object instances, but other than that, this would have been exactly my suggested approach. – Chad Mar 10 '15 at 01:50