c++templatestemplate-meta-programmingstd-variantstdapply

Generically wrap member function of object to modify return type


Version restriction: C++17.

I'm trying to create a type capable of accepting a callable object of any type and wrapping one of its member functions (in this case, operator()) to take the same arguments, but modify (cast) the return type. An example follows:

template <typename Ret, typename Callable>
struct ReturnConverter : Callable
{
    ReturnConverter(Callable cb) : Callable(cb) { }

    Ret operator()(argsof(Callable::operator()) args)  // magic happens here
    {
        return static_cast<Ret>(Callable::operator()(std::forward<??>(args)); // how to forward?
    }
};

template <typename Ret, typename Callable>
auto make_converter(Callable cb)
{
    return ReturnConverter<Ret, Callable>(cb);
}

int main()
{
    auto callable = []() { return 1.0f; };
    auto converted = make_converter<int>(callable);

    auto x = converted(); // decltype(x) = int
}

ReturnConverter can take an object and override that object's operator() to cast whatever it returns to Ret.

The problem is expressing the argument types of the wrapping function - they should be exactly the same as those of Callable::operator(). Using a variadic template with std::forward does not satisfy this goal, as it would modify the signature of the function (operator() now becomes a template where it wasn't before).

How can I express the argsof operator I've highlighted above?


Motivation: I'd like to modify the std::visit overload technique demonstrated in this article to be able to specify the desired return type from multiple lambda functors, so that I don't have to strictly match the return type in every lambda, for instance:

std::variant<int, float, void*> v = ...;
auto stringify = overload(
    [](int x) { return "int: " + std::to_string(x); },
    [](float x) { return "float: " + std::to_string(x); },
    [](auto v) { return "invalid type!"; }  // error! const char* != std::string
);
std::visit(stringify, v);

With the change above, I'd be able to write something like auto stringify = overload<std::string>(...);


Solution

  • I don't see a way to respond to your exact answer but... considering the "Motivation" of the question... I propose a wrapper for overload (a class that inherit from a class with one or more operator(), call the appropriate operator() from the base class and cast the return value to type Ret)

    template <typename Ret, typename Wrpd>
    struct wrp_overload : public Wrpd
    {
      template <typename ... Args>
      Ret operator() (Args && ... as)
      { return Wrpd::operator()(std::forward<Args...>(as)...); }
    };
    

    and, given the Ret type isn't deducible from the argument (the overload class) and that CTAD doesn't permit to explicit a template argumend, seems to me that a make_wrp_overload() function is required

    template <typename Ret, typename ... Cs>
    auto make_wrp_overload (Cs && ... cs)
    { return wrp_overload<Ret, overload<Cs...>>{{std::forward<Cs>(cs)...}}; }
    

    so your std::visit() call become

    std::visit(make_wrp_overload<std::string>(
               [](int x) { return "int: " + std::to_string(x); },
               [](float x) { return "float: " + std::to_string(x); },
               [](auto v) { return "invalid type!"; } 
    ), package);
    

    The following is a full compiling C++17 example

    #include <iostream>
    #include <variant>
    
    template <typename ... Ts>
    struct overload : public Ts...
    { using Ts::operator()...; };
    
    // not required anymore (also C++17)
    //template <typename ... Ts> overload(Ts...) -> overload<Ts...>;
    
    template <typename Ret, typename Wrpd>
    struct wrp_overload : public Wrpd
    {
      template <typename ... Args>
      Ret operator() (Args && ... as)
      { return Wrpd::operator()(std::forward<Args...>(as)...); }
    };
    
    template <typename Ret, typename ... Cs>
    auto make_wrp_overload (Cs && ... cs)
    { return wrp_overload<Ret, overload<Cs...>>{{std::forward<Cs>(cs)...}}; }
    
    
    int main() {
        std::variant<int, float, void*> package;
    
        std::visit(make_wrp_overload<std::string>(
                   [](int x) { return "int: " + std::to_string(x); },
                   [](float x) { return "float: " + std::to_string(x); },
                   [](auto v) { return "(no more) invalid type"; } 
        ), package);
    }