Consider this example:
template <typename T> inline constexpr bool C1 = true;
template <typename T> inline constexpr bool C2 = true;
template <typename T> requires C1<T> && C2<T>
constexpr int foo() { return 0; }
template <typename T> requires C1<T>
constexpr int foo() { return 1; }
constexpr int bar() {
return foo<int>();
}
Is the call foo<int>()
ambiguous, or does the constraint C1<T> && C2<T>
subsume C1<T>
?
A constraint is a requirement that types used as type arguments must satisfy. For example, a constraint might be that the type argument must implement a certain interface or inherit from a specific class. Constraints are optional; not specifying a constraint on a parameter is equivalent to using a Object constraint.
Let's define a few concepts in this post. A concept can be defined by a function template or by a variable template. A variable template is new with C++14 and declares a family of variables. If you use a function template for your concept, it's called a function concept; in the second case a variable concept.
Yes. Only concepts can be subsumed. The call to foo<int>
is ambiguous because neither of the declarations is "at least as constrained as" the other.
If, however, C1
and C2
were both concept
s instead of inline constexpr bool
s, then the declaration of the foo()
that returns 0
would be at least as constrained as the declaration of the foo()
that returns 1
, and the call to foo<int>
would be valid and return 0
. This is one reason to prefer to use concepts as constraints over arbitrary boolean constant expressions.
The reason for this difference (concepts subsume, arbitrary expressions do not) is best expressed in Semantic constraint matching for concepts, which is worth reading in full (I will not reproduce all the arguments here). But taking an example from the paper:
namespace X { template<C1 T> void foo(T); template<typename T> concept Fooable = requires (T t) { foo(t); }; } namespace Y { template<C2 T> void foo(T); template<typename T> concept Fooable = requires (T t) { foo(t); }; }
X::Fooable
is equivalent toY::Fooable
despite them meaning completely different things (by virtue of being defined in different namespace). This kind of incidental equivalence is problematic: an overload set with functions constrained by these two concepts would be ambiguous.That problem is exacerbated when one concept incidentally refines the others.
namespace Z { template<C3 T> void foo(T); template<C3 T> void bar(T); template<typename T> concept Fooable = requires (T t) { foo(t); bar(t); }; }
An overload set containing distinct viable candidates constrained by
X::Fooable
,Y::Fooable
, andZ::Fooable
respectively will always select the candidate constrained byZ::Fooable
. This is almost certainly not what a programmer wants.
The subsumption rule is in [temp.constr.order]/1.2:
an atomic constraint A subsumes another atomic constraint B if and only if the A and B are identical using the rules described in [temp.constr.atomic].
Atomic constraints are defined in [temp.constr.atomic]:
An atomic constraint is formed from an expression
E
and a mapping from the template parameters that appear withinE
to template arguments involving the template parameters of the constrained entity, called the parameter mapping ([temp.constr.decl]). [ Note: Atomic constraints are formed by constraint normalization.E
is never a logicalAND
expression nor a logicalOR
expression. — end note ]Two atomic constraints are identical if they are formed from the same expression and the targets of the parameter mappings are equivalent according to the rules for expressions described in [temp.over.link].
The key here is that atomic constraints are formed. This is the key point right here. In [temp.constr.normal]:
The normal form of an expression
E
is a constraint that is defined as follows:
- The normal form of an expression ( E ) is the normal form of E.
- The normal form of an expression E1 || E2 is the disjunction of the normal forms of E1 and E2.
- The normal form of an expression E1 && E2 is the conjunction of the normal forms of E1 and E2.
- The normal form of an id-expression of the form C<A1, A2, ..., An>, where C names a concept, is the normal form of the constraint-expression of C, after substituting A1, A2, ..., An for C's respective template parameters in the parameter mappings in each atomic constraint. If any such substitution results in an invalid type or expression, the program is ill-formed; no diagnostic is required. [ ... ]
- The normal form of any other expression
E
is the atomic constraint whose expression isE
and whose parameter mapping is the identity mapping.
For the first overload of foo
, the constraint is C1<T> && C2<T>
, so to normalize it, we get the conjunction of the normal forms of C1<T>
1 and C2<T>
1 and then we're done. Likewise, for the second overload of foo
, the constraint is C1<T>
2 which is its own normal form.
The rule for what makes atomic constraints identical is that they must be formed from the same expression (the source-level construct). While both functions hvae an atomic constraint which uses the token sequence C1<T>
, those are not the same literal expression in the source code.
Hence the subscripts indicating that these are, in fact, not the same atomic constraint. C1<T>
1 is not identical to C1<T>
2. The rule is not token equivalence! So the first foo
's C1<T>
does not subsume the second foo
's C1<T>
, and vice versa.
Hence, ambiguous.
On the other hand, if we had:
template <typename T> concept D1 = true;
template <typename T> concept D2 = true;
template <typename T> requires D1<T> && D2<T>
constexpr int quux() { return 0; }
template <typename T> requires D1<T>
constexpr int quux() { return 1; }
The constraint for the first function is D1<T> && D2<T>
. The 3rd bullet gives us the conjunction of D1<T>
and D2<T>
. The 4th bullet then leads us to substitute into the concepts themselves, so the first one normalizes into true
1 and the second into true
2. Again, the subscripts indicate which true
is being referred to.
The constraint for the second function is D1<T>
, which normalizes (4th bullet) into true
1.
And now, true
1 is indeed the same expression as true
1, so these constraints are considered identical. As a result, D1<T> && D2<T>
subsumes D1<T>
, and quux<int>()
is an unambiguous call that returns 0
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With