通过引用重载多个函数对象

Overloading multiple function objects by reference

本文关键字:函数 对象 重载 引用      更新时间:2023-10-16

在 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"
}

魔杖盒上的现场示例


由于上述overloader继承自Ts...,因此需要复制或移动函数对象才能工作。我想要一些提供相同重载行为的东西,但只引用传递的函数对象。

我们称该假设函数为ref_overload(fs...)。我的尝试是使用std::reference_wrapperstd::ref如下:

template <typename... Ts>
auto ref_overload(Ts&... xs)
{
return overloader<reference_wrapper<Ts>...>{ref(xs)...};
}

看起来很简单,对吧?

int main()
{
auto l0 = [](char){ cout << "CHAR"; };
auto l1 = [](int) { cout << "INT";  };
auto o = ref_overload(l0, l1);
o('a'); // BOOM
o(0);
}

error: call of '(overloader<...>) (char)' is ambiguous
o('a'); // BOOM
^

魔杖盒上的现场示例

它不起作用的原因很简单:std::reference_wrapper::operator()是一个可变参数函数模板,它不能很好地与重载配合使用。

为了使用using Ts::operator()...语法,我需要Ts...来满足FunctionObject。如果我尝试制作自己的FunctionObject包装器,我会遇到同样的问题:

template <typename TF>
struct function_ref
{
TF& _f;
decltype(auto) operator()(/* ??? */);
};

由于无法表达"编译器,请用与TF::operator()完全相同的参数填充???">,因此我需要使用可变参数函数模板,什么也解决不了。

我也不能使用类似boost::function_traits的东西,因为传递给overload(...)的函数之一可能是函数模板重载的函数对象本身!

因此,我的问题是:有没有一种方法可以实现一个ref_overload(fs...)函数,给定任意数量的fs...函数对象,返回一个新的函数对象,该对象的行为类似于fs...的重载,但引用fs...而不是复制/移动它们?

好的,这是计划:我们将确定哪个函数对象包含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 << "An"; } };
struct B { template <class T> void operator()(T*) { std::cout << "Bn"; } };
int main()
{
A a;
B b;
auto c = [](int*) { std::cout << "Cn"; };
auto d = [](int*) mutable { std::cout << "Dn"; };
auto e = [](char*) mutable { std::cout << "En"; };
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
}

魔杖盒上的现场示例


警告:

  • 我们假设,即使我们不想要基于继承的重载器,如果我们愿意,我们也可以从这些函数对象继承。不会创建这样的派生对象,但在未计算的上下文中执行的检查依赖于这是可能的。我想不出任何其他方法将这些重载带入同一范围,以便可以将重载解决方案应用于它们。
  • 我们假设转发对调用的参数正常工作。鉴于我们持有对目标对象的引用,我不明白如果没有某种转发,这如何工作,所以这似乎是一个强制性要求。
  • 这目前适用于 Clang。对于 GCC,看起来我们依赖的派生到基础转换不是 SFINAE 上下文,因此它会触发硬错误;据我所知,这是不正确的。MSVC 非常有用,可以消除对我们调用的歧义:看起来它只是选择碰巧排在第一位的基类子对象;在那里,它有效 - 有什么不喜欢的?(MSVC 目前与我们的问题不太相关,因为它也不支持其他 C++17 功能)。
  • 组合通过一些特殊的预防措施工作 - 在测试基于假设继承的重载器时,ref_overloader被解包到其组成函数对象中,以便它们的operator()参与过载解析而不是转发operator()。任何其他尝试编写ref_overloader的重载器显然都会失败,除非它执行类似操作。

一些有用的位:

  • 维托里奥的一个很好的简化示例显示了模棱两可的基本思想。
  • 关于add_base的实现:ref_overloaderover_base的部分专用化执行上述"解包",以使包含其他ref_overloaderref_overloader成为可能。有了这个,我只是重用它来构建add_base,我承认这有点黑客。add_base真的是类似于inheritance_overloader<over_base<2, U>, over_base<1, Ts>...>的东西,但我不想定义另一个可以做同样事情的模板。
  • 关于over_succeeds中的那个奇怪的测试:逻辑是,如果重载解决在正常情况下失败(没有添加模棱两可的基数),那么它也会在所有"检测"情况下失败,无论添加什么基数,因此checks数组将只包含true元素。相反,如果重载解析在正常情况下成功,那么它也会在除一种情况之外的所有其他情况下成功,因此checks将包含一个true元素,所有其他元素都等于false

    鉴于checks值的这种一致性,我们可以只看前两个元素:如果两者都true,则表示在正常情况下过载解析失败;所有其他组合表示解析成功。这是懒惰的解决方案;在生产实现中,我可能会进行全面的测试,以验证checks是否确实包含预期的配置。


GCC 的错误报告,由 Vittorio 提交。

MSVC 的错误报告。

在一般情况下,我认为即使在 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);

你怎么可能做到这一点?我们可以检查这两种类型都可以用int*调用,但两个operator()都是模板,所以我们无法挑选出它们的签名。即使我们可以,推导的参数本身也是相同的 - 两个函数都int*.你怎么知道打电话给b

为了使这种情况正确,您基本上需要做的是将返回类型注入调用运算符。如果我们可以创建类型:

struct A' {
template <class T> index_<0> operator()(T );
};
struct B' {
template <class T> index_<1> operator()(T* );
};

然后我们可以使用decltype(overload(declval<A'>(), declval<B'>()))::value来选择调用自己的引用。

最简单的情况下 - 当AB(以及C和...)都有一个不是模板的operator()时,这是可行的 - 因为我们实际上可以检查&X::operator()并操纵这些签名以生成我们需要的新签名。这允许我们仍然使用编译器为我们进行重载解析。

我们还可以检查什么类型的overload(declval<A>(), declval<B>(), ...)(args...)产量。如果最佳匹配的返回类型几乎在所有可行候选项中都是唯一的,我们仍然可以在ref_overload中选择正确的重载。这将为我们覆盖更多领域,因为我们现在可以正确处理过载或模板化呼叫运营商的某些情况,但我们会错误地拒绝许多不明确的呼叫。


但是为了解决一般问题,对于具有重载的类型或具有相同返回类型的模板化调用运算符,我们需要更多的东西。我们需要一些未来的语言功能。

完全反射将允许我们注入如上所述的返回类型。我不知道具体会是什么样子,但我期待看到Yakk的实现。

另一种潜在的未来解决方案是使用重载operator .。第 4.12 节包含一个示例,该示例指示该设计允许通过不同的operator.()按名称重载不同的成员函数。如果该提案今天以某种类似的形式通过,那么实现引用重载将遵循与今天对象重载相同的模式,只是用不同的operator .()替换今天的不同operator ()

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?
};