通过引用重载多个功能对象 [英] Overloading multiple function objects by reference

查看:134
本文介绍了通过引用重载多个功能对象的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在C ++ 17中,实现overload(fs...)函数非常简单,该函数在给定任意数量fs...满足

In C++17, it is trivial to implement an overload(fs...) function that, given any number of arguments fs... satisfying FunctionObject, returns a new function object that behaves like an overload of fs.... Example:

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...,因此它需要复制或移动功能对象才能起作用. 我想要提供相同重载行为的东西,但只引用传递的函数对象.

Since the above overloader inherits from Ts..., it needs to either copy or move the function objects in order to work. I want something that provides the same overloading behavior, but only references to the passed function objects.

我们叫该假设函数ref_overload(fs...).我的尝试是按如下方式使用std::reference_wrapperstd::ref:

Let's call that hypothetical function ref_overload(fs...). My attempt was using std::reference_wrapper and std::ref as follows:

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

看起来很简单,对吧?

Seems simple enough, right?

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
      ^

魔盒上的在线示例

不起作用的原因很简单: variadic函数模板在重载时效果不佳.

The reason it doesn't work is simple: std::reference_wrapper::operator() is a variadic function template, which does not play nicely with overloading.

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

In order to use the using Ts::operator()... syntax, I need Ts... to satisfy FunctionObject. If I try to make my own FunctionObject wrapper, I encounter the same issue:

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

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

Since there's no way of expressing "compiler, please fill the ??? with the exact same arguments as TF::operator()", I need to use a variadic function template, solving nothing.

我也不能使用

I also cannot use something like boost::function_traits because one of the functions passed to overload(...) may be a function template or an overloaded function object itself!

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

Therefore my question is: is there a way of implementing a ref_overload(fs...) function that, given any number of fs... function objects, returns a new function object that behaves like an overload of fs..., but refers to fs... instead of copying/moving them?

推荐答案

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

All right, here's the plan: we're going to determine which function object contains the operator() overload that would be chosen if we used a bare-bones overloader based on inheritance and using declarations, as illustrated in the question. We're going to do that (in an unevaluated context) by forcing an ambiguity in the derived-to-base conversion for the implicit object parameter, which happens after overload resolution succeeds. This behaviour is specified in the standard, see N4659 [namespace.udecl]/16 and 18.

基本上,我们将依次添加每个函数对象作为附加的基类子对象.对于成功解决重载的调用,为不包含获胜的重载的任何函数对象创建基本歧义不会更改任何内容(该调用仍将成功).但是,对于重复的基元包含所选重载的情况,调用将失败.这为我们提供了一个SFINAE环境.然后,我们通过相应的参考转发呼叫.

Basically, we're going to add each function object in turn as an additional base class subobject. For a call for which overload resolution succeeds, creating a base ambiguity for any of the function objects that don't contain the winning overload won't change anything (the call will still succeed). However, the call will fail for the case where the duplicated base contains the chosen overload. This gives us a SFINAE context to work with. We then forward the call through the corresponding reference.

#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
}

在魔盒上的实时示例

注意事项:

  • 我们假设,即使我们不希望基于继承的重载程序,也可以根据需要从这些函数对象中继承.没有创建这样的派生对象,但是在未评估的上下文中进行的检查依赖于这种可能性.我想不出任何其他方法将这些重载带入相同的作用域,以便可以将重载解决方案应用于它们.
  • 我们假设对于呼叫的参数,转发正确.鉴于我们拥有对目标对象的引用,因此我不知道如果没有某种转发就无法实现此目的,因此这似乎是一项强制性要求.
  • 当前在Clang上有效.对于GCC,看起来我们依赖的派生基础转换不是SFINAE上下文,因此会触发硬错误.据我所知,这是不正确的. MSVC非常有用,它消除了对我们的要求:看起来它只是选择了碰巧排在最前面的基类子对象;在那里,它有效-不喜欢什么? (MSVC目前与我们的问题不太相关,因为它也不支持其他C ++ 17功能).
  • 组合通过一些特殊的预防措施进行工作-在测试假设的基于继承的重载程序时,ref_overloader被展开到其组成函数对象中,以便其operator()参与重载解析,而不是转发operator().除非它做类似的事情,否则任何其他试图组成ref_overloader的重载程序显然都将失败.
  • We're assuming that, even though we don't want an overloader based on inheritance, we could inherit from those function objects if we wanted to. No such derived object is created, but the checks done in unevaluated contexts rely on this being possible. I can't think of any other way to bring those overloads into the same scope so that overload resolution can be applied to them.
  • We're assuming that forwarding works correctly for the arguments to the call. Given that we hold references to the target objects, I don't see how this could work without some kind of forwarding, so this seems like a mandatory requirement.
  • This currently works on Clang. For GCC, it looks like the derived-to-base conversion we're relying on is not a SFINAE context, so it triggers a hard error; this is incorrect as far as I can tell. MSVC is super helpful and disambiguates the call for us: it looks like it just chooses the base class subobject that happens to come first; there, it works - what's not to like? (MSVC is less relevant to our problem at the moment, since it doesn't support other C++17 features either).
  • Composition works through some special precautions - when testing the hypothetical inheritance based overloader, a ref_overloader is unwrapped into its constituent function objects, so that their operator()s participate in overload resolution instead of the forwarding operator(). Any other overloader attempting to compose ref_overloaders will obviously fail unless it does something similar.

一些有用的位:

  • A nice simplified example by Vittorio showing the ambiguous base idea in action.
  • About the implementation of add_base: the partial specialization of over_base for ref_overloader does the "unwrapping" mentioned above to enable ref_overloaders containing other ref_overloaders. With that in place, I just reused it to build add_base, which is a bit of a hack, I'll admit. add_base is really meant to be something like inheritance_overloader<over_base<2, U>, over_base<1, Ts>...>, but I didn't want to define another template that would do the same thing.
  • About that strange test in over_succeeds: the logic is that if overload resolution would fail for the normal case (no ambiguous base added), then it would also fail for all the "instrumented" cases, regardless of what base is added, so the checks array would contain only true elements. Conversely, if overload resolution would succeed for the normal case, then it would also succeed for all the other cases except one, so checks would contain one true element with all the others equal to false.

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

Given this uniformity in the values in checks, we can look at just the first two elements: if both are true, this indicates overload resolution failure in the normal case; all the other combinations indicate resolution success. This is the lazy solution; in a production implementation, I would probably go for a comprehensive test to verify that checks really contains an expected configuration.

GCC的错误报告,由Vittorio .

错误报告用于MSVC .

这篇关于通过引用重载多个功能对象的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆