C++20 Concepts : Which template specialization gets chosen when the template argument qualifies for multiple concepts?
This is because concepts can be more specialized than others, a bit like how template order themselves. This is called partial ordering of constraints
In the case of concepts, they subsumes each other when they include equivalent constraints. For example, here's how std::integral
and std::signed_integral
are implemented:
template<typename T>
concept integral = std::is_integral_v<T>;
template<typename T> // v--------------v---- Using the contraint defined above
concept signed_integral = std::integral<T> && std::is_signed_v<T>;
Normalizing the constraints the compiler boil down the contraint expression to this:
template<typename T>
concept integral = std::is_integral_v<T>;
template<typename T>
concept signed_integral = std::is_integral_v<T> && std::is_signed_v<T>;
In this example, signed_integral
implies integral
completely. So in a sense, a signed integral is "more constrained" than an integral.
The standard writes it like this:
From [temp.func.order]/2 (emphasis mine):
Partial ordering selects which of two function templates is more specialized than the other by transforming each template in turn (see next paragraph) and performing template argument deduction using the function type. The deduction process determines whether one of the templates is more specialized than the other. If so, the more specialized template is the one chosen by the partial ordering process. If both deductions succeed, the partial ordering selects the more constrained template as described by the rules in [temp.constr.order].
That means that if there is multiple possible substitution for a template and both are choosen from partial ordering, it will select the most constrained template.
From [temp.constr.order]/1:
A constraint P subsumes a constraint Q if and only if, for every disjunctive clause Pi in the disjunctive normal form of P, Pi subsumes every conjunctive clause Qj in the conjunctive normal form of Q, where
a disjunctive clause Pi subsumes a conjunctive clause Qj if and only if there exists an atomic constraint Pia in Pi for which there exists an atomic constraint Qjb in Qj such that Pia subsumes Qjb, and
an atomic constraint A subsumes another atomic constraint B if and only if A and B are identical using the rules described in [temp.constr.atomic].
This describe the subsumption algorithm that compiler use to order constraints, and therefore concepts.
C++20 has a mechanism for deciding when one particular constrained entity is "more constrained" than another. This is not a simple thing.
This starts with the concept of breaking a constraint down into its atomic components, a process called constraint normalization. It's big and too complex to go into here, but the basic idea is that each expression in a constraint is broken down into its atomic conceptual pieces, recursively, until you reach a component sub-expression that isn't a concept.
So given that, let's look at how the integral
and signed_integral
concepts are defined:
template concept integral = is_integral_v; template concept signed_integral = integral && is_signed_v;
The decomposition of integral
is just is_integral_v
. The decomposition of signed_integral
is is_integral_v && is_signed_v
.
Now, we come to the concept of constraint subsumption. It's kind of complicated, but the basic idea is that a constraint C1 is said to "subsume" a constraint C2 if the decomposition of C1 contains every sub-expression in C2. We can see that integral
does not subsume signed_integral
, but signed_integral
does subsume integral
, since it contains everything integral
does.
Next, we come to ordering constrained entities:
A declaration D1 is at least as constrained as a declaration D2 if
- D1 and D2 are both constrained declarations and D1's associated constraints subsume those of D2; or
- D2 has no associated constraints.
Because signed_integral
subsumes integral
, the <signed_integral> wrapper
is "at least as constrained" as the <integral> wrapper
. However, the reverse is not true, due to the subsumption not being reversible.
Therefore, in accord with the rule for "more constrained" entities:
A declaration D1 is more constrained than another declaration D2 when D1 is at least as constrained as D2, and D2 is not at least as constrained as D1.
Since the <integral> wrapper
is not at least as constrained as <signed_integral> wrapper
, the latter is considered more constrained than the former.
And therefore, when the two of them could both apply, the more constrained declaration wins.
Be aware that the rules of constraint subsumption stop when an expression is encountered which is not a concept
. So if you did this:
template<typename T>
constexpr bool my_is_integral_v = std::is_integral_v<T>;
template<typename T>
concept my_signed_integral = my_is_integral_v<T> && std::is_signed_v<T>;
In this case, my_signed_integral
would not subsume std::integral
. Even though my_is_integral_v
is defined identically to std::is_integral_v
, because it isn't a concept, C++'s subsumption rules cannot peer through it to determine that they are the same.
So the subsumption rules encourage you to build concepts out of operations on atomic concepts.
With Partial_ordering_of_constraints
A constraint P is said to subsume constraint Q if it can be proven that P implies Q up to the identity of atomic constraints in P and Q.
and
Subsumption relationship defines partial order of constraints, which is used to determine:
- the best viable candidate for a non-template function in overload resolution
- the address of a non-template function in an overload set
- the best match for a template template argument
- partial ordering of class template specializations
- partial ordering of function templates
And concept std::signed_integral
subsumes std::integral<T>
concept:
template < class T >
concept signed_integral = std::integral<T> && std::is_signed_v<T>;
So your code is ok, as std::signed_integral
is more "specialized".