C++20 Concepts: 约束规范化

huangapple go评论72阅读模式
英文:

C++20 Concepts: Constraint Normalization

问题

这是来自C++20标准(ISO/IEC 14882:2020)第13.5.4节的示例,第1段(重点在于):

概念标识符C<A1,A2,...,An>的正常形式是C的约束表达式的正常形式,在每个原子约束的参数映射中将A1,A2,...,An替换为C的相应模板参数。如果任何这种替换导致无效类型或表达式,程序将被认为是非法的;不需要进行诊断。

template<typename T> concept A = T::value || true;
template<typename U> concept B = A<U*>;
template<typename V> concept C = B<V&&>;

B的约束表达式的规范化是有效的,并且在映射T -> U*中结果为T::value(没有映射),V true(带有空映射),尽管表达式T::value对于指针类型T来说是非法的。 C的约束表达式的规范化导致程序非法,因为它将在参数映射中形成无效类型V&&*

我理解C会使程序非法(原因也很明确)。但是,对我来说不太清楚B是否会导致程序非法。文本说明B的规范化是有效的,但与此同时它说明表达式T::value由于指针类型而非法(我理解这一点)。这是否意味着过程的规范化部分是有效的,但程序本身在后续阶段检查T::value时非法?还是程序在任何情况下都是有效的,而对T::value的检查在某种程度上被跳过或避免了?

我在Godbolt的Compiler Explorer上检查了这个示例,GCC和Clang似乎都可以正常工作。然而,由于标准中说“不需要进行诊断”,这并没有太多帮助。

英文:

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&lt;typename T&gt; concept A = T::value || true;
template&lt;typename U&gt; concept B = A&lt;U*&gt;;
template&lt;typename V&gt; concept C = B&lt;V&amp;&gt;;

> Normalization of B’s constraint-expression is valid and results in T::value (with the mapping T -&gt; 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&amp;* 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.

答案1

得分: 16

以下是要翻译的内容:

"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&amp;*).

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&amp;* 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.

英文:

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&amp;*).

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&amp;* 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.

答案2

得分: 13

以下是翻译好的部分:

请注意,每个规范化的约束都由两部分组成:
原子约束和相关的参数映射。


让我们将每个约束分成这两部分,以适用于你的三个示例概念:

在你的示例中,概念A的规范化形式将是这两个约束的析取:

  • 原子表达式:X::value
    参数映射:X ↦ T
  • 原子表达式:true
    没有参数映射

概念B的规范化形式将是这两个约束的析取:

  • 原子表达式:X::value
    参数映射:X ↦ U*
  • 原子表达式:true
    没有参数映射

概念C的规范化形式将是这两个约束的析取:

  • 原子表达式:X::value
    参数映射:X ↦ V&amp;*
  • 原子表达式:true
    没有参数映射

参数映射的形成方式

形成原子表达式的参数映射很简单:

默认情况下,原子表达式始终以身份参数映射(即无类型修改)开始:
> 13.5.4 约束规范化 [[temp.constr.normal]]
> <sup>(1)</sup> 表达式E的正常形式是以下方式定义的约束:
> [...]
> <sup>(1.5)</sup> 其他表达式E的正常形式是其表达式为E且参数映射为身份映射的原子约束。

获得非身份参数映射的唯一方法是在约束中命名另一个概念:
> 13.5.4 约束规范化 [[temp.constr.normal]]
> <sup>(1.4)</sup> 概念标识C&lt;A1, A2, ..., An&gt;的正常形式是C的约束表达式的正常形式,在每个原子约束的参数映射中将A1、A2、...、An替换为C的相应模板参数

以下是一些示例:

template&lt;class T&gt; constexpr bool always_true = true;

// 原子约束:always_true&lt;X&gt;
// 参数映射:X ↦ T(身份)
template&lt;class T&gt; concept Base = always_true&lt;T&gt;;

// 原子约束:always_true&lt;X&gt;
// 参数映射:X ↦ U(身份)
template&lt;class U&gt; concept Foo = Base&lt;U&gt;;

// 原子约束:always_true&lt;X&gt;
// 参数映射:X ↦ V::type
template&lt;class V&gt; concept Bar = Base&lt;typename V::type&gt;;

// 原子约束:always_true&lt;X&gt;
// 参数映射:X ↦ W&amp;&amp;
template&lt;class W&gt; concept Baz = Base&lt;W&amp;&amp;&gt;;

您引用的部分

这使我们回到了您最初引用的部分:
> 13.5.4 约束规范化 [[temp.constr.normal]]
> <sup>(1.4)</sup> 概念标识C&lt;A1, A2, ..., An&gt;的正常形式是C的约束表达式的正常形式,将A1、A2、...、An替换为C的相应模板参数。如果任何这样的替换导致无效的类型或表达式,则程序无效;不需要诊断。

请注意,上面突出显示的声明仅适用于参数映射,而不适用于原子表达式本身。

这就是为什么您示例中的概念C是非法的,不符合规范(NDR),因为其原子表达式的参数映射形成了无效类型(指向引用的指针):X ↦ V&amp;*

请注意,在规范化阶段,替代V的实际类型无关紧要;唯一重要的是映射本身是否形成了无效的类型或表达式。

以下是更多示例:

template&lt;class T&gt; constexpr bool always_true = true;

// 合法
// 原子约束:always_true&lt;X&gt;
// 参数映射:X ↦ T(身份)
template&lt;class T&gt; concept Base = always_true&lt;T&gt;

// 合法
// 原子约束:always_true&lt;X&gt;
// 参数映射:X ↦ U::type
template&lt;class U&gt; concept Foo = Base&lt;typename U::type&gt;;

// 非法,NDR(无效的参数映射)
// 原子约束:always_true&lt;X&gt;
// 参数映射:X ↦ V*::type
template&lt;class V&gt; concept Bar = Foo&lt;V*&gt;;

// 非法,NDR(无效的参数映射)
// 原子约束:always_true&lt;X&gt;
// 参数映射:X ↦ W&amp;*
template&lt;class W&gt; concept Baz = Foo&lt;W&amp;&gt;;

编译过程中的大致时间轴

要回答您的程序在哪个阶段会出现非法NDR的问题

英文:

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&amp;*
  • 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]]
> <sup>(1)</sup> The normal form of an expression E is a constraint that is defined as follows:
> [...]
> <sup>(1.5)</sup> 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]]
> <sup>(1.4)</sup> The normal form of a concept-id C&lt;A1, A2, ..., An&gt; 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&lt;class T&gt; constexpr bool always_true = true;

// Atomic constraint: always_true&lt;X&gt;
// Parameter mapping: X ↦ T (identity)
template&lt;class T&gt; concept Base = always_true&lt;T&gt;;

// Atomic constraint: always_true&lt;X&gt;
// Parameter mapping: X ↦ U (identity) 
template&lt;class U&gt; concept Foo = Base&lt;U&gt;;

// Atomic constraint: always_true&lt;X&gt;
// Parameter mapping: X ↦ V::type
template&lt;class V&gt; concept Bar = Base&lt;typename V::type&gt;;

// Atomic constraint: always_true&lt;X&gt;
// Parameter mapping: X ↦ W&amp;&amp;
template&lt;class W&gt; concept Baz = Base&lt;W&amp;&amp;&gt;;

Your quoted section

Which brings us back to your original quoted section:
> 13.5.4 Constraint normalization [[temp.constr.normal]]
> <sup>(1.4)</sup> The normal form of a concept-id C&lt;A1, A2, ..., An&gt; 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&amp;*

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&lt;class T&gt; constexpr bool always_true = true;

// well-formed
// Atomic constraint: always_true&lt;X&gt;
// Parameter mapping: X ↦ T (identity)
template&lt;class T&gt; concept Base = always_true&lt;T&gt;;

// well-formed
// Atomic constraint: always_true&lt;X&gt;
// Parameter mapping: X ↦ U::type
template&lt;class U&gt; concept Foo = Base&lt;typename U::type&gt;;

// ill-formed, ndr (invalid parameter mapping)
// Atomic constraint: always_true&lt;X&gt;
// Parameter mapping: X ↦ V*::type
template&lt;class V&gt; concept Bar = Foo&lt;V*&gt;;

// ill-formed, ndr (invalid parameter mapping)
// Atomic constraint: always_true&lt;X&gt;
// Parameter mapping: X ↦ W&amp;*
template&lt;class W&gt; concept Baz = Foo&lt;W&amp;&gt;;

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]]
    > <sup>[Note 1]</sup> 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]]
    > <sup>(3)</sup> 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&lt;T&gt;.

  • > 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
    //   -&gt; results in normalization of C
    //   -&gt; ill-formed, ndr
    static_assert(C&lt;/* something */&gt;); 
    
    template&lt;C T&gt;
    void foo() {}
    
    // constraints of foo will be determined
    //   -&gt; results in normalization of C
    //   -&gt; ill-formed, ndr
    foo&lt;/* something */&gt;();  
    

huangapple
  • 本文由 发表于 2023年2月14日 09:11:12
  • 转载请务必保留本文链接:https://go.coder-hub.com/75442605.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定