In C++17, it is trivial to implement an overload(fs...)
function that, given any number of arguments fs...
satisfying FunctionObject, returns a new
In the general case, I don't think such a thing is possible even in C++17. Consider the most obnoxious case:
struct A {
template <class T> int operator()(T );
} a;
struct B {
template <class T> int operator()(T* );
} b;
ref_overload(a, b)(new int);
How could you possibly make that work? We could check that both types are callable with int*
, but both operator()
s are templates so we can't pick out their signatures. Even if we could, the deduced parameters themselves are identical - both functions take an int*
. How would you know to call b
?
In order to get this case correct, what you'd basically need to do is to inject return type into the call operators. If we could create types:
struct A' {
template <class T> index_<0> operator()(T );
};
struct B' {
template <class T> index_<1> operator()(T* );
};
Then we could use decltype(overload(declval<A'>(), declval<B'>()))::value
to pick which reference to call ourselves.
In the simplest case - when both A
and B
(and C
and ...) have one single operator()
that is not a template, this is doable - since we can actually inspect &X::operator()
and manipulate those signatures to produce the new ones that we need. This allows us to still use the compiler to do overload resolution for us.
We can also check what type overload(declval<A>(), declval<B>(), ...)(args...)
yields. If the best match's return type is unique almost all the viable candidates, we can still pick the correct overload in ref_overload
. This will cover more ground for us, as we can now correctly handle some cases with overloaded or templated call operators, but we will incorrectly reject many calls as ambiguous that aren't.
But in order to solve the general problem, with types that have overloaded or templated call operators with the same return type, we need something more. We need some future language features.
Full reflection would allow us to inject a return type as described above. I don't know what that would look like specifically, but I look forward to seeing Yakk's implementation of it.
An alternative potential future solution would be to use overloaded operator .. Section 4.12 includes an example that indicates that the design allows for overloading of different member functions by name through different operator.()
s. If that proposal passes in some similar form today, then implementing reference-overloading would follow the same pattern as object-overloading today, just substituting different operator .()
s for today's different 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?
};
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:
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_overloader
s will obviously fail unless it does something similar.Some useful bits:
add_base
: the partial specialization of over_base
for ref_overloader
does the "unwrapping" mentioned above to enable ref_overloader
s containing other ref_overloader
s. 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.