Why does double negation change the value of C++ concept?

Here the concept D is the same as the concept C

They are not. Constraints (and concept-ids) are normalized when checked for satisfaction and broken down to atomic constraints.

[temp.names]

8 A concept-id is a simple-template-id where the template-name is a concept-name. A concept-id is a prvalue of type bool, and does not name a template specialization. A concept-id evaluates to true if the concept's normalized constraint-expression ([temp.constr.decl]) is satisfied ([temp.constr.constr]) by the specified template arguments and false otherwise.

And the || is regarded differently in C and D:

[temp.constr.normal]

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

For C the atomic constraints are T::a and T::b.
For D there is only one atomic constraint that is !!(T::a || T::b).

Substitution failure in an atomic constraint makes it not satisfied and evaluate to false. C<A> is a disjunction of one constraint that is satisified, and one that is not, so it's true. D<A> is false since its one and only atomic constraint has a substitution failure.


The important thing to realize is that per [temp.constr.constr], atomic constraints are composed only via conjunctions (through top-level &&) and disjunctions (through top-level ||). Negation must be thought of as part of a constraint, not the negation of a constraint. There's even a non-normative note pointing this out explicitly.

With that in mind, we can examine the two cases. C is a disjunction of two atomic constraints: T::a and T::b. Per /3, disjunctions employ short-circuiting behaviour when checking for satisfaction. This means that T::a is checked first. Since it succeeds, the entire constraint C is satisfied without ever checking the second.

D, on the other hand, is one atomic constraint: !!(T::a || T::b). The || does not create a disjunction in any way, it's simply part of the expression. We look to [temp.constr.atomic]/3 to see that template parameters are substituted in. This means that both T::a and T::b have substitution performed. This paragraph also states that if substitution fails, the constraint is not satisfied. As the earlier note suggests, the negations out front are not even considered yet. In fact, having only one negation yields the same result.


Now the obvious question is why concepts were designed this way. Unfortunately, I don't remember coming across any reasoning for it in the designer's conference talks and other communications. The best I've been able to find was this bit from the original proposal:

While negation has turned out to be fairly common in our constraints (see Section 5.3), we have not found it necessary to assign deeper semantics to the operator.

In my opinion, this is probably really underselling the thought that was put into the decision. I'd love to see the designer elaborate on this, as I'm confident he has more to say than this small quotation.