Ivan Smirnov Ivan Smirnov - 2 months ago 9
C++ Question

Manually prioritize overloaded functions

I have multiple overloads of the same function. For some set of arguments several overloads fit equally, and I naturally get a "ambiguous overload" error. I want to define priorities to these functions someway such that in case of ambiguity the one with the smallest priority is selected.

I tried to make a series of template helper classes, "tags",

P<0>
,
P<1>
, ..., such that
P<N>
inherits
P<N+1>
with some upper bound. Thus if there are two functions
f(P<2>)
and
f(P<4>)
, and I call
f(P<0>)
, the first one is selected.

It doesn't work in practice. The "clean" example with classic
"int/long" and "long/int" functions still produces an ambiguity. I tried to add several tag parameters to the function to increase the "weight" of a tag, but it does'n help.

Can this approach be tuned somehow?

constexpr static int MAX = 20;

template<int N, class Enable = void>
class P {};

template<int N>
class P<N, typename std::enable_if<N < MAX, void>::type> : public P<N+1> {};

void f(int, long, P<2>) {}
void f(long, int, P<5>) {}

int main() {
f(1, 2, P<0>());
}

Answer

First of all, you can simplify your tag type construction like:

template <int N> struct P : P<N+1> { };
template <> struct P<MAX> { };

Next, the escalator of tag types only works if they are the sole tiebreaker for your overloads. That is, every other argument is equivalent - except the tag argument. The reason your calls are still ambiguous is that the conversion sequences you end up with are:

f(int, long, P<2>); // #1: Exact, Integral Conversion, P<0> -> ... -> P<2>
f(long, int, P<5>); // #2: Integral Conversion, Exact, P<0> -> ... -> P<2> -> ... -> P<5>

One overload is only better than another overload if, for each argument, the conversion sequence is at least as good as the other overload's argument's conversion sequence. That's not true here: #1 is better in the first argument and worse in the second argument than #2. The addition of another argument doesn't change that flaw.

Where the tag types are helpful is if you are using SFINAE with non-disjoint conditions:

template <class T, std::enable_if_t<cond_a<T>::value>* = nullptr>
void f(T&&, P<0> ) { ... }

template <class T, std::enable_if_t<cond_b<T>::value>* = nullptr>
void f(T&&, P<1> ) { ... }

template <class T, std::enable_if_t<cond_c<T>::value>* = nullptr>
void f(T&&, P<2> ) { ... }

f(whatever, P<0>{});

cond_a, cond_b, and cond_c need not be disjoint, and the first argument in every case is the same. So the final tiebreaker, for those overloads that aren't removed by SFINAE, is the tag.