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

Overloading multiple function objects by reference

在 C++17 中,实现一个 overload(fs...) 函数很简单,给定任意数量的参数 fs... 满足 FunctionObject,returns 一个新的 函数对象,其行为类似于 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"
}

live example on wandbox


由于上面的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
      ^

live example on wandbox

它不起作用的原因很简单:std::reference_wrapper::operator() 是一个 可变函数模板 不能很好地处理重载.

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

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

由于无法表达"compiler, please fill the ??? with the exact same arguments as TF::operator()",我需要使用可变函数模板,解决不了任何问题。

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

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

在一般情况下,即使在 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?

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

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...) 产生的类型。如果最佳匹配的 return 类型在几乎所有可行的候选中都是唯一的,我们仍然可以在 ref_overload 中选择正确的重载。这将为我们覆盖更多领域,因为我们现在可以正确处理某些使用重载或模板化调用运算符的情况,但我们会错误地拒绝许多不明确的调用。


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

全反射允许我们注入一个 return 类型,如上所述。我不知道具体会是什么样子,但我期待看到 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?
};

好吧,计划如下:我们将确定哪个函数对象包含 operator() 重载,如果我们使用基于继承和 using 声明的基本重载程序,如图所示在问题中。我们将(在未计算的上下文中)通过在隐式对象参数的派生到基转换中强制产生歧义来做到这一点,这发生在重载解析成功之后。此行为在标准中指定,请参阅 N4659 [namespace.udecl]/16 and 18.

基本上,我们将依次添加每个函数对象作为附加基础 class 子对象。对于重载解析成功的调用,为任何不包含获胜重载的函数对象创建基本歧义不会改变任何内容(调用仍会成功)。但是,如果重复的基包含所选的重载,调用将失败。这为我们提供了一个可以使用的 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
}

live example on wandbox


注意事项:

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

一些有用的位:

  • A nice simplified example by Vittorio 展示了模棱两可的基本想法。
  • 关于 add_base 的实现:over_baseref_overloader 的部分特化执行上面提到的 "unwrapping" 以启用 ref_overloader 包含其他 ref_overloaders。有了它,我就重新使用它来构建 add_base,我承认这有点 hack。 add_base 实际上是类似于 inheritance_overloader<over_base<2, U>, over_base<1, Ts>...> 的东西,但我不想定义另一个模板来做同样的事情。
  • 关于 over_succeeds 中那个奇怪的测试:逻辑是如果重载决策在正常情况下会失败(没有添加不明确的基数),那么它也会在所有 "instrumented" 的情况下,不管添加什么基数,所以 checks 数组将只包含 true 个元素。相反,如果重载解析在正常情况下会成功,那么它也会在除一种情况之外的所有其他情况下成功,因此 checks 将包含一个 true 元素,所有其他元素都等于 false.

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


Bug report for GCC, submitted by Vittorio.

Bug report for MSVC.