1

I have implemented a class called MethodMap that allows me to store member function pointers of a class and call them at runtime using a key string. The member function can take any parameters or not at all. The class looks like this:

template <typename T, typename... Args>
class MethodMap {
private:
    std::unordered_map<std::string, std::function<void(T*, Args...)>> method_map;
public:
    void Insert(const std::string& key, void (T::* method)(Args...)) {
        method_map[key] = [method](T* obj, Args... args) { (obj->*method)(args...); };
    }

    void Call(const std::string& key, T* instance, Args&&... methodArgs) const {
        auto it = method_map.find(key);
        if (it != method_map.end()) {
            auto& func = it->second;
            // use tuple to store and forward the arguments
            std::tuple<Args...> arg_tuple(std::forward<Args>(methodArgs)...);
            std::apply(func, std::tuple_cat(std::make_tuple(instance), arg_tuple));
            return;
        }
        std::cerr << "Error: method '" << key << "' not found" << std::endl;
    }
};

The Insert method inserts a member function pointer to the map, and the Call method calls the member function with the given key and arguments.

It works well, but I realized that I need to create a different instance of MethodMap for every member function pointer that takes different arguments. For example, if I have the following member functions:

class MyClass {
public:
    void Method1(int x);
    void Method2(double d);
    void Method3(int x, const std::string& s);
    void Method4();
};

I would need to create a different instance of MethodMap for each member function pointer because they have different argument lists. For example:

MethodMap<MyClass> methodmap;
MyClass myClass;
methodmap.Insert("key", &MyClass::Method4); 
methodmap.Call("key", &myClass); 
MethodMap<MyClass, int> methodmapWithParameters; 
methodmapWithParameters.Insert("key", &MyClass::Method1);
methodmapWithParameters.Call("key", &myClass, 1);

Is there a way to handle this with a single instance of MethodMap? I did encounter similar questions, but in all of them the parameters given were always the same and I'm having trouble to generalize this myself.

DannyBoy
  • 23
  • 7
  • Do you plan to have only one instance of `MyClass` (i.e., static would do) or do you plan to have multiple instances? – lorro Feb 26 '23 at 12:35
  • I don't have the time to write an answer, but what you could do is to have a class `member_base_ptr` that is virtual, and that one can store in your map as pointer (ideally some kind of managed pointer so that it is properly released), from that you can extend a `member_ptr` with the types you need. In you `Call` you look up for that `member_base_ptr` and try to do a `dynamic_cast` to that `member_ptr` if it is successful, you then can forward the call to that one. – t.niese Feb 26 '23 at 12:50
  • @lorro I currently plan to use only one instance, yes, but I cannot make the class static (the option to create more instances in the future should be available). – DannyBoy Feb 26 '23 at 13:31
  • How do you expect the compiler to know whether `methodmapWithParameters.Call("key", &myClass, 1);` is correct, or `methodmapWithParameters.Call("key", &myClass, 1, 2, 3, 49);` is correct, or `methodmapWithParameters.Call("key", &myClass, "what", "is", "going". "on")` is correct? – n. m. could be an AI Feb 26 '23 at 15:02
  • Does https://stackoverflow.com/a/74482353/12173376 help? – joergbrech Feb 26 '23 at 18:35

3 Answers3

2

As the other answer used dynamic_cast, which I prefer to avoid, I'm showing you an alternative without it. Idea is to have a static in a map getter template member function; this maps this to name to function. Then your member functions will have template arguments instead of your class:

#include <iostream>
#include <string>
#include <functional>
#include <unordered_map>

class MethodMap {
private:
public:
    template <typename T, typename... Args>
    std::unordered_map<std::string, std::function<void(T*, Args...)>>& get_methodmap() const
    {
        static std::unordered_map<const MethodMap*, std::unordered_map<std::string, std::function<void(T*, Args...)>>> this2name2method;
        return this2name2method[this];
    }

    template <typename T, typename... Args>
    void Insert(const std::string& key, void (T::* method)(Args...)) {
        get_methodmap<T, Args...>()[key] = [method](T* obj, Args... args) { (obj->*method)(args...); };
    }

    template <typename T, typename... Args>
    void Call(const std::string& key, T* instance, Args&&... methodArgs) const {
        auto&& method_map = get_methodmap<T, Args...>();
        auto it = method_map.find(key);
        if (it != method_map.end()) {
            auto& func = it->second;
            // use tuple to store and forward the arguments
            std::tuple<Args...> arg_tuple(std::forward<Args>(methodArgs)...);
            std::apply(func, std::tuple_cat(std::make_tuple(instance), arg_tuple));
            return;
        }
        std::cerr << "Error: method '" << key << "' not found" << std::endl;
    }
};


class MyClass {
public:
    void Method1(int x) {}
    void Method2(double d) {}
    void Method3(int x, const std::string& s) {}
    void Method4() {}
};


int main()
{
    MethodMap methodmap;
    MyClass myClass;
    methodmap.Insert("key", &MyClass::Method4); 
    methodmap.Call("key", &myClass); 
    methodmap.Insert("key", &MyClass::Method1);
    methodmap.Call("key", &myClass, 1);
}
lorro
  • 10,687
  • 23
  • 36
  • That's also a nice solution. One thing to note is that with this solution, one key could hold different functions (if the class and/or the function signature differs), this can be something that is intentional and fit desired use case, but it could also be something that is not desired. – t.niese Feb 26 '23 at 15:27
  • Re: _"the option to create more instances in the future should be available"_ and the `static` maps will probably not work out well if I understood it correctly. – Ted Lyngmo Feb 26 '23 at 19:53
  • @TedLyngmo It'll work out well, that static map is first indexed by `this` pointer. – lorro Feb 26 '23 at 19:57
  • @t.niese As per OP's comment, it's desired to be able to have multiple different `MethodMap` instances. – lorro Feb 26 '23 at 19:58
  • @lorro Ah, yes, now I see. Ok, as long as keeping these maps alive until the program dies is fine, it should be ok. In a way, it's similar to my solution except I store the maps in a non-`static` `tuple`. – Ted Lyngmo Feb 26 '23 at 20:05
1

What you could do is to have a class member_base_ptr that is virtual and that one can store in your map as a pointer (ideally some kind of managed pointer so that it is properly released), from that you can extend a member_ptr with the types you need. In that you do the the look up for that member_base_ptr and try to do a dynamic_cast to that member_ptr, and if it is successful, you than can forward the call to that one.

Here a rough draft of that idea, but I didn't spend much time thinking about everything in that code, please verify if everything is really valid and does not result in undefined behavior.

#include <iostream>
#include <functional>
#include <memory>
#include <map>

struct Test {
    int test1(float i){
        std::cout << "test1" << "\n";

        return 10;
    }


    int test2(std::string s){
        std::cout << "test1" << "\n";

        return 20;
    }
};

struct member_base_ptr {
    virtual ~member_base_ptr() = default;
};

template <typename T, typename RT, typename... Args>
struct member_ptr: public member_base_ptr {

    std::function<RT(T*, Args...)> m_ptr;

    member_ptr(RT (T::* method)(Args...)) {
        m_ptr = [method](T* obj, Args... args) { return (obj->*method)(args...); };
    }

    RT call(T* instance, Args&&... methodArgs) const {
        return m_ptr(instance, std::forward<Args>(methodArgs)...);
    }
};

struct method_map {
    std::map<std::string, std::unique_ptr<member_base_ptr>> m_ptrs;


    void insert(std::string key, auto type) {
        std::unique_ptr<member_base_ptr> ptr = std::make_unique<decltype(member_ptr(type))>(type);
        m_ptrs.insert(std::make_pair(key, std::move(ptr)));
    }

    template <typename RT, typename T, typename... Args>
    RT call(const std::string& key, T* instance, Args&&... methodArgs) const {
        auto it = m_ptrs.find(key);
        if(it != m_ptrs.end()) {
            member_base_ptr *base_ptr = it->second.get();
            auto test = dynamic_cast<member_ptr<T, RT, Args...> *>(base_ptr);
            if( test == nullptr ) {
                throw std::runtime_error("casting failed");
            }
            return test->call(instance, std::forward<Args>(methodArgs)...);
        }
        throw std::runtime_error("not found");
    }
};


int main()
{
    Test t;
    method_map map;
    map.insert("test1", &Test::test1);
    map.insert("test2", &Test::test2);
    std::cout << map.call<int>("test1", &t, 1.f) << "\n";
    std::cout << map.call<int>("test2", &t, std::string("test")) << "\n";
    
    return 0;
}

Here a changed version of the code that allows "type hinting" for the insert function if overloaded functions should be supported:

#include <iostream>
#include <functional>
#include <memory>
#include <map>

struct Test {
    int test1(float i){
        std::cout << "test1 f" << "\n";

        return 10;
    }


    int test1(int i){
        std::cout << "test1 i" << "\n";
        return 10;
    }


    int test2(std::string s){
        std::cout << "test1" << "\n";

        return 20;
    }
};

struct member_base_ptr {
    virtual ~member_base_ptr() = default;
};

template <typename T, typename RT, typename... Args>
struct member_ptr: public member_base_ptr {

    std::function<RT(T*, Args...)> m_ptr;

    member_ptr(RT (T::* method)(Args...)) {
        m_ptr = [method](T* obj, Args... args) { return (obj->*method)(args...); };
    }

    RT call(T* instance, Args&&... methodArgs) const {
        return m_ptr(instance, std::forward<Args>(methodArgs)...);
    }
};

struct method_map {
    std::map<std::string, std::unique_ptr<member_base_ptr>> m_ptrs;


    template <typename... Args, typename RT, typename T>
    void insert(std::string key,RT (T::* method)(Args...)) {
        std::unique_ptr<member_base_ptr> ptr = std::make_unique<member_ptr<T, RT, Args ...>>(method);
        m_ptrs.insert(std::make_pair(key, std::move(ptr)));
    }

    template <typename RT, typename T, typename... Args>
    RT call(const std::string& key, T* instance, Args&&... methodArgs) const {
        auto it = m_ptrs.find(key);
        if(it != m_ptrs.end()) {
            member_base_ptr *base_ptr = it->second.get();
            auto test = dynamic_cast<member_ptr<T, RT, Args...> *>(base_ptr);
            if( test == nullptr ) {
                throw std::runtime_error("casting failed");
            }
            return test->call(instance, std::forward<Args>(methodArgs)...);
        }
        throw std::runtime_error("not found");
    }
};


int main()
{
    Test t;
    method_map map;
    map.insert<float>("test1f", &Test::test1);
    map.insert<int>("test1i", &Test::test1);
    map.insert("test2", &Test::test2);
    std::cout << map.call<int>("test1f", &t, 1.f) << "\n";
    std::cout << map.call<int>("test1i", &t, 1) << "\n";
    std::cout << map.call<int>("test2", &t, std::string("test")) << "\n";
    
    return 0;
}
t.niese
  • 39,256
  • 9
  • 74
  • 101
  • It works! To further expand upon your answer, in case the methods are called the same (test1), the ```insert``` commend would be :```map.insert("test1", static_cast(&Test::test1));```, where ```int``` is the return type and ```float``` is the parameter type. – DannyBoy Feb 26 '23 at 14:55
  • @DannyBoy If they are called the same but have different arguments you could rewrite the `insert` member function to allow you to explicitly tell which parameter types to use. I added an update code that does that. – t.niese Feb 26 '23 at 15:11
  • What if one of the overloaded methods has no parameters? I tried ```Insert```, ```Insert<>``` and ```Insert``` but I still get an error. – DannyBoy Feb 26 '23 at 17:26
0

I've opted for a version without virtual dispatch and store functions with different signatures in separate maps instead. Instantiating a MethodMap will then be done by specifying what member function signatures the map should support. Example:

class MyClass {
public:
    void Method1(int x) { std::cout << "got int " << x << '\n'; }
    double Method2(double a, double b) { return a + b; }
    int Method3(int x) { return x * x; }
};

MethodMap<MyClass, void(int), double(double, double), int(int)> methodmap;

The inner "map" then becomes a std::tuple:

template<class...> struct arg_pack;
template<class T, class R, class... Args> struct arg_pack<T, R(Args...)> {
    using function_type = std::function<R(T&, Args...)>;
};
template<class... Ts> using arg_pack_t = typename arg_pack<Ts...>::function_type;

template <class T, class... ArgPacks>
class MethodMap {
public:
    using map_type = std::tuple<std::unordered_map<std::string,
                                                   arg_pack_t<T, ArgPacks>>...>;
private:
    map_type method_map;
};

Storing new member function pointers is done by first std::getting the correct map from the tuple. This "lookup" is done at compile time:

template<class R, class... Args>
void Insert(const std::string& key, R(T::*method)(Args...) ) {
    auto& m = std::get<std::unordered_map<std::string,
                                          arg_pack_t<T, R(Args...)>>>(method_map);

    m[key] = [method](T& instance, Args&&... args) -> decltype(auto) {
        return (instance.*method)(std::forward<Args>(args)...);
    };
}

and calling functions is done in a similar fashion:

template<class R, class... Args>
decltype(auto) Call(const std::string& key, T& instance, Args&&... args) const {
    auto& m = std::get<std::unordered_map<std::string,
                                          arg_pack_t<T, R(Args...)>>>(method_map);
    return m.at(key)(instance, std::forward<Args>(args)...);
}

Since calling these functions also supports return values other than void, you supply the return type to Call:

int main() {
    MethodMap<MyClass, void(int), double(double, double), int(int)> methodmap;
    MyClass myClass;

    methodmap.Insert("key1", &MyClass::Method1);
    methodmap.Insert("key2", &MyClass::Method2);
    methodmap.Insert("key3", &MyClass::Method3);

    methodmap.Call<void>("key1", myClass, 1); // void

    std::cout << methodmap.Call<double>("key2", myClass, 2.141, 1.0) << '\n';
    std::cout << methodmap.Call<int>("key3", myClass, 5) << '\n';
}

Demo

Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108