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

在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上的实例


由于上面的overloaderinheritance自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()(/* ??? */); }; 

由于没有办法expression“编译器,请填写与TF::operator()完全相同的参数” ,我需要使用可变参数函数模板 ,不需要任何解决方法。

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

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

好的,这里的计划是:我们要确定哪个函数对象包含operator()重载,如果我们使用基于inheritance和使用声明的裸机重载器,将会被select。 我们将这样做(在未评估的上下文中),通过强制隐式对象参数的派生到基础转换中的含糊不清,这在重载parsing成功后发生。 该行为在标准中指定,请参阅N4659 [namespace.udecl] / 16和18 。

基本上,我们将依次添加每个函数对象作为附加的基类子对象。 对于重载parsing成功的调用,为任何不包含获胜超载的函数对象创build一个基本的含糊不会改变任何东西(调用仍将成功)。 但是,如果重复的基址包含所选的过载,则调用将失败。 这给了我们一个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上的现场示例


注意事项:

  • 我们假设,即使我们不想要一个基于inheritance的重载器,如果我们愿意,我们也可以inheritance这些函数对象。 没有这样的派生对象被创build,但在未评估的上下文中进行的检查依赖于这是可能的。 我想不出有什么其他办法可以将这些超载问题纳入相同的范围,以便可以对它们应用超载解决scheme。
  • 我们假设转发对于调用的参数是正确的。 考虑到我们持有对目标对象的引用,我不明白如果没有某种转发,这是如何工作的,所以这似乎是一个强制性的要求。
  • 目前这个工作在Clang上。 对于GCC,它看起来像我们依赖的派生到基础的转换不是一个SFINAE上下文,所以它触发了一个硬错误; 据我所知,这是不正确的。 MSVC是超级有用的,消除了我们的呼吁:它看起来只是select恰好来到的基类子对象; 那里,它的工作 – 什么是不喜欢? (MSVC与我们目前的问题不太相关,因为它不支持其他C ++ 17function)。
  • 组合通过一些特殊的预防措施工作 – 当testing基于假想inheritance的overloader时,一个ref_overloader被解包到它的组成函数对象中,以便它们的operator()参与重载parsing而不是转发operator() 。 试图ref_overloader的任何其他的overloader显然会失败,除非它做了类似的事情。

一些有用的位:

  • 维托里奥的 一个很好的简单的例子显示了在行动中模棱两可的基本思想。
  • 关于add_base的实现: add_base的部分over_base ref_overloader了上面提到的“unwrapping”,以启用包含其他ref_overloader 。 有了这个,我只是重用它来build立add_base ,这是一个黑客,我承认。 add_base实际上意味着像inheritance_overloader<over_base<2, U>, over_base<1, Ts>...> ,但我不想定义另一个模板来做同样的事情。
  • 关于over_succeeds那个奇怪的testing:逻辑是,如果重载解决scheme在正常情况下(不添加任何含糊不清的基础)失败,那么无论添加了什么base,所有“instrumented”情况下都会失败,数组只包含true元素。 相反,如果重载parsing对于正常情况是成功的,那么除了一个以外的其他所有情况也会成功,所以checks将包含一个true元素,其他所有其他情况等于false

    考虑到checks值的一致性,我们可以只看前两个元素:如果两者都是true ,则表示正常情况下的超负荷分辨率失败; 所有其他组合表明解决scheme成功。 这是懒惰的解决scheme; 在生产实现中,我可能会进行全面的testing来validationchecks确实包含预期的configuration。


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

你怎么可能做这个工作? 我们可以检查这两种types是可以用int*调用的,但是这两个operator()都是模板,所以我们不能挑出它们的签名。 即使我们可以,推导的参数本身也是相同的 – 两个函数都是int* 。 你怎么知道给b打电话?

为了使这个案例正确,你基本上需要做的是将返回types注入到调用操作符中。 如果我们可以创buildtypes:

 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来select自己调用哪个引用。

最简单的情况下 – 当AB (以及C和…)都有一个不是模板的单个operator() ,这是可行的 – 因为我们可以实际检查&X::operator()并操纵这些签名生产我们需要的新产品。 这使我们仍然可以使用编译器为我们做重载parsing。

我们也可以检查什么types的overload(declval<A>(), declval<B>(), ...)(args...) 。 如果最佳匹配的返回types几乎是唯一可行的候选者,那么我们仍然可以在ref_overloadselect正确的重载。 这将为我们提供更多的理由,因为我们现在可以正确地处理一些重载或模板调用操作符的情况,但是我们会错误地拒绝许多不确定的调用。


但是为了解决一般问题,对于重载的types或者具有相同返回types的模板调用操作符,我们需要更多的东西。 我们需要一些未来的语言function。

完全reflection将允许我们注入如上所述的返回types。 我不知道具体是什么样的,但我期待看到雅克的实施。

另一种可能的未来解决scheme是使用重载operator . 。 4.12节包含一个例子,表明这个devise允许通过不同的operator.()通过名字来重载不同的成员函数。 如果这个提议今天以某种相似的forms通过,那么实现引用重载将遵循与当前对象重载相同的模式,只是用不同的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? };