Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overloading multiple function objects by reference

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

live example on wandbox


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.

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       ^ 

live example on wandbox

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

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()(/* ??? */); }; 

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!

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?

like image 548
Vittorio Romeo Avatar asked Mar 23 '17 21:03

Vittorio Romeo


People also ask

Why do we pass by reference for operator overloading?

Advantages of passing by reference: It allows us to have the function change the value of the argument, which is sometimes useful. Because a copy of the argument is not made, it is fast, even when used with large structs or classes. We can pass by const reference to avoid unintentional changes.

What is multiple overloading in C++?

C++ allows you to specify more than one definition for a function name or an operator in the same scope, which is called function overloading and operator overloading respectively.

What are the rules of overloading functions?

Function overloading must have: Same function name is used for more than one function definition. Overloaded functions must have different parameters. This difference can be created using a different number of function parameters or parameters of different data types in function definitions.

Can we overload functions in OOP?

Method overloading is a salient feature in Object-Oriented Programming (OOP). It lets you declare the same method multiple times with different argument lists.


1 Answers

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.

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 } 

live example on wandbox


Caveats:

  • 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.

Some useful bits:

  • 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.

    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.


Bug report for GCC, submitted by Vittorio.

Bug report for MSVC.

like image 182
bogdan Avatar answered Sep 29 '22 22:09

bogdan