Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

A friend of mine shown me a C++20 program with concepts, which puzzled me:

struct A { static constexpr bool a = true; };

template <typename T>
concept C = T::a || T::b;

template <typename T>
concept D = !!(T::a || T::b);

static_assert( C<A> );
static_assert( !D<A> );

It is accepted by all compilers: https://gcc.godbolt.org/z/e67qKoqce

Here the concept D is the same as the concept C, the only difference is in double negation operator !!, which from the first sight shall not change the concept value. Still for the struct A the concept C is true and the concept D is false.

Could you please explain why it is so?

like image 802
Fedor Avatar asked Jul 31 '21 11:07

Fedor


People also ask

What is double negation in C?

In C and C++ the double negation operator can be (and often is) used to convert a value to a boolean. Simply put, if int x = 42 , !! x evaluates to 1. If x = 0 , !! x evaluates to 0.

What is the purpose of double negation?

In propositional logic, double negation is the theorem that states that "If a statement is true, then it is not the case that the statement is not true." This is expressed by saying that a proposition A is logically equivalent to not (not-A), or by the formula A ≡ ~(~A) where the sign ≡ expresses logical equivalence ...


2 Answers

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.

like image 135
StoryTeller - Unslander Monica Avatar answered Oct 11 '22 15:10

StoryTeller - Unslander Monica


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.

like image 26
chris Avatar answered Oct 11 '22 13:10

chris