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

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)

wandbox上的实例


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

wandbox上的实例

它不起作用的原因很简单: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]/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
}
Run Code Online (Sandbox Code Playgroud)

wandbox上的实例


注意事项:

  • 我们假设,即使我们不希望基于继承的overloader,我们也可以继承这些函数对象.没有创建这样的派生对象,但是在未评估的上下文中进行的检查依赖于这种可能性.我想不出任何其他方法将这些重载带入同一范围,以便可以对它们应用重载决策.
  • 我们假设转发对于调用的参数正常工作.鉴于我们持有对目标对象的引用,我没有看到如果没有某种转发它可以如何工作,所以这似乎是一个强制性要求.
  • 这目前适用于Clang.对于GCC,看起来我们所依赖的派生到基础转换不是SFINAE上下文,因此它会触发硬错误; 据我所知,这是不正确的.MSVC非常有用并消除了对我们的调用的歧义:看起来它只是选择了碰巧先到的基类子对象; 那里,它有效 - 什么不喜欢?(MSVC目前与我们的问题不太相关,因为它也不支持其他C++ 17功能).
  • 组合通过一些特殊的预防措施 - 当测试基于overloader的假设继承时,a 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真的包含预期的配置.


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

MSVC的错误报告.

  • @TC"证明TC错误" - 我很确定在标准的某个地方有一个段落,这样的声明形成了错误的NDR :-).平心而论,这个解决方案回避了你在评论中与Yakk讨论的问题,所以我认为你对自己太过刻苦.除了笑话,你的举动对我来说意义重大; 谢谢. (7认同)
  • 很高兴被证明是错误的:)现在我只是等待维托里奥奖励他的赏金,以便我可以自己解决... (6认同)

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用来选择自称的引用.

最简单的情况下 - 当两者AB(和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)