24

I am trying to create a map with string as key and a generic method as value in C++, but I do not know if that is even possible. I would like to do something like that:

void foo(int x, int y)
{
   //do something
}

void bar(std::string x, int y, int z)
{
   //do something
} 

void main()
{
   std::map<std::string, "Any Method"> map;

   map["foo"] = &foo;      //store the methods in the map
   map["bar"] = &bar;

   map["foo"](1, 2);       //call them with parameters I get at runtime
   map["bar"]("Hello", 1, 2);
}

Is that possible? If yes, how can I realise this?

Davide Spataro
  • 7,319
  • 1
  • 24
  • 36

3 Answers3

27

You can type-erase the function types into a container, then provide a template operator(). This will throw std::bad_any_cast if you get it wrong.

N.B. because of the type erasure, you will have to specify exactly matching arguments at the call site, as e.g. std::function<void(std::string)> is distinct from std::function<void(const char *)>, even though both can be called with a value like "Hello".

#include <any>
#include <functional>
#include <map>
#include <string>
#include <iostream>

template<typename Ret>
struct AnyCallable
{
    AnyCallable() {}
    template<typename F>
    AnyCallable(F&& fun) : AnyCallable(std::function(std::forward<F>(fun))) {}
    template<typename ... Args>
    AnyCallable(std::function<Ret(Args...)> fun) : m_any(fun) {}
    template<typename ... Args>
    Ret operator()(Args&& ... args) 
    { 
        return std::invoke(std::any_cast<std::function<Ret(Args...)>>(m_any), std::forward<Args>(args)...); 
    }
    std::any m_any;
};

void foo(int x, int y)
{
    std::cout << "foo" << x << y << std::endl;
}

void bar(std::string x, int y, int z)
{
    std::cout << "bar" << x << y << z << std::endl;
} 

using namespace std::literals;

int main()
{
    std::map<std::string, AnyCallable<void>> map;
    
    map["foo"] = &foo;      //store the methods in the map
    map["bar"] = &bar;
    
    map["foo"](1, 2);       //call them with parameters I get at runtime
    map["bar"]("Hello, std::string literal"s, 1, 2);
    try {
        map["bar"]("Hello, const char *literal", 1, 2); // bad_any_cast
    } catch (std::bad_any_cast&) {
        std::cout << "mismatched argument types" << std::endl;
    }
    map["bar"].operator()<std::string, int, int>("Hello, const char *literal", 1, 2); // explicit template parameters
    
    return 0;
}
Caleth
  • 52,200
  • 2
  • 44
  • 75
  • 1
    @buttonsrtoys iirc VS2017 is not fully compliant, it probably lacks the deduction guide for `std::function` – Caleth May 24 '18 at 20:38
  • Changing `map["foo"](1, 2)` to `int n = 1; map["foo"](n, 2);` fails at runtime with a `bad_any_cast`. The call is translating into `std::function` while the `any` has a `std::function`. This make the map very tricky to use -- any ideas how to mitigate this issue? (I am using c++2a mode on gcc-mp-8 (MacPorts gcc8 8.2.0_0) 8.2.0 -- not sure if that is a factor or not). – RandomBits Sep 27 '18 at 22:52
  • @RandomBits no, this technique is very fragile. You can mitigate it by explicitly naming the declaration type, e.g. `map["foo"].operator()(n, m);`, but that's ugly and still easy to get wrong – Caleth Sep 27 '18 at 23:00
  • @Caleth this gives me an error on the line where `AnyCallable(std::function(fun)) {}` is. It says "std::function": use of class template requires template argument list. – Josefhu15 Feb 03 '20 at 17:58
  • @Caleth: What about generifying the return value as well instead of hardcoding it to `void`? Also when using variables only writing `int i = 1; map["foo"].operator()((int) i, 2);` seems to work. The last cast is necessary or it won't compile. This solution is still highly unsafe and does not behave like a "normal" function call would. – BullyWiiPlaza Dec 28 '20 at 13:06
  • @BullyWiiPlaza It turns out the specialisation for `void` isn't necessary. – Caleth Nov 17 '21 at 13:40
  • What if the return type is also different? – Avrdan Nov 23 '21 at 18:46
  • @Avrdan `AnyCallable` and cast the result (or `std::variant` for a known finite set of return types) – Caleth Nov 23 '21 at 19:12
6

The most (I cannot say best here) you can do is to use a signature erasure. That mean to convert the pointer to functions to a common signature type, and then convert them back to the correct signature before using them.

That can only be done in very special use cases (I cannot imagine a real world one) and will be highly unsecure: nothing prevent you to pass the wrong parameters to a function. In short: NEVER DO THIS IN REAL WORLD CODE.

That being said, here is a working example:

#include <iostream>
#include <string>
#include <map>

typedef void (*voidfunc)();

void foo(int x, int y)
{
    std::cout << "foo " << x << " " << y << std::endl;
}

void bar(std::string x, int y, int z)
{
    std::cout << "bar " << x << " " << y << " " << z << std::endl;
}

int main()
{
    std::map<std::string, voidfunc> m;
    m["foo"] = (voidfunc) &foo;
    m["bar"] = (voidfunc)& bar;
    ((void(*)(int, int)) m["foo"])(1, 2);
    ((void(*)(std::string, int, int)) m["bar"])("baz", 1, 2);
    return 0;
}

It gives as expected:

foo 1 2
bar baz 1 2

I could not find in standard whether this invokes or not Undefined Behaviour because little is said about function pointer conversions, but I am pretty sure that all common compilers accept that, because it only involve function pointers casting.

Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • You could always make a std::any of std::functions... I wouldn't be surprised that this has the same potential UB problems, but at least gives the semblance of definedness by using standard library constructions – André Aug 16 '17 at 14:46
3

You cannot store functions with different signatures in a container like map, no matter if you store them as a function pointer or std ::function<WHATEVER>. The information about the signature of the function is one and only one in both cases.

The types for the value in map is one, meaning that the object stored in it are all of the same type.

So if your functions have all the same signature, then it's easy, otherwise, you have to abandon type safety and start walking in a very dangerous realm. The one in which you erase the type information about the functions stored inside the map. This translates to something like map<string, void*>.

Davide Spataro
  • 7,319
  • 1
  • 24
  • 36
  • When saving a function pointer as `void*` you have to cast it back to the actual signature before it can be called, at least with my tests!? So I don't think it makes much sense to use a map to store function pointers with different signatures then. Better use classes with virtual functions or overloaded methods. – xander Aug 16 '17 at 14:19
  • @xander yes you have to cast it back in order to use it. Using classes allows to obtain the same result, but you're not really storing functions. That is why it is not included in my answer. Good point though. – Davide Spataro Aug 16 '17 at 14:25