Home > Net >  Function pointer to member function T(U) where T and U may or may not be void
Function pointer to member function T(U) where T and U may or may not be void

Time:08-24

I have a class Frobnicator that handles various requests.

class Frobnicator
{
    public:

        // Handlers are member functions.
        // They optionally take some input. They optionally return some output. But they always take the context!
        // There are more types than just int involved, but it's always just one input or void, and one output or void.
        void performSomething(Context* context)            { /* ... */ } // Takes void, returns void
        void setSomething    (Context* context, int input) { /* ... */ } // Takes int , returns void
        int  getSomething    (Context* context)            { /* ... */ } // Takes void, returns int
        int  convertSomething(Context* context, int input) { /* ... */ } // Takes int , returns int

        template<typename TResult, typename TParameter>
        void registerHandler(std::string identifier, TResult(Frobnicator::* handler)(Context*, TParameter))
        {
            // The external API actually wants a callback that takes and returns JSON. We give it a lambda that does the conversion and calls the actual member function.
            // The identifier tells the external API which callback to call for which request. It's not relevant for this question, just to show the idea. Think of something like a REST API.
            someExternalApiThatWantsJson.registerHandler(identifier, [&](Context* context, Json input)
            {
                // Idealy, this would be a one-liner.
                //return Json::convertFrom((this->*handler)(context, input.convertTo<TParameter>()));
                // But calling Json.convertTo<void>() and Json::convertFrom(void) does not work automagically anyways, so we need to split it up manually:
                Json result;
                if constexpr (std::is_same<TResult, void>::value)
                    if constexpr (std::is_same<TParameter, void>::value)                            (this->*handler)(context                               ) ; // Takes void, returns void
                    else                                                                            (this->*handler)(context, input.convertTo<TParameter>()) ; // Takes something, returns void
                else
                    if constexpr (std::is_same<TParameter, void>::value) result = Json::convertFrom((this->*handler)(context                               )); // Takes void, returns something
                    else                                                 result = Json::convertFrom((this->*handler)(context, input.convertTo<TParameter>())); // Takes something, returns something
                return result;
            });
        }

        // Set up the handlers.
        void setup()
        {
            // The problem is that some of these calls don't work:
            registerHandler            ("PerformSomething", &Frobnicator::performSomething); // "failed template argument deduction"
            registerHandler<void, void>("PerformSomething", &Frobnicator::performSomething); // Trying to specify the types explicitly: "substitution failure [with TResult = void, TParameter = void]: argument may not have 'void' type"
            registerHandler            ("SetSomething"    , &Frobnicator::setSomething);     // Compiles fine
            registerHandler            ("GetSomething"    , &Frobnicator::getSomething);     // "failed template argument deduction"
            registerHandler<int , void>("GetSomething"    , &Frobnicator::getSomething);     // Trying to specify the types explicitly: "substitution failure [with TResult = int, TParameter = void]: argument may not have 'void' type"
            registerHandler            ("ConvertSomething", &Frobnicator::convertSomething); // Compiles fine
        }

};

TResult can be int or void and it works fine. But it only works when TParameter isn't void.
How can I make registerHandler also accept pointers to functions that take no arguments?

The idea is to have the member functions' signatures very clean and the calls to registerHandler mostly clean. So giving performSomething and getSomething a dummy parameter is out of the question. Manually specifying the types when calling registerHandler is ugly but I'll accept it, if it's necessary.

The body of registerHandler is relatively short and mostly deals with distinguishing void from non-void anyways, so providing a specialization for when TParameter is void would be a fine solution:

template<typename TResult>
void registerHandler<TResult, void>(std::string identifier, TResult(Frobnicator::* handler)(Context*))

Except that "function template partial specialization is not allowed".

CodePudding user response:

Use template parameter pack to deal with void / non-void cases, since the number of arguments is indetermined (1 or 2).

template<typename TResult, typename... TParameter>
void registerHandler(std::string identifier, TResult(Frobnicator::* handler)(Context*, TParameter...))

The second parameter accepts a pointer to member function whose the first argument is required to be Context*, that's it.

Then,

void setup()
{
    registerHandler            ("PerformSomething", &Frobnicator::performSomething);
    registerHandler            ("SetSomething"    , &Frobnicator::setSomething);
    registerHandler            ("GetSomething"    , &Frobnicator::getSomething);
    registerHandler            ("ConvertSomething", &Frobnicator::convertSomething);
}

The implementation of registerHandler may also need some changes, the std::is_same_v<TParameter, void> can be replaced by sizeof...(TParameter) == 0.

Demo

CodePudding user response:

Well. Sometimes you waste an hour because you narrowly miss a solution. Just don't make it a specification:

template<typename TResult>
void registerHandler(std::string identifier, TResult(Frobnicator::* handler)(Context*))

But!: This is a valid solution for me, but it would be nice to have a solution that does not require duplicating almost the entire function body. So better answers are absolutely welcome!

CodePudding user response:

What I would do is follow the 0 1 infinity rule and write infinity instead of 2 cases.

Support any number of arguments. Map to tuples. Empty tuples for 0, mono for 1, etc.

Then the conversion code should work naturally. You can bind the call to be (take tuple, return tuple), then write the input/output code to handle 0, 1, or n from/to json.

Now the json function call logic no longer cares about void. The packing unpacking does. And the code that calls the raw function from tuples and packs it into a tuple return does.

template<class F>
auto packresult(F&&f){
  if constexpr(f() is void)
    f()
    return tuple<>{};
  else if constexpr(f returns a tuple)
    return f();
  else
    return std::make_tuple(f());
}

Now you do the same for inputs

auto unpackargs(auto&&f){
  return [f](auto&&tup){
    return std::apply(f, tup);
  }
}

which makes your code look like:

   converttupletojson(packresult(bind(unpackargs(bind this to method), getargsfromjson<Args...>())))

and viola, the most surprising musical instrument.

  • Related