为什么same_as概念两次检查类型相等性?

use*_*147 17 c++ concept c++20

https://en.cppreference.com/w/cpp/concepts/same_as上查看same_as概念的可能实现,因为我注意到正在发生奇怪的事情。

namespace detail {
    template< class T, class U >
    concept SameHelper = std::is_same_v<T, U>;
}

template< class T, class U >
concept same_as = detail::SameHelper<T, U> && detail::SameHelper<U, T>;
Run Code Online (Sandbox Code Playgroud)

第一个问题是为什么要插入一个SameHelper概念?第二个就是same_as检查是否T相同UU一样T?这不是多余的吗?

Kae*_*Rin 14

有趣的问题。我最近看了安德鲁·萨顿(Andrew Sutton)关于概念的演讲,在问答环节中,有人问了以下问题(以下链接中的时间戳): CppCon 2018:安德鲁·萨顿“ 60岁的概念:您需要知道的一切,而您不需要的一切”

因此问题归结为:If I have a concept that says A && B && C, another says C && B && A, would those be equivalent?Andrew回答“是”,但指出了一个事实,即编译器具有一些内部方法(对用户透明),可将这些概念分解为原子逻辑命题(atomic constraints如Andrew所说的术语),并检查它们是否当量。

现在看看cppreference的内容std::same_as

std::same_as<T, U>归类std::same_as<U, T>,反之亦然。

基本上,这是一种“如果且仅当”的关系:它们相互暗示。(逻辑等效)

我的猜测是,这里的原子约束是std::is_same_v<T, U>。编译器对待的方式std::is_same_v可能会使他们思考std::is_same_v<T, U>并被std::is_same_v<U, T>视为两个不同的约束(它们是不同的实体!)。因此,如果std::same_as仅使用其中之一来实现:

template< class T, class U >
concept same_as = detail::SameHelper<T, U>;
Run Code Online (Sandbox Code Playgroud)

然后std::same_as<T, U>std::same_as<U, T>将“爆炸”到不同的原子约束,并且变得不等效。

好吧,为什么编译器会关心?

考虑这个例子

#include <type_traits>
#include <iostream>
#include <concepts>

template< class T, class U >
concept SameHelper = std::is_same_v<T, U>;

template< class T, class U >
concept my_same_as = SameHelper<T, U>;

// template< class T, class U >
// concept my_same_as = SameHelper<T, U> && SameHelper<U, T>;

template< class T, class U> requires my_same_as<U, T>
void foo(T a, U b) {
    std::cout << "Not integral" << std::endl;
}

template< class T, class U> requires (my_same_as<T, U> && std::integral<T>)
void foo(T a, U b) {
    std::cout << "Integral" << std::endl;
}

int main() {
    foo(1, 2);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

理想情况下,my_same_as<T, U> && std::integral<T>包含在内my_same_as<U, T>;因此,编译器应选择第二个模板特化,除非...否则:编译器会发出错误error: call of overloaded 'foo(int, int)' is ambiguous

其背后的原因是,由于my_same_as<U, T>my_same_as<T, U>彼此不相加,my_same_as<T, U> && std::integral<T>并且my_same_as<U, T>变得无可比拟(在包含关系下的部分约束集上)。

但是,如果您更换

template< class T, class U >
concept my_same_as = SameHelper<T, U>;
Run Code Online (Sandbox Code Playgroud)

template< class T, class U >
concept my_same_as = SameHelper<T, U> && SameHelper<U, T>;
Run Code Online (Sandbox Code Playgroud)

代码会编译。

  • 要求编译器将*两个*表达式视为约束约束的唯一表达式,但它可以以明显的方式考虑*参数。因此,我们不仅需要两个方向(因此,在比较约束时它们的命名顺序无关紧要),我们还需要* SameHelper **:它使is_same_v的两个用途都成为可能。从相同的表达式派生。 (2认同)
  • 关于概念平等的传统观点似乎是错误的。与“is_same&lt;T, U&gt;”与“is_same&lt;U, T&gt;”相同的模板不同,两个原子约束不被视为相同,除非它们也由相同的表达式形成。因此两者都需要。 (2认同)

dfr*_*fri 6

[concept.same] 作为LWG 问题 3182 的一部分进行了更改(在该概念根据P1754R1Same重命名之前)[强调我的]:is_same

3182.Same的规范可以更清楚

  • 部分:18.4.2 [concept.same]
  • 状态:WP
  • [...]

讨论:

18.4.2[concept.same]中Same概念的规范:

template<class T, class U>
  concept Same = is_same_v<T, U>;
Run Code Online (Sandbox Code Playgroud)
  1. Same<T, U>包含Same<U, T>,反之亦然。

似乎矛盾。仅从概念定义来看,情况并非Same<T, U>包含在内Same<U, T>,反之亦然。第 1 段试图告诉我们,有一些魔法可以提供所述的包含关系,但对于普通读者来说,它似乎是一个错误注释的注释。我们应该添加注释来解释这里实际发生的事情,或者以自然提供指定的包含关系的方式定义概念。

鉴于对称包含习语有一个简单的库实现,后一种选择似乎更可取。

[...]

提议的决议:

该措辞是相对于 N4791 而言的。

将 18.4.2 [concept.same] 更改如下:

template<class T, class U>
  concept same-impl = // exposition only
    is_same_v<T, U>;

template<class T, class U>
  concept Same = is_same_v<T, U>same-impl<T, U> && same-impl<U, T>;
Run Code Online (Sandbox Code Playgroud)
  1. [注:Same<T, U>包含在内Same<U, T>,反之亦然。— 尾注]

我将开始解决 OP 的第二个问题(因为第一个问题的答案将随之而来):

OP:第二个是为什么要same_as检查 if Tis the same asUUthe same as T?不是多余的吗?

根据上面强调的最后一部分:

[...] 鉴于对称包含习语有一个简单的库实现,后一种选择似乎更可取。

CWG 3182 的决议是重新定义库规范以使用两个对称约束,专门以(语义上)自然的方式实现两者之间的包含关系(“对称包含习语”,如果您愿意的话)。

作为切线(但与回答 OP 的第一个问题相关),根据[temp.constr.order],尤其是[temp.constr.order]/1[temp.constr.order ],这对于通过约束进行偏序排序可能很重要]/3

/1 约束P包含约束Q当且仅当,[...] [ 示例:让 A 和 B 成为原子约束。约束A ? B包含AA不包含A ? B。约束A包含A ? BA ? B不包含A。另请注意,每个约束都包含自身。— 结束示例 ]

/3 声明D1 至少与声明一样受到约束D2如果

  • (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.

Such that in the following example:

#include <iostream>

template <typename T> concept C1 = true;    
template <typename T> concept C2 = true; 

template <typename T> requires C1<T> && C2<T> // #1
void f() { std::cout << "C1 && C2"; }

template <typename T> requires C1<T>          // #2
void f() { std::cout << "C1"; }
Run Code Online (Sandbox Code Playgroud)

a call to, say, f<int>(), is not ambiguous (#1 will be called) as the constraints at #1, C1<T> && C2<T>, subsumes the constraint at #2, C1<T>, but not vice versa.

We could, however, go down the rabbit hole of [temp.constr.order] and [temp.constr.atomic] to show that even in the older implementation of same_as:

// old impl.; was named Same back then
template<typename T, typename U>
concept same_as = is_same_v<T, U>;
Run Code Online (Sandbox Code Playgroud)

same_as<T, U> would still subsume same_as<U, T> and vice versa; this is not entirely trivial, however.

Thus, instead of choosing the option of "add a note to explain what's actually happening here" to resolve LWG 3182, [concept.same] instead changed the library implementation to be defined in a form that had a clearer semantic meaning to the "casual reader":

// A and B are concepts
concept same_as = A ^ B
Run Code Online (Sandbox Code Playgroud)

As per the (tangential) part above, we may also note that same_as subsumes both the concepts A and B in isolation, whereas A and B in isolation does not subsume same_as.


OP: The first question is why a SameHelper concept is nedded?

根据temp.constr.order]/1,只能包含概念。因此,对于该概念的较旧实现,is_same直接使用转换特征(不是概念),特征本身不属于包含规则。含义如下:

template< class T, class U >
concept same_as = std::is_same_v<T, U> && std::is_same_v<U, T>
Run Code Online (Sandbox Code Playgroud)

将真正包含一个冗余的 rhs for &&,因为类型特征不能包含类型特征。当 LWG 3182 解决时,意图是在语义上显示如上所述的包含关系,添加了一个中间概念来强调包含。