Yes concepts
are designed for this purpose. If a sent parameter doesn't meet the required concept argument the function would not be considered in the overload resolution list, thus avoiding ambiguity.
Moreover, if a sent parameter meets several functions, the more specific one would be selected.
Simple example:
void print(auto t) {
std::cout << t << std::endl;
}
void print(std::integral auto i) {
std::cout << "integral: " << i << std::endl;
}
Above print
functions are a valid overloading that can live together.
- If we send a non integral type it will pick the first
- If we send an integral type it will prefer the second
e.g., calling the functions:
print("hello"); // calls print(auto)
print(7); // calls print(std::integral auto)
No ambiguity -- the two functions can perfectly live together, side-by-side.
No need for any SFINAE code, such as enable_if
-- it is applied already (hidden very nicely).
Picking between two concepts
The example above presents how the compiler prefers constrained type (std::integral auto) over an unconstrained type (just auto). But the rules also apply to two competing concepts. The compiler should pick the more specific one, if one is more specific. Of course if both concepts are met and none of them is more specific this will result with ambiguity.
Well, what makes a concept be more specific? if it is based on the other one1.
The generic concept - GenericTwople:
template<class P>
concept GenericTwople = requires(P p) {
requires std::tuple_size<P>::value == 2;
std::get<0>(p);
std::get<1>(p);
};
The more specific concept - Twople:
class Any;
template<class Me, class TestAgainst>
concept type_matches =
std::same_as<TestAgainst, Any> ||
std::same_as<Me, TestAgainst> ||
std::derived_from<Me, TestAgainst>;
template<class P, class First, class Second>
concept Twople =
GenericTwople<P> && // <= note this line
type_matches<std::tuple_element_t<0, P>, First> &&
type_matches<std::tuple_element_t<1, P>, Second>;
Note that Twople is required to meet GenericTwople requirements, thus it is more specific.
If you replace in our Twople the line:
GenericTwople<P> && // <= note this line
with the actual requirements that this line brings, Twople would still have the same requirements but it will no longer be more specific than GenericTwople. This, along with code reuse of course, is why we prefer to define Twople based on GenericTwople.
Now we can play with all sort of overloads:
void print(auto t) {
cout << t << endl;
}
void print(const GenericTwople auto& p) {
cout << "GenericTwople: " << std::get<0>(p) << ", " << std::get<1>(p) << endl;
}
void print(const Twople<int, int> auto& p) {
cout << "{int, int}: " << std::get<0>(p) << ", " << std::get<1>(p) << endl;
}
And call it with:
print(std::tuple{1, 2}); // goes to print(Twople<int, int>)
print(std::tuple{1, "two"}); // goes to print(GenericTwople)
print(std::pair{"three", 4}); // goes to print(GenericTwople)
print(std::array{5, 6}); // goes to print(Twople<int, int>)
print("hello"); // goes to print(auto)
We can go further, as the Twople concept presented above works also with polymorphism:
struct A{
virtual ~A() = default;
virtual std::ostream& print(std::ostream& out = std::cout) const {
return out << "A";
}
friend std::ostream& operator<<(std::ostream& out, const A& a) {
return a.print(out);
}
};
struct B: A{
std::ostream& print(std::ostream& out = std::cout) const override {
return out << "B";
}
};
add the following overload:
void print(const Twople<A, A> auto& p) {
cout << "{A, A}: " << std::get<0>(p) << ", " << std::get<1>(p) << endl;
}
and call it (while all the other overloads are still present) with:
print(std::pair{B{}, A{}}); // calls the specific print(Twople<A, A>)
Code: https://godbolt.org/z/3-O1Gz
Unfortunately C++20 doesn't allow concept specialization, otherwise we would go even further, with:
template<class P>
concept Twople<P, Any, Any> = GenericTwople<P>;
Which could add a nice possible answer to this SO question, however concept specialization is not allowed.
1 The actual rules for Partial Ordering of Constraints are more complicated, see: cppreference / C++20 spec.