Vit*_*meo 39 c++ overloading function-object template-meta-programming c++17
在C++ 17中,实现一个overload(fs...)函数是很容易的,在fs...满足任意数量的参数的情况下FunctionObject,它返回一个行为类似于重载的新函数对象fs....例:
template <typename... Ts>
struct overloader : Ts...
{
template <typename... TArgs>
overloader(TArgs&&... xs) : Ts{forward<TArgs>(xs)}...
{
}
using Ts::operator()...;
};
template <typename... Ts>
auto overload(Ts&&... xs)
{
return overloader<decay_t<Ts>...>{forward<Ts>(xs)...};
}
int main()
{
auto o = overload([](char){ cout << "CHAR"; },
[](int) { cout << "INT"; });
o('a'); // prints "CHAR"
o(0); // prints "INT"
}
Run Code Online (Sandbox Code Playgroud)
由于上面的overloader继承Ts...,它需要复制或移动函数对象才能工作.我想要一些提供相同重载行为的东西,但只引用传递的函数对象.
我们称之为假设函数ref_overload(fs...).我的尝试正在使用std::reference_wrapper,std::ref如下:
template <typename... Ts>
auto ref_overload(Ts&... xs)
{
return overloader<reference_wrapper<Ts>...>{ref(xs)...};
}
Run Code Online (Sandbox Code Playgroud)
看起来很简单吧?
int main()
{
auto l0 = [](char){ cout << "CHAR"; };
auto l1 = [](int) { cout << "INT"; };
auto o = ref_overload(l0, l1);
o('a'); // BOOM
o(0);
}
Run Code Online (Sandbox Code Playgroud)
error: call of '(overloader<...>) (char)' is ambiguous
o('a'); // BOOM
^
Run Code Online (Sandbox Code Playgroud)
它不起作用的原因很简单:std::reference_wrapper::operator()是一个可变参数函数模板,它不能很好地与重载一起使用.
为了使用using Ts::operator()...语法,我需要Ts...满足FunctionObject.如果我尝试制作自己的FunctionObject包装器,我会遇到同样的问题:
template <typename TF>
struct function_ref
{
TF& _f;
decltype(auto) operator()(/* ??? */);
};
Run Code Online (Sandbox Code Playgroud)
由于没有办法表达"编译器,请填写???与"完全相同的参数TF::operator()",我需要使用可变参数函数模板,什么都不解决.
我也不能使用这样的东西,boost::function_traits因为传递给overload(...)它的一个函数可能是一个函数模板或一个重载的函数对象本身!
因此我的问题是:有没有一种方法来实现一个ref_overload(fs...)函数,给定任意数量的fs...函数对象,返回一个新的函数对象,其行为类似于重载fs...,但是指的是fs...不是复制/移动它们?
bog*_*dan 28
好吧,这是计划:我们将确定哪个函数对象包含operator()将在我们使用基于继承和使用声明的简单重载程序时选择的重载,如问题所示.我们将通过强制隐式对象参数的派生到基础转换中的歧义来实现(在未评估的上下文中),这会在重载解析成功之后发生.此行为在标准中指定,请参阅N4659 [namespace.udecl]/16和18.
基本上,我们将依次添加每个函数对象作为附加的基类子对象.对于重载解析成功的调用,为任何不包含获胜重载的函数对象创建基本模糊不会改变任何内容(调用仍将成功).但是,对于复制的基础包含所选重载的情况,调用将失败.这为我们提供了SFINAE环境.然后,我们通过相应的参考转发呼叫.
#include <cstddef>
#include <type_traits>
#include <tuple>
#include <iostream>
template<class... Ts>
struct ref_overloader
{
static_assert(sizeof...(Ts) > 1, "what are you overloading?");
ref_overloader(Ts&... ts) : refs{ts...} { }
std::tuple<Ts&...> refs;
template<class... Us>
decltype(auto) operator()(Us&&... us)
{
constexpr bool checks[] = {over_fails<Ts, pack<Us...>>::value...};
static_assert(over_succeeds(checks), "overload resolution failure");
return std::get<choose_obj(checks)>(refs)(std::forward<Us>(us)...);
}
private:
template<class...>
struct pack { };
template<int Tag, class U>
struct over_base : U { };
template<int Tag, class... Us>
struct over_base<Tag, ref_overloader<Us...>> : Us...
{
using Us::operator()...; // allow composition
};
template<class U>
using add_base = over_base<1,
ref_overloader<
over_base<2, U>,
over_base<1, Ts>...
>
>&; // final & makes declval an lvalue
template<class U, class P, class V = void>
struct over_fails : std::true_type { };
template<class U, class... Us>
struct over_fails<U, pack<Us...>,
std::void_t<decltype(
std::declval<add_base<U>>()(std::declval<Us>()...)
)>> : std::false_type
{
};
// For a call for which overload resolution would normally succeed,
// only one check must indicate failure.
static constexpr bool over_succeeds(const bool (& checks)[sizeof...(Ts)])
{
return !(checks[0] && checks[1]);
}
static constexpr std::size_t choose_obj(const bool (& checks)[sizeof...(Ts)])
{
for(std::size_t i = 0; i < sizeof...(Ts); ++i)
if(checks[i]) return i;
throw "something's wrong with overload resolution here";
}
};
template<class... Ts> auto ref_overload(Ts&... ts)
{
return ref_overloader<Ts...>{ts...};
}
// quick test; Barry's example is a very good one
struct A { template <class T> void operator()(T) { std::cout << "A\n"; } };
struct B { template <class T> void operator()(T*) { std::cout << "B\n"; } };
int main()
{
A a;
B b;
auto c = [](int*) { std::cout << "C\n"; };
auto d = [](int*) mutable { std::cout << "D\n"; };
auto e = [](char*) mutable { std::cout << "E\n"; };
int* p = nullptr;
auto ro1 = ref_overload(a, b);
ro1(p); // B
ref_overload(a, b, c)(p); // B, because the lambda's operator() is const
ref_overload(a, b, d)(p); // D
// composition
ref_overload(ro1, d)(p); // D
ref_overload(ro1, e)(p); // B
}
Run Code Online (Sandbox Code Playgroud)
注意事项:
ref_overloader被打包到其组成的功能对象中,以便它们operator()参与重载决策而不是转发operator().试图编写ref_overloaders的任何其他overloader 显然会失败,除非它做类似的事情.一些有用的部分:
add_base:over_basefor 的部分特ref_overloader化做上面提到的"解包"使ref_overloaders包含其他ref_overloaders.有了它,我只是重新使用它来构建add_base,这是一个黑客,我承认.add_base是真的意思是inheritance_overloader<over_base<2, U>, over_base<1, Ts>...>,但我不想定义另一个做同样事情的模板.关于那个奇怪的测试over_succeeds:逻辑是如果重载解析在正常情况下会失败(没有添加模糊的基础),那么对于所有"已检测"的情况它也会失败,无论添加什么基数,所以checks数组会仅包含true元素.相反,如果重载解析对于正常情况会成功,那么除了一个之外,所有其他情况也会成功,因此checks将包含一个true其他所有等于的元素false.
鉴于值中的这种一致性checks,我们可以只查看前两个元素:如果两者都是true,则表示正常情况下的重载解析失败; 所有其他组合表明解决方案成功.这是懒惰的解决方案; 在生产实现中,我可能会进行全面测试以验证是否checks真的包含预期的配置.
Bar*_*rry 10
在一般情况下,即使在C++ 17中,我也不认为这样的事情是可能的.考虑最令人讨厌的案例:
struct A {
template <class T> int operator()(T );
} a;
struct B {
template <class T> int operator()(T* );
} b;
ref_overload(a, b)(new int);
Run Code Online (Sandbox Code Playgroud)
你怎么可能做那个工作?我们可以检查这两种类型是否可调用int*,但两者operator()都是模板,因此我们无法选择其签名.即使我们可以,推导出的参数本身也是相同的 - 两个函数都需要int*.你怎么知道打电话b?
为了使这种情况正确,你基本上需要做的是将返回类型注入到调用操作符中.如果我们可以创建类型:
struct A' {
template <class T> index_<0> operator()(T );
};
struct B' {
template <class T> index_<1> operator()(T* );
};
Run Code Online (Sandbox Code Playgroud)
然后我们可以decltype(overload(declval<A'>(), declval<B'>()))::value用来选择自称的引用.
在最简单的情况下 - 当两者A和B(和C......)都有一个operator()不是模板的单一时,这是可行的 - 因为我们实际上可以检查&X::operator()和操作这些签名以生成我们需要的新签名.这允许我们仍然使用编译器为我们做重载解析.
我们还可以检查什么类型的overload(declval<A>(), declval<B>(), ...)(args...)产量.如果最佳匹配的返回类型几乎是所有可行的候选者都是唯一的,我们仍然可以选择正确的重载ref_overload.这将为我们提供更多的基础,因为我们现在可以正确处理一些带有重载或模板化调用操作符的情况,但是我们会错误地拒绝许多不明确的调用.
但是为了解决一般问题,对于具有相同返回类型的重载或模板化调用操作符的类型,我们需要更多内容.我们需要一些未来的语言功能.
完全反射将允许我们注入如上所述的返回类型.我不知道具体是什么,但我期待看到Yakk的实施.
另一种潜在的未来解决方案是使用过载operator ..第4.12节包含一个示例,表明该设计允许通过不同operator.()的名称通过名称重载不同的成员函数.如果该提议今天以类似的形式传递,那么实现引用重载将遵循与今天的对象重载相同的模式,只需用不同的operator .()s代替今天的不同operator ()s:
template <class T>
struct ref_overload_one {
T& operator.() { return r; }
T& r;
};
template <class... Ts>
struct ref_overloader : ref_overload_one<Ts>...
{
ref_overloader(Ts&... ts)
: ref_overload_one<Ts>{ts}...
{ }
using ref_overload_one<Ts>::operator....; // intriguing syntax?
};
Run Code Online (Sandbox Code Playgroud)