C++20 Concepts: Constraint Normalization - c++

This is an example from the C++20 Standard (ISO/IEC 14882:2020), Section 13.5.4 ([temp.constr.normal]), Paragraph 1 (emphasis mine):
The normal form of a concept-id C<A1 , A2 , ..., An> 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.
template<typename T> concept A = T::value || true;
template<typename U> concept B = A<U*>;
template<typename V> concept C = B<V&>;
Normalization of B’s constraint-expression is valid and results in T::value (with the mapping T -> U*) V true (with an empty mapping), despite the expression T::value being ill-formed for a pointer type T. Normalization of C’s constraint-expression results in the program being ill-formed, because it would form the invalid type V&* in the parameter mapping.
I understand that C makes a program ill-formed (and why). However, it is not clear to me if B would result in the program being ill-formed or not. The text states that B's normalization is valid, but at the same time it states that the expression T::value is ill-formed due to that pointer type (which I understand). Does it mean that only the normalization part of the process is valid but the program itself is ill-formed in a later stage when checking T::value? Or is the program valid in any case and the check of T::value is skipped/avoided somehow?
I checked with Godbolt's Compiler Explorer and both GCC and Clang seem to be fine with this. Nevertheless, since the Standard says "no diagnostic is required", this does not help much.

The concept B is valid, as you can pass a pointer to the concept A. Inside A itself the pointer cannot access ::value but this, according to the spec, [temp.constr.atomic], would not be considered as an error but rather as false, then the || true on concept A would make the entire expression true.
Note that if we pass int& to concept B then our code would be IFNDR, as B would try to pass to A an invalid type (int&*).
The concept C is IFNDR as is, since it passes a reference to B, that tries to pass a pointer to this reference to A, and again a V&* type is invalid.
Trying to evaluate an ill-formed-no-diagnostic-required expression using static_assert will not necessarily help in answering the question whether the expression is valid or not, as the compiler is not required to fail a static_assert on an ill-formed-no-diagnostic-required expression.

Note that each normalized constraint consists out of 2 parts:
The atomic constraint and an associated parameter mapping.
Let's separate each constraint into those two parts for your three example concepts:
In your example the normalized form of concept A would be the disjunction of these two constraints:
Atomic expression: X::value
Parameter Mapping: X ↦ T
Atomic expression: true
No Parameter Mapping
The normalized form of concept B would be the disjunction of these two constraints:
Atomic expression: X::value
Parameter Mapping: X ↦ U*
Atomic expression: true
No Parameter Mapping
And the normalized form of concept C would be the disjunction of these two constraints:
Atomic expression: X::value
Parameter Mapping: X ↦ V&*
Atomic expression: true
No Parameter Mapping
How the parameter mappings get formed
Forming the parameter mapping for an atomic expression is straightforward:
By default an atomic expression always starts out with an identity parameter mapping (i.e. no type modifications):
13.5.4 Constraint normalization [[temp.constr.normal]]
(1) The normal form of an expression E is a constraint that is defined as follows:
[...]
(1.5) The normal form of any other expression E is the atomic constraint whose expression is E and whose parameter mapping is the identity mapping.
and the only way to get a non-identity parameter mapping is to name another concept within the constraint:
13.5.4 Constraint normalization [[temp.constr.normal]]
(1.4) The normal form of a concept-id C<A1, A2, ..., An> 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. [...]
Here are a few examples:
template<class T> constexpr bool always_true = true;
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ T (identity)
template<class T> concept Base = always_true<T>;
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ U (identity)
template<class U> concept Foo = Base<U>;
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ V::type
template<class V> concept Bar = Base<typename V::type>;
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ W&&
template<class W> concept Baz = Base<W&&>;
Your quoted section
Which brings us back to your original quoted section:
13.5.4 Constraint normalization [[temp.constr.normal]]
(1.4) The normal form of a concept-id C<A1, A2, ..., An> 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.
Note that the highlighted statement only applies to the parameter mapping - not to the atomic expression itself.
This is why concept C in your example is ill-formed, NDR - because the parameter mapping for its atomic expressions forms an invalid type (pointer to reference): X ↦ V&*
Note that the actual type that gets substituted for V does not matter at the normalization stage; the only thing that matters is if the mapping itself forms an invalid type or expression.
Here are a few more examples:
template<class T> constexpr bool always_true = true;
// well-formed
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ T (identity)
template<class T> concept Base = always_true<T>;
// well-formed
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ U::type
template<class U> concept Foo = Base<typename U::type>;
// ill-formed, ndr (invalid parameter mapping)
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ V*::type
template<class V> concept Bar = Foo<V*>;
// ill-formed, ndr (invalid parameter mapping)
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ W&*
template<class W> concept Baz = Foo<W&>;
Rough Timeline of events during compilation
To answer the question of when your program gets ill-formed ndr, we need to establish the order in which events take place during compilation.
When the constraints of an associated declaration are determined OR the value of a concept is evaluated then constraint normalization will take place.
This is given by:
13.5.4 Constraint normalization [[temp.constr.normal]]
[Note 1] Normalization of constraint-expressions is performed when determining the associated constraints of a declaration and when evaluating the value of an id-expression that names a concept specialization.
This is where your program would become ill-formed, ndr if the parameter mapping forms an invalid type or expression.
After the constraints have been normalized the actual type will be substituted into the constraints:
13.5.2.3 Atomic constraints [[temp.constr.atomic]]
(3) To determine if an atomic constraint is satisfied, the parameter mapping and template arguments are first substituted into its expression. If substitution results in an invalid type or expression, the constraint is not satisfied.
Note that at this point it is allowed for invalid types or expression to be formed - if that's the case then the result of the constraint will simply be false.
Conclusion
So to answer your questions:
Does it mean that only the normalization part of the process is valid but the program itself is ill-formed in a later stage when checking T::value?
Concepts A and B are well-formed.
Concept C is ill-formed, ndr during the normalization process.
The actual atomic constraint T::value does not matter in this case; it could as well simply be always_true<T>.
Or is the program valid in any case and the check of T::value is skipped/avoided somehow?
The program is valid as long as concept C never gets normalized.
i.e. explicitly evaluating it or using it as a constraint would make your program ill-formed, ndr.
Example:
// evaluates concept C
// -> results in normalization of C
// -> ill-formed, ndr
static_assert(C</* something */>);
template<C T>
void foo() {}
// constraints of foo will be determined
// -> results in normalization of C
// -> ill-formed, ndr
foo</* something */>();

Related

Why is substitution failure in parameter mappings considered ill-formed?

In this code,
template<class T, class U>
concept always_true = true;
template<class T>
concept always_true_if_tagged = always_true<T, typename T::tag>;
struct A {
using tag = int;
};
static_assert(always_true_if_tagged<A>);
static_assert(!always_true_if_tagged<int>); //GCC says this failed
GCC says that the second assertion failed. Both Clang and MSVC are agree to compile it.
I originally thought that it is ill-form with no diagnostic required, because of temp.constr.normal#1.4
The normal form of a concept-id C<A1, A2, ..., An> 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 substitution T::typename tag is a parameter mapping for always_true, so it is ill-formed; no diagnostic is required.
So my first two questions are
Was I correct? (Is it ill-formed and did I cite the correct reason?)
Why it should be ill-formed? (If I was correct.)
One of the solutions is to check the nested typename before. So the parameter mapping for always_true doesn't happen.
template<class T>
concept always_true_if_tagged =
requires {typename T::tag;}
&& always_true<T, typename T::tag>;
Furthermore, temp.constr.atomic#3 says
To determine if an atomic constraint is satisfied, the parameter mapping and template arguments are first substituted into its expression. If substitution results in an invalid type or expression, the constraint is not satisfied. Otherwise, the lvalue-to-rvalue conversion is performed if necessary, and E shall be a constant expression of type bool. The constraint is satisfied if and only if evaluation of E results in true. If, at different points in the program, the satisfaction result is different for identical atomic constraints and template arguments, the program is ill-formed, no diagnostic required.
If I write
template<class T>
concept always_true_if_tagged = bool(always_true<T, typename T::tag>);
bool(always_true<T, typename T::tag>) is an atomic constraint IIUC. The substitution of T::typename tag with T=int results in invalid type, so it should be well-formed and not satisfied.
So my last two(or four) questions are
Why this doesn't apply to the first code or why [temp.constr.normal#1.4] doesn't apply here?
3.1. Is this substitution a parameter mapping for concept-id always_true?
3.2. Is the usage of always_true in concept C = always_true<T, typename T::tag> an atomic contraint? temp.constr.constr#general-1 says there are three different kinds of constraints: conjunctions, disjunctions and atomic constraints.
Why can't concept C = always_true<T, typename T::tag> with T=int be well-formed like this one? (Likely the same as the second question though)
First of all, the lhs of scope resolution operator (::) can only be "a namespace or a class, enumeration, or dependent type", ill-formed otherwise.
With my experiment, always_true_if_tagged<int> is actually evaluate to true in gcc compiler, so the static_assert failed is because of evaluating to false. I guess that gcc compiler assumed that regardless of T, U, always_true is always true, so optimized out the evaluation of typename T::tag in constraint normalization.
So my answers to your questions:
I believe that you are correct.
The compiler has no idea whether the constraint is satisfied in this state (constraint normalization), and cannot perform further evaluation until a concept returns true or false.
As descibe above, I think compiler optimzed out the evaluation.
Subtitution is happend when concept is used, and parameter mapping is "created" when concept is declared, see follow.
Atomic constraint
An atomic constraint consists of 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 its parameter mapping.
I believe you have the answer now.
Were you correct? […]
Yes typename T::tag doesn’t name a valid type with [T = int].
Why should it be ill-formed?
The reason you cited is right. typename int::tag is just non-sense.
From memory, you’d also need to move the template specialization below the struct A to use typename A::tag in the template.
Why […]
I don’t believe this is well-formed because of §7.3.2 [conv.lval]
I don’t have the answer to the questions, only more questions.
In a concept, it’s not a template instantiation (See: [temp.concept], [temp.spec], [temp.dep]), so I don’t understand how that could be an atomic constraint (is it a constant expression? How can it evaluate to anything).
clang 14.0.0 doesn’t generate warnings with -fsanitize=undefined on 😰
See also:
SFINAE

Is a requires expression an atom when normalizing constraints?

I want to make sure I properly understand the constraint normalization process since cppreference is slightly fuzzy on this topic.
It seems that during the normalization, anything that is inside of a requires expression is always considered an atom, no matter how specific/complex.
This seems to be supported by the different handling of:
template<typename T>
concept Decrementable = requires(T t) { --t; };
template<typename T>
concept RevIterator = Decrementable<T> && requires(T t) { *t; };
template<typename T>
concept RevIterator2 = requires(T t) { --t; *t; };
Where Decrementable < RevIterator but Decrementable and RevIterator2 are not ordered.
So, is this correct? Or can someone point me to the specific part of the standard that talks about this?
Yes your understanding is correct. For subsumption (what you denote by <) to occur, there has to be a certain relationship between the normal forms of the constraint expression. And if one examines the constraint normalization process:
[temp.constr.normal]
1 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 a concept-id C<A1, A2, ..., An> 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.
one sees that logical AND expressions, logical OR expressions, and concept-ids are the only expressions that get "broken down". Every other sort of expression pretty much forms its own atomic constraint, including a requires expression like requires(T t) { --t; *t; }.
The rules of constraint expression normalization act recursively top-down. If an expression is not a conjunction/disjunction of two expressions, is not a parenthesized expression, and is not a concept name, then it is an atomic constraint expression. A requires expression is not one of those exceptions, so it is an atomic constraint expression.

Why does "&& true" added to a constraint make a function template a better overload?

Consider the following two overloads of a function template foo:
template <typename T>
void foo(T) requires std::integral<T> {
std::cout << "foo requires integral\n";
}
template <typename T>
int foo(T) requires std::integral<T> && true {
std::cout << "foo requires integral and true\n";
return 0;
}
Note the difference between the two constraints: the second has an extra && true.
Intuitively speaking, true is redundant in a conjunction (since X && true is just X). However, it looks like this makes a semantic difference, as foo(42) would call the second overload.
Why is this the case? Specifically, why is the second function template a better overload?
As per [temp.constr.order], particularly [temp.constr.order]/1 and [temp.constr.order]/3
/1 A constraint P subsumes a constraint Q if and only if, [...] [ Example: Let A and B be atomic constraints. The constraint A ∧ B subsumes A, but A does not subsume A ∧ B. The constraint A subsumes A ∨ B, but A ∨ B does not subsume A. Also note that every constraint subsumes itself. — end example ]
/3 A declaration D1 is at least as constrained as a declaration D2 if
(3.1) D1 and D2 are both constrained declarations and D1's associated constraints subsume those of D2; or
(3.2) D2 has no associated constraints.
if we consider A as std::integral<T> and B as true; then:
A ∧ B which is std::integral<T> && true subsumes A, which is std::integral<T>,
meaning that for the following declarations:
// Denote D1
template <typename T>
void foo(T) requires std::integral<T> && true;
// Denote D2
template <typename T>
void foo(T) requires std::integral<T>;
the associated constraints of D1 subsume those of D2, and thus D1 is at least as constrained as D2. Meanwhile the reverse does not hold, and D2 is not at least as constrained as D1. This means, as per [temp.constr.order]/4
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.
that the declaration D1 is more constrained than declaration D2, and D1 is subsequently chosen as the best match by overload resolution, as per [temp.func.order]/2:
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 (if one exists) as determined below.
The constraint std::integral<T> && true subsumes std::integral<T> and therefore "wins" according to the partial ordering of constraints rules.
To check if constraint A subsumes B ([temp.constr.order]):
1. Both constraints are brought to a disjunctive normal form. This means all || are "expanded" to their independent form.
2. Then each disjunctive clause is split into atomic clauses (smallest &&-parts).
3. The meaning of atomic clauses themselves isn't compared, they are only compared for equality.
If constraint A contains all the atomic clauses of B and some more, then A subsumes B.
See Example 1:
[Example 1: Let A and B be atomic constraints. The constraint A ∧ B subsumes A, but A does not subsume A ∧ B. The constraint A subsumes A ∨ B, but A ∨ B does not subsume A. Also note that every constraint subsumes itself. — end example]
So it doesn't matter that the additional clause is a no-op, it's there in A but not in B, and that's all there is to it really.
Intuitively speaking, true is redundant in a conjunction
Indeed! In the interest of adopting standard-speak, let's say your specializations are functionally equivalent.
Why is the second function template a better overload?
Surprise! It isn't.
The standard explicitly references your && true constraint in an example of a program that is ill-formed, no diagnostic required.
[13.5.2.3]
a program is ill-formed, no diagnostic required, when the meaning of the program depends on whether two constructs are equivalent, and they are functionally equivalent but not equivalent.
[Example 2:
template <unsigned N> void f2()
requires Add1<2 * N>;
template <unsigned N> int f2()
requires Add1<N * 2> && true;
void h2() {
f2<0>(); // ill-formed, no diagnostic required:
// requires determination of subsumption between atomic constraints that are
// functionally equivalent but not equivalent
}
— end example]
Second specialization is picked because C++ uses partial ordering algorithm for picking the generic function specialization.
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 (if one exists) as determined below.
13.5.4 Partial ordering by constraints
13.7.6.2 Partial ordering of function templates
of C++ 20 final working draft which can be found here:
http://open-std.org/jtc1/sc22/wg21/docs/papers/2020/n4861.pdf

How is the best constrained function template selected with concepts?

In a presentation of concepts something like this was shown:
template <bidirectional_iterator It>
void sort(It begin, It end); // #1
template <random_access_iterator It>
void sort(It begin, It end); // #2
std::list<int> l{};
sort(l.begin(), l.end()); // #A -> calls #1
std::vector<int> v{};
sort(v.begin(), v.end()); // #B -> calls #2
For the call #A it's simple: only sort #1 is viable as the constraint random_access_iterator is not satisfied so it calls #1.
But for the call #B both sorts are viable as both constraints (random_access_iterator and bidirectional_iterator are satisfied). So how is the "more efficient" sort #2 chosen? The presenter said "it just works".
So how is the "more efficient" sort #2 chosen?
It works because there is a partial ordering on constraints (defined by the subsumes relation).
sort #2 (the one with the randomaccess_iterator) is more constrained than sort #1 (the one with bidirectional_iterator) because randomaccess_iterator subsumes bidirectional_iterator:
template <class It>
concept bidirectional_iterator = requires /*...*/;
template <class It>
concept randomaccess_iterator = bidirectional_iterator<It> && requires /*...*/;
To make this work constraints are aware at the language level of conjunctions and disjunctions.
The process for determining if a declaration is more or less constrained than another goes like this: Constraint normalization -> constraint subsumes relation -> (defines) constraint partial ordering -> (determines) declarations are more/less constraint relation.
Simplified, normalization is the substitution of the concepts template parameters in the parameter mapping of constraints.
Example:
template <class T> concept integral = std::is_integral_v<T>;
template <class T> concept signed_integral = integral<T> && std::is_signed_v<T>;
template <class T> concept integral_4 = integral<T> && sizeof(T) == 4;
void foo_1(integral auto) // #0
void foo_1(signed_integral auto) // #1
void foo_1(integral_4 auto) // #2
auto test1()
{
foo_1(std::uint16_t{}); // calls #0
foo_1(std::uint32_t{}); // calls #2
foo_1(std::int16_t{}); // calls #1
//foo_1(std::int32_t{}); // error ambiguous between #1 and #2
}
the normal form of integral is std::is_integral_v<T>
the normal form of signed_integral is std::is_integral_v<T> ∧ std::is_signed_v<T>
the normal form integral_4 is std::is_integral_v<T> ∧ sizeof(T) == 4
signed_integral subsumes integral
integral_4 subsumes integral
#1 is more constraint than #0
#2 is more constraint than #0
Example:
template <class T> concept integral = std::is_integral_v<T>;
template <class T> concept signed_integral_sad = std::is_integral_v<T> &&
std::is_signed_v<T>;
template <class T> concept integral_4_sad = std::is_integral_v<T> && sizeof(T) == 4;
void foo_2(integral auto) // #0
void foo_2(signed_integral_sad auto); // #1
void foo_2(integral_4_sad auto); // #2
auto test2()
{
foo_2(std::uint16_t{}); // calls #0
//foo_2(std::uint32_t{}); // error ambiguous between #0 and #2
//foo_2(std::int16_t{}); // error ambiguous between #0 and #1
//foo_2(std::int32_t{}); // error ambiguous between #0, #1 and #2
}
the normal form of integral is std::is_integral_v<T>
the normal form of signed_integral_sad is std::is_integral_v<T> ∧ std::is_signed_v<T>
the normal form integral_4_sad is std::is_integral_v<T> ∧ sizeof(T) == 4
However there is a rule
§13.5.1.2 Atomic constraints [temp.constr.atomic]
Two atomic constraints, e1 and e2, are identical if they are formed from the same appearance of the same expression [...]
This means that the std::is_integral_v<T> atomic expressions from the 3 normal forms are not identical between them because they were not formed from the same expression. So:
there is no subsumes relation
there is no more constraint relation
Which leads to the extra ambiguities.
§ 13.5.1 Constraints [temp.constr.constr]
A constraint is a sequence of logical operations and operands that specifies requirements on template arguments. The operands of a
logical operation are constraints. There are three different kinds of
constraints:
(1.1) conjunctions (13.5.1.1)
(1.2) disjunctions (13.5.1.1), and
(1.3) atomic constraints (13.5.1.2).
§13.5.1.1 Logical operations [temp.constr.op]
There are two binary logical operations on constraints: conjunction and disjunction. [Note: These logical operations have no corresponding
C++ syntax. For the purpose of exposition, conjunction is spelled
using the symbol ∧ and disjunction is spelled using the symbol ∨]
§13.5.3 Constraint normalization [temp.constr.normal]
The normal form of an expression E is a constraint (13.5.1) that is defined as follows:
(1.1) The normal form of an expression ( E ) is the normal form of
E.
(1.2) The normal form of an expression E1 || E2 is the
disjunction (13.5.1.1) of the normal forms of E1 and E2.
(1.3) The normal form of an expression E1 && E2 is the conjunction
of the normal forms of E1 and E2.
(1.4) The normal form of
a concept-id C<A1, A2, ..., An> 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. [...]
(1.5) The normal form of any other
expression E is the atomic constraint whose expression is E and
whose parameter mapping is the identity mapping.
The process of obtaining the normal form of a constraint-expression is called normalization.
§13.5.4 Partial ordering by constraints [temp.constr.order]
A constraint P subsumes a constraint Q if and only if, for every disjunctive clause Pi in the disjunctive normal form
130 of P, Pi subsumes every conjunctive clause Qj in
the conjunctive normal form 131 of Q, where
(1.1) 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
(1.2) an atomic constraint A subsumes
another atomic constraint B if and only if A and B are identical
using the rules described in 13.5.1.2.
[Example: Let A and B
be atomic constraints (13.5.1.2). The constraint A ∧ B subsumes A,
but A does not subsume A ∧ B. The constraint A subsumes A ∨ B,
but A ∨ B does not subsume A. Also note that every constraint
subsumes itself. — end example]
[Note: The subsumption relation defines a partial ordering on constraints. This partial ordering is used to determine
(2.1) the best viable candidate of non-template functions (12.4.3),
(2.2) the address of a non-template function (12.5),
(2.3) the matching of template template arguments (13.4.3),
(2.4) the partial ordering of class template specializations (13.7.5.2), and
(2.5) the partial ordering of function
templates (13.7.6.2).
— end note]
A declaration D1 is at least as constrained as a declaration D2 if
(3.1) D1 and D2 are both constrained declarations and D1’s
associated constraints subsume those of D2; or
(3.2) D2 has no associated constraints.
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.
130) A constraint is in disjunctive normal form when it is a
disjunction of clauses where each clause is a conjunction of atomic
constraints. [Example: For atomic constraints A, B, and C, the
disjunctive normal form of the constraint A ∧ (B ∨ C) is (A ∧ B) ∨
(A ∧ C). Its disjunctive clauses are (A ∧ B) and (A ∧ C). — end
example]
131) A constraint is in conjunctive normal form when it is a
conjunction of clauses where each clause is a disjunction of atomic
constraints. [Example: For atomic constraints A, B, and C, the
constraint A ∧ (B ∨ C) is in conjunctive normal form. Its
conjunctive clauses are A and (B ∨ C). — end example

Does constraint subsumption only apply to concepts?

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>?
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.