c++templatesc++20metaprogramming

C++20 Map of wrapped member functions


For intent, check Clarification below:

I have a class with several member functions. Each one has not only different number of parameters, but also different return types.

Let's say my class is like this:

class Pipe
{
public:

    Pipe() {}
    
    int MethodA()
    {
        std::cout << "MethodA" << std::endl;
        return 0;
    }
    float MethodA1(int a)
    {
        std::cout << "MethodA1, a: " << a << std::endl;
        return 1.5f;
    }
    int MethodA2(int a, char b) { return 2; }
    
    void MethodB() {}
    void MethodB1(int a) {}
    void MethodB2(int a, char b) {}

};

In C++, this next code is valid and works as expected:

std::function<int(Pipe*)> f1 = std::mem_fn(&Pipe::MethodA);
std::function<float(Pipe*, int)> f2 = std::mem_fn(&Pipe::MethodA1);

Pipe p;
f1(&p);
f2(&p, 20);

The result as expected is:

MethodA
MethodA1, a: 20

Let's say, I have also the next class:

class PipelineMethodResult
{
public:
    //template <typename Ret>
    PipelineMethodResult(int result) : result_(result) {}
    template<typename T>
    void PackReturnValue(T returnValue)
    {

    }
private:
    int result_;
    unsigned char returnValue[];
};

template<>
void PipelineMethodResult::PackReturnValue<int>(int returnValue)
{
    std::cout << "Packing int " << returnValue << std::endl;
}

template<>
void PipelineMethodResult::PackReturnValue<float>(float returnValue)
{
    std::cout << "Packing float " << returnValue << std::endl;
}

We can do this as well:

PipelineMethodResult g1(Pipe* p)
{
    auto res = f1(p);
    PipelineMethodResult r(0);
    r.PackReturnValue(res);

    std::cout << res << std::endl;

    return r;
}

PipelineMethodResult g2(Pipe* p, int theInt)
{
    auto res = f2(p, theInt);
    PipelineMethodResult r(0);
    r.PackReturnValue(res);

    std::cout << res << std::endl;

    return r;
}

Pipe p;
g1(&p);
g2(&p, 10);

The result as expected is:

MethodA
Packing int 0
0
MethodA1, a: 10
Packing float 1.5
1.5

Now, as per Store functions with different signatures in a map, we have this class:

template<typename Ret>
struct AnyCallable
{
    using RetType = Ret;

    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;
};

We can do the next thing with this:

auto callable1 = AnyCallable<PipelineMethodResult>(g1);
auto callable2 = AnyCallable<PipelineMethodResult>(g2);

Pipe p;
callable1(&p);
callable2(&p, 15);

The result obtained again is the expectecd one:

MethodA
Packing int 0
0
MethodA1, a: 15
Packing float 1.5
1.5

And we can do this as well:

std::map<std::string, AnyCallable<PipelineMethodResult>> callableMap = {
    { "MethodA", AnyCallable<PipelineMethodResult>(g1) },
    { "MethodA1", AnyCallable<PipelineMethodResult>(g2) }
};

Pipe p;
callableMap["MethodA"](&p);
callableMap["MethodA1"](&p, 30);

Again getting what we expect:

MethodA
Packing int 0
0
MethodA1, a: 30
Packing float 1.5
1.5

My question is the next: How can I make this simpler. There could be two options for me:

The idea is to have a map of member functions for the methods of Pipe.

Thanks in advance.

CLARIFICATION

What I want to achieve is a kind of RPC call method, but instead of knowing exactly the signature of the function in the client and the server (having a proxy and a stub), what I want is that the server be able to provide a dynamic list of methods, so that the client does just only need to get that list and populate the methods dinamically. (The Server is C++, the client is C#).

As I want to call the server methods using the string passed via flatbuffers, I need AnyCallable to be able to be put in a map, and for this reason they must return the same type. The wrapper intends to wrap the real result type that will be returned in a flatbuffer to the client, making able to put the AnyCallable in a map.

About the construction, I would like to convert this 463035818_is_not_an_ai answer:

AnyCallable A = std::function<int(Pipe*)>{&Pipe::MethodA};

to something simpler:

std::map<std::string, AnyCallable<PipelineMethodResult>> callableMap = {
    { "MethodA", AnyCallable<PipelineMethodResult>(&Pipe::MethodA) },
    { "MethodA1", AnyCallable<PipelineMethodResult>(&Pipe::MethodA1) }
};

SOLUTION

Thanks to @463035818_is_not_an_ai I came to a solution. Can be found here (https://godbolt.org/z/5978oq75h) and below.

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

class Pipe
{
public:

    Pipe() {}

    int MethodA()
    {
        std::cout << "MethodA" << std::endl;
        return 0;
    }
    float MethodA1(int a)
    {
        std::cout << "MethodA1, a: " << a << std::endl;
        return 1.5f;
    }
    int MethodA2(int a, char b) { return 2; }

    void MethodB() {}
    void MethodB1(int a) {}
    void MethodB2(int a, char b) {}
};

class PipelineMethodResult
{
public:
    PipelineMethodResult(int result) : result_(result) {}
    template<typename T>
    void PackReturnValue(T returnValue)
    {

    }
private:
    int result_;
    unsigned char returnValue[];
};

template<>
void PipelineMethodResult::PackReturnValue<int>(int returnValue)
{
    std::cout << "Packing int " << returnValue << std::endl;
}

template<>
void PipelineMethodResult::PackReturnValue<float>(float returnValue)
{
    std::cout << "Packing float " << returnValue << std::endl;
}

struct PipelineMethodWrapper
{
    PipelineMethodWrapper() {}
    template<typename R, typename T, typename... Args>
    PipelineMethodWrapper(R(T::*method)(Args...))
    {
        auto pmr = [method](T* p, Args... args) -> PipelineMethodResult
        {
            R res = (p->*method)(args...);
            PipelineMethodResult r(0);
            r.PackReturnValue(res);

            return r;
        };

        callable_ = std::function<PipelineMethodResult(T*, Args...)>{pmr};
    }
    template<typename... Args>
    PipelineMethodResult operator()(Args&&... args)
    {
        try
        {
            auto casted = std::any_cast<std::function<PipelineMethodResult(Args...)>>(callable_);
            PipelineMethodResult r = std::invoke(casted, std::forward<Args>(args)...);
            return r;
        }
        catch (std::bad_any_cast&)
        {

        }

        return PipelineMethodResult(-1);
    }
    std::any callable_;
};


std::map<std::string, PipelineMethodWrapper> callableMap = {
    { "MethodA", PipelineMethodWrapper(&Pipe::MethodA) },
    { "MethodA1", PipelineMethodWrapper(&Pipe::MethodA1) }
};

int main()
{
    Pipe p;

    callableMap["MethodA"](&p);
    callableMap["MethodA1"](&p, 40);

    return 0;
}

Solution

  • I am not sure if I correctly understand the question....

    AnyCallable is based on std::function and std::function works flawlessly with member functions already. You do not need std::mem_fn for that. You also do not need a "PipeMethodWrapper class". std::functions constructor does the magic under the hood. You can choose to have the instance parameter as pointer or reference (I choose pointer below).

    Without modifications you can use Pipe and AnyCallable to store different member functions and call them:

    int main() {
        AnyCallable A = std::function<int(Pipe*)>{&Pipe::MethodA};
        AnyCallable A1 = std::function<float(Pipe*,int)>{&Pipe::MethodA1};
        Pipe p;
        A(&p);
        A1(&p,42);
    }
    

    Complete example

    However, as far as I can see your AnyCallable is somewhat broken. Instead of blindy appliying the any_cast it should use the any_cast overload that throws when the wrong type is requested and handle that error in an appropriate way.