correct way to store a pointer to member function inside std::any

260 Views Asked by At

Looking into this question about map of member function I'm observing an anomaly in the way to pass a pointer to member function into a std::any.

I'm using the following snippet:

#include <any>
#include <iostream>
#include <map>

class VarCall {
   public:
    VarCall()
        : calls({{"Alice", std::any(&VarCall::foo)},
                 {"Bob", std::any(&VarCall::bar)},
                 {"Charlie", std::any(&VarCall::baz)}}) {}

    template <typename... Args>
    void call(const std::string& what, Args... args) {
        void (VarCall::*ptr)(Args...);
        std::any a = ptr;
        std::cout << a.type().name() << std::endl;
        std::cout << calls[what].type().name() << std::endl;
        // failed attempt to call
        // this->*(std::any_cast<decltype(ptr)>(calls[what]))(args...);
    }

   public:
    void foo() { std::cout << "foo()" << std::endl; }
    void bar(const std::string& s) {
        std::cout << "bar(" << s << ")" << std::endl;
    }
    void baz(int i) { std::cout << "baz(" << i << ")" << std::endl; }

    std::map<std::string, std::any> calls;
};

int main() {
    VarCall v;

    void (VarCall::*ptr)(const std::string& s);
    std::any a = ptr;
    std::any b(&VarCall::bar);
    std::cout << a.type().name() << std::endl;
    std::cout << b.type().name() << std::endl;

    // v.call("Alice");
    v.call("Bob", "2");
    // v.call("Charlie", 1);

    return 0;
}

I'm expecting to have the following output:

M7VarCallFvRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE

M7VarCallFvRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE

M7VarCallFvRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE

M7VarCallFvRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE

but get

M7VarCallFvRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE

M7VarCallFvRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE

M7VarCallFvPKcE

M7VarCallFvRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEE

When initializing the std::any inside the VarCall() initializer list, I'm getting a different signature.

What is happening? Is there a way to get the expected output? The final goal, as indicated in the original question from @merula, if to be able to call the member function, for instance with: std::any_cast<std::add_pointer_t<void(Args ...)>>(calls[what])(args...); inside call member function.

2

There are 2 best solutions below

5
Oersted On BEST ANSWER

With the help of @Mestkon, a more complete answer (also to this question) would be:

#include <any>
#include <iostream>
#include <map>

class VarCall {
   public:
    VarCall()
        : calls({{"Alice", std::any(&VarCall::foo)},
                 {"Bob", std::any(&VarCall::bar)},
                 {"Charlie", std::any(&VarCall::baz)}}) {}

    template <typename... Args>
    void call(const std::string& what, const Args&... args) {
        using ptr_t = void (VarCall::*)(const Args&...);
        // all parenthesis are important
        (this->*(std::any_cast<ptr_t>(calls[what])))(args...);
    }

   public:
    void foo() { std::cout << "foo()" << std::endl; }
    void bar(const std::string& s) {
        std::cout << "bar(" << s << ")" << std::endl;
    }
    void baz(int const& i) { std::cout << "baz(" << i << ")" << std::endl; }

    std::map<std::string, std::any> calls;
};

int main() {
    VarCall v;

    v.call("Alice");
    v.call("Bob", std::string("2"));
    v.call("Charlie", int(1));

    return 0;
}

Live

[EDIT] in answer to @merula comment, and in order to illustrate my anwer to that comment, here is a code using forwarding-reference:

#include <any>
#include <iostream>
#include <map>

class VarCall {
   public:
    VarCall()
        : calls({
              {"Alice", std::any(&VarCall::foo)},
              {"Bob", std::any(&VarCall::bar)},
              {"Charlie", std::any(&VarCall::baz)},
              {"Bobrv", std::any(&VarCall::barrv)},
              {"Charlierv", std::any(&VarCall::bazrv)},
          }) {}

    // using forwarding reference
    template <typename... Args>
    void call(const std::string& what, Args&&... args) {
        using ptr_t = void (VarCall::*)(Args&&...);
        (this->*(std::any_cast<ptr_t>(calls[what])))(
            std::forward<Args...>(args)...);
    }

   public:
    void foo() { std::cout << "foo()" << std::endl; }
    void bar(const std::string& s) {
        std::cout << "bar(" << s << ")" << std::endl;
    }
    void baz(int const& i) { std::cout << "baz(" << i << ")" << std::endl; }
    void barrv(std::string&& s) {
        std::cout << "barrv(" << s << ")" << std::endl;
    }
    void bazrv(int&& i) { std::cout << "bazrv(" << i << ")" << std::endl; }

    std::map<std::string, std::any> calls;
};

int main() {
    VarCall v;

    v.call("Alice");
    v.call("Bobrv", std::string("2"));
    // v.call("Bob", std::string("2"));    // KO
    v.call("Charlierv", int(1));
    // v.call("Charlie", int(1));  // KO

    return 0;
}

Live The main idea is that a rvalue of type T is also map to an rvalue of same type but the VarCall API did not contain functions with rvalue signature. Thus there must be added as in the snippet above.

1
Mestkon On

To answer why you get a different signature inside the call function you have to figure out what type you are actually dealing with.

The template function parameters to a function template are deduced by the types you are passing to it. When the template is unqualified as in this case (no const or &) then the deduced parameter will have decayed value semantics.

The parameter you provide is "2" which is of type const char(&)[2], a reference to an array containing '2' and '\0'. This parameter decays to a const char* when passed to the function.

Therefore the function signature you typedef inside the call member function is deduced as void (VarCall::*)(const char*), whereas the other signatures that you are printing is declared as void (VarCall::*)(const std::string&).

To get the same signature as expected, then you have to modify the signature of the call function to void call(const std::string&, Args&& ...) or void call(const std::string&, const Args& ...). Then you have to modify the callsite to either const std::string arg = "2"; v.call("Bob", arg); or v.call("Bob", std::string("2")); (The last only works for the second call signature above).