Does constraint subsumption only apply to concepts?

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 concepts instead of inline constexpr bools, 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.


Background

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 to Y::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, and Z::Fooable respectively will always select the candidate constrained by Z::Fooable. This is almost certainly not what a programmer wants.


Standard References

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 within E 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 logical AND expression nor a logical OR 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 is E 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 true1 and the second into true2. Again, the subscripts indicate which true is being referred to.

The constraint for the second function is D1<T>, which normalizes (4th bullet) into true1.

And now, true1 is indeed the same expression as true1, 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.

Leave a Comment