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.

5
  • Do you plan to have only one instance of MyClass (i.e., static would do) or do you plan to have multiple instances? Commented Feb 26, 2023 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. Commented Feb 26, 2023 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). Commented Feb 26, 2023 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? Commented Feb 26, 2023 at 15:02
  • Does stackoverflow.com/a/74482353/12173376 help? Commented Feb 26, 2023 at 18:35

3 Answers 3

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);
}
Sign up to request clarification or add additional context in comments.

5 Comments

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.
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.
@TedLyngmo It'll work out well, that static map is first indexed by this pointer.
@t.niese As per OP's comment, it's desired to be able to have multiple different MethodMap instances.
@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.
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;
}

3 Comments

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<int(Test::*)(float)>(&Test::test1));, where int is the return type and float is the parameter type.
@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.
What if one of the overloaded methods has no parameters? I tried Insert<void>, Insert<> and Insert but I still get an error.
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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.