为什么双重否定会改变C++概念的价值?

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

我的一个朋友向我展示了一个C++20的程序,有概念,让我很困惑:

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> );

它被所有编译器接受:https://gcc.godbolt.org/z/e67qKoqce

这里的概念D和概念C是一样的,唯一不同的是双重否定运算符!!,乍一看应该不会改变概念值.仍然对于结构 A,概念 C 为真,概念 D 为假。

你能解释一下为什么会这样吗?

Here the concept D is the same as the concept C

他们不是。在检查满意度并分解为原子约束时,约束(和概念 ID)被规范化。

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

||CD 中有不同的看法:

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

对于C,原子约束是T::aT::b
对于 D 只有一个原子约束,即 !!(T::a || T::b).

原子约束中的替换失败使其不满足并评估为 falseC<A> 是一个满足的约束和一个不满足的约束的析取,所以它是 trueD<A> 是错误的,因为它的唯一原子约束具有替换失败。

要认识到的重要一点是 [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 明确指出了这一点。

考虑到这一点,我们可以检查这两个案例。 C 是两个原子约束的析取:T::aT::bPer /3,析取在检查满意度时采用 short-circuiting 行为。这意味着 T::a 首先被检查。由于它成功了,所以整个约束 C 都得到了满足,而无需检查第二个约束。

另一方面,

D 是一个原子约束:!!(T::a || T::b)|| 不会以任何方式创建析取,它只是表达式的一部分。我们查看 [temp.constr.atomic]/3 以查看模板参数被替换。这意味着 T::aT::b 都执行了替换。本段还指出,如果替换失败,则不满足约束条件。正如前面的说明所暗示的那样,甚至还没有考虑前面的否定。事实上,只有一个否定会产生相同的结果。


现在显而易见的问题是为什么要这样设计概念。不幸的是,我不记得在设计师的会议演讲和其他交流中遇到过任何理由。我能找到的最好的是原始提案中的这一点:

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.

在我看来,这可能真的低估了决策中的想法。我很乐意看到设计师详细说明这一点,因为我相信他比这个小引述有更多的话要说。