Warm tip: This article is reproduced from serverfault.com, please click

Is forwarding of all callables still not possible in C++20?

发布于 2020-11-27 22:04:05

I would like to make a universal wrapper function which can accept any

  • free standing or static function,
  • member function ( perhaps as a specialization with the first argument used as *this)

including overloaded or templated cases together with variable arguments. Such wrapper will then, in the body, call the function exactly with the forwarded parameters.

Example:

template<typename Fnc,typename...Args>
void wrapper(Fnc fnc, Args&&...args){
    // Do some stuff

    // Please disregard the case when return type is void,
    // that be SFINAED with std::result_of.
    auto res = fnc(std::forward<Args>(args)...); 

    // Do some stuff
    return res;
}
#include <vector>

auto foo(int i){ return i; }
auto foo(double& d){ return d; }
auto foo(double&& d){ return d; }
auto foo(const char* str){ return str; }

template<typename...T>
auto generic_foo(T&&...ts){/* ...*/ return /* ...*/; }

template<typename T>
void constrained_foo(std::vector<T>& lref,std::vector<T>&& rref, std::vector<T> value){ /**/}

int main(){
    // Basics
    wrapper(foo, 1);// foo(int)
    wrapper(foo, 1.1); // foo(double&&)
    wrapper(foo, "ahoj"); // foo(const char*)

    // Conversion must work too
    wrapper(foo, 1.1f); // foo(double&&)
    wrapper(foo, (short)1); // foo(int)

    // Detecting lvalues, rvalues is also a must
    double y;
    foo(y);
    foo(std::move(y));
    
    // Similarly for templates
    int x;
    std::vector<int> v1, v2, v3;
    wrapper(generic_foo, 1, 1.1, "ahoj", x, &x);
    wrapper(constrained_foo, v1, std::move(v2), v3);
}

The thing I do find so frustrating about this is that I am supplying all the necessary information to make these calls right next to each other, there is no added ambiguity about what to call, I could call it myself, I could(will) make a macro that can do it, but there is no proper C++ syntax for it AFAIK.

I discovered the need for it while trying to automatically call all my methods in certain "context". But thinking about it more, I believe this could have really widespread usage for <algorithm>, <thread> libraries too. Where you would not have to make one-statement lambdas just to call a function/operator with the lambdas parameters or something captured.

The issue arises in any function accepting another function which will be eventually called together with the passed known parameters.

My attempts

generic_foo can be resolved if the return type is fixed:

template<typename...Args>
void wrapper(void(*f)(Args&&... args) , Args&&...args){
    // ...
    f(std::forward<Args>(args)...);
}

int main(){
    int x;
    wrapper(generic_foo, 1, 1.1, "ahoj", x, &x, std::move(x));
}

This works nicely, the return type can maybe be resolved too by some obscure and clever use of std::invoke_result_t, but currently it is a kind of chicken-egg situation with the parameter type. Because the only thing how to resolve the name generic_foo is to force it to decay to a function pointer and then there is no name to put in std::invoke_result_t since the parameter is still being deduced.

This will also work with overloads as long as there is an exact match, so it cannot do conversions. This approach is as far as I can get without macros when the function name is not known in advance.

If the name of the callable is fixed, there is this frequently-used variation of lambda trick:

template<typename Fnc, typename...Args>
void wrapper(Fnc f , Args&&...args){
    // ...
    f(std::forward<Args>(args)...);
}

int main(){
    int x;
    wrapper([](auto&&...args)
            { return generic_foo(std::forward<decltype(args)>(args)...);},
            1, 1.1, "ahoj", x, &x, std::move(x));
}

If I add a macro doing exactly this:

#define WRAP_CALL(wrapper_fnc, called_fnc, ...) \
wrapper_fnc([&](auto&&...args) \
            { return called_fnc(std::forward<decltype(args)>(args)...);}, \
            __VA_ARGS__ ); 

int main(){
    int x;
    
    WRAP_CALL(wrapper, generic_foo, 1, 1.1, "ahoj", x, &x, std::move(x))
}

I get the least macro-infested working solution I can think of, it works with any callable and any wrappers which can stay proper C++ functions. But I would like macro-less version of this and especially for functions.

So I will probably still use this version, does not seem too unreasonable. Are there any corners cases I should know about?

I did not write a single C++20 concept yet, so I still have very small hope there might be something that can work in that area perhaps? But probably not since std::thread(foo,1); also suffers from it.

So this might sadly require language changes because the name of the overload set or a template cannot currently be passed anywhere, even just as some kind of aggregated type. So perhaps something akin to std::initializer_list class plus its sometimes-magical syntax?

If this is indeed the case, I would gladly accept any answer listing any currently active proposals that might help with this. If there are even any.

I did found N3579 - A Type trait for signatures which could perhaps work together with the function pointer solution if the chicken-egg problem is addressed. But the proposal looks very dead.

Questioner
Quimby
Viewed
0
Davis Herring 2020-11-28 11:27:46

The "overloaded or templated cases" are not entities that can be function/template arguments—certain cases where overload resolution can use contextual information notwithstanding. What you mean by foo in your wrapper(foo,…) is little more than a token (in particular, it's an overload set), and the language simply has no way of addressing such an object since it has no ("aggregated"?) type. (Conversely, macros do operate on tokens, which is why they are applicable here.) You seem to know most of this, but it may be helpful to see why it's knowably impossible, and why it's a bad idea to think of an overload set as "a callable". (After all, syntactically a type name is also "callable", but it wouldn't make any sense to pass one as a function argument.)

Even if a call f(g,…) were defined to try each overload of g in turn (which it is, for the narrow purpose of deducing f's template arguments), that wouldn't help for (an overload set containing) a template: there would be no way to even evaluate f's SFINAE conditions given a g template for which a specialization had not yet been chosen.

The standard lambda trick, which you also illustrated, is a way of performing overload resolution with the benefit of the argument list, which is why it's pretty much the only approach that works. There are certainly proposals to automate that process, including the vagaries of SFINAE-friendliness and exception specifications.