Per cppreference, the partial ordering of constraints over subsumption is used to determine "the best match for a template template argument". And in the example section, the option that is "more constrained", i.e. the stronger/tighter, condition is selected. But in practice I've found some confusing behavior when the functions concept constraints are clearly ordered and specific invocation can clearly distinguishes the strongest option.
I'm testing the following on gcc version 10.2.0 (Homebrew GCC 10.2.0): With:
template <typename... Ts>
void foo (Ts... ts) // option 1
{
(cout << ... << ts) << endl;
}
template <typename... Ts> requires (sizeof...(Ts)<=3)
void foo (Ts... ts) // option 2
{
(cout << ... << ts) << endl;
}
A call of foo(1,2)
would select option 2 since its constraints are clearly stronger than option 1. On the other hand, this would clearly cause ambiguity:
template <typename... Ts>
void foo (Ts... ts) // option 1
{
(cout << ... << ts) << endl;
}
template <typename... Ts> requires (sizeof...(Ts)<=3)
void foo (Ts... ts) // option 2
{
(cout << ... << ts) << endl;
}
template <typename... Ts> requires (same_as<Ts, int> && ...)
void foo (Ts... ts) // option 3
{
(cout << ... << ts) << endl;
}
since a call of foo(1,2)
cannot decide whether to choose option 2 or option 3 as they are incomparable. Now, if I understand it correctly, by adding a conjunctive case like:
template <typename... Ts>
void foo (Ts... ts) // option 1
{
(cout << ... << ts) << endl;
}
template <typename... Ts> requires (sizeof...(Ts)<=3)
void foo (Ts... ts) // option 2
{
(cout << ... << ts) << endl;
}
template <typename... Ts> requires (same_as<Ts, int> && ...)
void foo (Ts... ts) // option 3
{
(cout << ... << ts) << endl;
}
template <typename... Ts> requires (sizeof...(Ts)<=3) && (same_as<Ts, int> && ...)
void foo (Ts... ts) // option 4
{
(cout << ... << ts) << endl;
}
Calling foo(1,2)
should resolve with option 4, but my compiler says otherwise:
In function 'int main()':
error: call of overloaded 'foo(int, int)' is ambiguous
| foo(1,2);
| ^
note: candidate: 'void foo(Ts ...) [with Ts = {int, int}]'
| void foo (Ts... ts) // option 1
| ^~~
note: candidate: 'void foo(Ts ...) [with Ts = {int, int}]'
| void foo (Ts... ts) // option 2
| ^~~
note: candidate: 'void foo(Ts ...) [with Ts = {int, int}]'
| void foo (Ts... ts) // option 3
| ^~~
note: candidate: 'void foo(Ts ...) [with Ts = {int, int}]'
| void foo (Ts... ts) // option 4
Why is that? And if it is inevitable, is there any way around this?