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

40

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

在 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
      ^

wandbox 上的实时示例

它不能工作的原因很简单: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 ... 而不是复制/移动它们?


6
你所做的事情之所以被认为是“琐碎的”,是因为你可以从那些类型中推导出来,并因此使用using声明将所有那些operator()带入你自己的重载集合中。从而允许C ++编译器为你执行重载决议的工作。一旦你不能再使用这个技巧,你现在就必须手动实现重载决议。那将是…相当棘手的。祝你好运。 - Nicol Bolas
@Yakk 我会选择“绝对不可能”。你如何进行部分排序? - T.C.
5
@T.C. 我不确定。但我知道每年会有2-3次我能够解决我认为在 C++ 中不可能解决的问题。因此,除非我能证明它是不可能的,否则我会保留一些备选方案,并且我会怀疑我的证明。也许这可以通过使用方法指针和手动调度来实现;但很可能不行。我非常怀疑它是可能的,但我不排除这种可能性。 - Yakk - Adam Nevraumont
1
也许在未来... http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0352r0.pdf - metalfox
1
@metalfox 这里有更新的版本:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0352r1.pdf - Danra
显示剩余8条评论
2个回答

30
好的,这是计划:我们要确定哪个函数对象包含了operator()重载,如果我们使用基于继承和使用声明的基本重载器,就像问题中所示。我们将通过在派生到基础转换中为隐式对象参数强制产生歧义(在未评估的上下文中)来完成这一点,在重载分辨率成功之后发生。这种行为在标准中指定,参见N4659 [namespace.udecl]/1618
基本上,我们将逐个添加每个函数对象作为附加的基类子对象。对于重载分辨率成功的调用,创建任何不包含获胜重载的函数对象的基础模棱两可都不会改变任何事情(调用仍将成功)。然而,如果重复的基础包含所选的重载,调用将失败。这给了我们一个可以使用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
}

在wandbox上的实时示例


注意事项:

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

一些有用的内容:

  • 这里有一个很好的简化示例,由Vittorio提供,展示了模棱两可的基本概念。
  • 关于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的错误报告


@bogdan:你在add_baseover_base中使用ref_overloader的原因是什么?那里不用pack就可以吗? - Vittorio Romeo
7
很高兴被证明是错的 :) 现在我只是在等待Vittorio授予他的赏金,这样我就可以附加上我自己的... - T.C.
这太聪明了,向你致敬。唯一的问题是——相比其他部分,它一定非常简单,但是 over_succeeds 如何只用一个检查就能完成呢? - Quentin
@Quentin 一部分是合理的假设,一部分是懒惰 : -)。好问题;我已经在答案中添加了解释。 - bogdan
8
“证明 T.C. 错了” - 我相信标准中有一个段落说明这样的陈述是不合规范的 NDR :-). 公平地说,这个解决方案避开了你在评论中与 Yakk 讨论的问题,所以我认为你对自己太苛刻了。开玩笑的话,你的举动对我来说意义重大;谢谢。 - bogdan
显示剩余7条评论

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);

你怎么可能让那个起作用?我们可以检查两种类型是否都可以使用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?
};

好的回答。我本来希望得到一个疯狂聪明的解决方案,但是你的解释和例子让我相信目前不可能实现。如果提及并举例说明一种可能的operator.重载解决方案,这个答案会更好,我很可能会授予你奖励(除非有人神奇地想出了其他方法)。 - Vittorio Romeo
@VittorioRomeo 是的,Stroustrup的operator.()表示它应该通过多个operator.()支持成员名称的重载。我不知道该论文的状态或其相对采用反射解决方案的可能性。我不认为Hubert或Faisal的替代方案支持此功能,但我不确定。 - Barry

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接