The following code fails in static_assert:
#include <iostream>
#include <concepts>
#include <vector>
template <typename T>
concept container_type = requires(T& t) {
typename T::value_type;
typename T::reference;
typename T::iterator;
{ t.begin() } -> std::same_as<typename T::iterator>;
{ t.end() } -> std::same_as<typename T::iterator>;
};
// overload (0), fallback case
template <typename T>
constexpr int foo(const T& o) {
return 0;
}
// overload (1), using the concept
constexpr int foo(const container_type auto& o) {
return 1;
}
// overload (2), using template template parametar
template <typename T, template <typename ...> typename C >
constexpr int foo(const C<T>& o) {
return 2;
}
int main()
{
std::vector<int> a {1,3,3,44,55,5,66,76};
static_assert(foo(a) == 1);
return 0;
}
Compiler used is GCC on https://coliru.stacked-crooked.com, command line is g -std=c 20 -O2 -Wall -pedantic -pthread
.
I would expect that the overload (1) should be used as the best match, but overload (2) is used instead.
Is this standard conformant behavior?
CodePudding user response:
Constraints are considered for the purpose of choosing between two viable overloads only if there is no other tie breaker between the overloads, considered as unconstrained. In particular that means that if one is more specialized by the old partial ordering rules for function templates, ignoring constraints, then that will be chosen and constraints are not relevant.
Basically the constraints only matter when comparing function templates that are, except for the constraints, exactly equivalent in terms of their function parameters.
Leaving out the constraint (which is satisfied with the foo(a)
call in main
) your first overload looks like
constexpr int foo(const auto& o) {
return 1;
}
Now if you compare this to the second overload I think it should be clear that by the usual partial ordering rules for function templates this is less specialized than the second overload. As a consequence the second overload is chosen.
Even if you recast the conditions under which the second overload is valid into a concept Overload2Concept
and then use
constexpr int foo(const Overload2Concept auto& o) {
return 2;
}
instead, it won't work, because the constraints of the two overloads are not subsets of one another. You can have a container_type
per your definition which is not a specialization of a template and you can have a specialization of a template template <typename ...> class C;
which is not a container_type
.
So in that case neither would be more specialized than the other, neither by old partial ordering rules nor by new constraint ordering rules, and consequently overload resolution would be ambiguous.
Having concepts/constraints integrated into overload resolution in this way guarantees that adding additional overloads with constraints to pre-C 20 code won't cause any break in the way overloads were chosen beforehand. Were constraints allowed to overrule the old partial ordering in the way you seem to expect, then it would be dangerous to modify any pre-C 20 overload set of functions with constraints.
The following overload would be preferred over overload (2) because it is more specialized per constraints and the unconstrained equivalents identical:
template <typename T, template <typename ...> typename C >
requires container_type<C<T>>
constexpr int foo(const C<T>& o) {
return 1;
}