1

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.

Oersted
  • 769
  • 16
  • why use any? I think you can have std::function in your map and then use a [lambda expression](https://en.cppreference.com/w/cpp/language/lambda) to capture the object by reference. A member function (pointer) is still useless without an object instance to refer to so that must be added somehow too – Pepijn Kramer May 26 '23 at 14:25
  • 1
    "*to be able to call the member function, for instance with*" That doesn't use member function call syntax; it would never work. Also `"2"` is not a `std::string`, so why would `Args` include a `std::string` as an argument? – Nicol Bolas May 26 '23 at 14:26
  • 1
    Why do you expect that `void (VarCall::*)(const char*)` should have the same signature type name as `void (VarCall::*)(const std::string&)`? – Mestkon May 26 '23 at 14:26

2 Answers2

2

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).

Mestkon
  • 3,532
  • 7
  • 18
  • @Meskon Oh it was so obvious! Thanks. The right design would be probably a bit more touchy if we'd go for the forwarding reference syntax. As it was not originally my question, I leave it there. In order for the answer to be complete with respect to original question, how would I call the right function from ```call```? I leave you my failed attempt [there](https://godbolt.org/z/GWY36Pb6v) – Oersted May 26 '23 at 15:11
2

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.

Oersted
  • 769
  • 16
  • This solution is pretty close to what I was after. The only downside here is, that it enforces to use const refs in all member functions and the caller would have to cast the parameters to the correct type. Is that a limitation of the any cast? – merula May 31 '23 at 07:01
  • @merula no, the issue lies with the template variadic ```call``` and the```foo```, ```bar``` and ```baz``` signatures that must be compliant. Otherwise, you will give the wrong type for the cast, thus the exception. In order to have a more complete answer, you'll need to know in advance the type of signatures you want to support (accepting rvalues, lvalues, by ref, by value, const or not const,...). More to follow... – Oersted May 31 '23 at 07:27
  • @merula in you original design, you passed ```int``` by value, ```std::string``` by const ref there is no single template parameter pattern that can match both as such. For instance, if you use a forwarding-reference ```Args&&``` instead of ```const Args```, the rvalue ```std::string("2")``` will be bind to ```std::string&&``` and you don't have a function that handle that. Same applies to ```int(2)```, you'll need a function that specifically accept rvalue to ```int```. Beware I'm not 100% confident about that and I hope that some more experimented developer will confirm/amend what I wrote. – Oersted May 31 '23 at 07:31
  • If thought this could be solved by using `std::forward(Args)...`, because it preserves the rvalue see https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2027.html#Perfect_Forwarding. But if I use that in your example code I get an any_cast exception. – merula May 31 '23 at 08:52
  • @merula it's the purpose of the edit I made in the answer: you cannot use forwarding-references without adding the corresponding functions for rvalue handling in the API. – Oersted May 31 '23 at 08:57