在C++20中仍然无法转发所有可调用对象吗?
我想做一个通用的包装函数,它可以接受任何
- 独立或静态功能,
- 成员函数(也许作为第一个参数用作的特化
*this)
包括重载或模板化的情况以及可变参数。然后,这样的包装器将在主体中准确地使用转发的参数调用该函数。
示例:
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);
}
我确实对此感到非常沮丧的是,我正在提供所有必要的信息来使这些电话彼此相邻,没有关于要调用什么的额外歧义,我可以自己调用它,我可以(将)一个可以做到这一点的宏,但是AFAIK没有合适的C++语法。
我在尝试在某些“上下文”中自动调用我的所有方法时发现了它的需要。但是仔细想想,我相信这也可以广泛用于<algorithm>,<thread>库。您不必仅使用 lambdas 参数或捕获的某些内容来调用函数/运算符,而不必创建单语句 lambdas。
问题出现在任何接受另一个函数的函数中,该函数最终将与传递的已知参数一起被调用。
我的尝试
generic_foo 如果返回类型是固定的,则可以解决:
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));
}
这很好用,返回类型也可以通过一些晦涩而巧妙的使用来解决std::invoke_result_t,但目前它是一种参数类型的鸡-蛋情况。因为如何解析名称的唯一方法generic_foo是强制它衰减为函数指针,然后没有名称可放入,std::invoke_result_t因为参数仍在推导中。
只要存在完全匹配,这也适用于重载,因此它无法进行转换。当事先不知道函数名称时,这种方法是我可以在没有宏的情况下获得的。
如果可调用对象的名称是固定的,那么 lambda 技巧的常用变体是:
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));
}
如果我添加一个宏这样做:
#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))
}
我得到了我能想到的最少宏感染的工作解决方案,它适用于任何可调用和任何可以保持正确 C++ 函数的包装器。但我想要这个的无宏版本,尤其是对于函数。
所以我大概还是会用这个版本,好像也不是太不合理。有没有我应该知道的角落案例?
我还没有写过一个 C++20 的概念,所以我仍然希望可能有一些东西可以在那个领域工作?但可能不会因为std::thread(foo,1);它也受到影响。
因此,这可能需要更改语言,因为当前无法将重载集或模板的名称传递到任何地方,即使作为某种聚合类型也是如此。那么也许类似于std::initializer_listclass 加上它有时神奇的语法?
如果情况确实如此,我很乐意接受列出任何可能有助于解决此问题的当前有效提案的任何答案。如果有的话。
我确实发现了N3579 - 签名的类型特征,如果解决了鸡蛋问题,它可能可以与函数指针解决方案一起使用。但是这个提议看起来很死。
回答
“重载或模板化的情况”不是可以作为函数/模板参数的实体——在某些情况下,尽管重载解析可以使用上下文信息。你是什么意思foo你wrapper(foo,…)是小多了一个令牌(特别是它的过载集),而语言只是没有(“聚集”?)式的方式解决这样的对象,因为它没有的。(相反,宏确实对令牌进行操作,这就是它们适用于此处的原因。)您似乎了解大部分内容,但了解原因可能会有所帮助众所周知,这是不可能的,以及为什么将重载集视为“可调用的”是一个坏主意。(毕竟,从语法上讲,类型名称也是“可调用的”,但将其作为函数参数传递没有任何意义。)
即使f(g,…)定义了一个调用来依次尝试每个重载g(这是为了推导f的模板参数的狭隘目的),这对(包含)模板的重载集也无济于事:没有办法甚至在f给定g尚未选择专业化的模板的情况下评估的 SFINAE 条件。
您还说明了标准lambda技巧是一种利用参数列表执行重载解析的方法,这就是为什么它几乎是唯一有效的方法。当然有一些建议可以使该过程自动化,包括 SFINAE 友好性和异常规范的变幻莫测。