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

Why does same_as concept check type equality twice?

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

第一个问题是为什么需要一个SameHelper概念? 第二个是为什么same_as检查T是否与U相同,U是否与T相同?不是多余的吗?

std::is_same 定义为真当且仅当:

T and U name the same type with the same cv-qualifications

据我所知,标准没有定义"same type"的含义,但在自然语言和逻辑中"same"是一个等价关系,因此是可交换的。

鉴于我归因于这个假设,is_same_v<T, U> && is_same_v<U, V> 确实是多余的。但是same_­asis_same_v方面没有指定;那只是为了说明。

对两者的显式检查允许 same-as-impl 的实现满足 same_­as 而无需交换。以这种方式指定它准确地描述了概念的行为方式,而不限制它的实现方式。

我不知道为什么选择这种方法而不是根据 is_same_v 进行指定。所选方法的一个优点可以说是这两个定义是分离的。一个不依赖另一个。

有趣的问题。最近看了Andrew Sutton关于Concepts的演讲,在问答环节有人问了下面的问题(时间戳在下面link): CppCon 2018: Andrew Sutton “Concepts in 60: Everything you need to know and nothing you don't”

所以问题归结为:If I have a concept that says A && B && C, another says C && B && A, would those be equivalent?安德鲁回答是,但指出编译器有一些内部方法(对用户透明)将概念分解为原子逻辑命题(atomic constraints 正如 Andrew 所说的那样)并检查它们是否等价。

现在看看cppreference是怎么说的 std::same_as:

std::same_as<T, U> subsumes std::same_as<U, T> and vice versa.

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

我猜想这里的原子约束是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>;

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

嗯,编译器为什么要关心?

考虑 this example:

#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> 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;
}

理想情况下,my_same_as<T, U> && std::integral<T> 包含 my_same_as<U, T>;因此,编译器应该 select 第二个模板专业化,除了......它没有:编译器发出错误 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>;

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

代码编译。

[concept.same] 已更改为 LWG issue 3182 (before the concept Same was renamed to is_same as per P1754R1 的一部分)[强调 我的]:

3182. Specification of Same could be clearer

  • Section: 18.4.2 [concept.same]
  • Status: WP
  • [...]

Discussion:

The specification of the Same concept in 18.4.2 [concept.same]:

template<class T, class U>
  concept Same = is_same_v<T, U>;
  1. Same<T, U> subsumes Same<U, T> and vice versa.

似乎自相矛盾。单从概念定义来看,并不是 Same<T, U> 包含 Same<U, T> 的情况,反之亦然。段落 1 试图告诉我们 有一些魔法可以提供 声明的包含关系,但对于一个不经意的reader来说似乎 是一个错误注释的笔记。我们应该添加注释来解释 这里实际发生了什么,或者以这种方式定义概念 它自然地提供了指定的包含关系。

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

[...]

提议的决议:

这个写法是相对于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>;
  1. [Note: Same<T, U> subsumes Same<U, T> and vice versa. — end note]

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

OP: The second is why same_as checks if T is the same as U and U the same as T? Isn't it redundant?

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

[...] Given that there's a straightforward library implementation of the symmetric subsumption idiom, the latter option seems preferable.

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

作为切线(但与回答 OP 的第一个问题相关),这对于约束的部分排序很重要,根据 [temp.constr.order], particularly [temp.constr.order]/1 and [temp.constr.order]/3

/1 A constraint P subsumes a constraint Q if and only if, [...] [ Example: Let A and B be atomic constraints. The constraint A ∧ B subsumes A, but A does not subsume A ∧ B. The constraint A subsumes A ∨ B, but A ∨ B does not subsume A. Also note that every constraint subsumes itself. — end example ]

/3 A declaration D1 is at least as constrained as a declaration D2 if

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

例如在下面的例子中:

#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"; }

调用 f<int>() 是明确的(将调用 #1),因为 #1C1<T> && C2<T> 处的约束包含在#2C1<T>,反之则不然。

然而,我们可以深入 [temp.constr.order] 和 [temp.constr.atomic] 的兔子洞,以证明即使在 same_as:[=72= 的旧实现中]

// old impl.; was named Same back then
template<typename T, typename U>
concept same_as = is_same_v<T, U>;

same_as<T, U> 仍会包含 same_as<U, T>,反之亦然;然而,这并不完全是微不足道的。

因此,不是选择 的选项“添加注释来解释这里实际发生的事情” 来解决 LWG 3182,[concept.same] 而是更改了库以语义“随意reader”:

更清晰的形式定义实现
// A and B are concepts
concept same_as = A ^ B

根据上面的(切线)部分,我们可能还注意到 same_as 包含概念 AB 两个孤立的概念,而 AB 孤立地不包含 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>

确实会包含多余的 r.h.s。对于 &&,因为类型特征不能包含类型特征。当解决 LWG 3182 时,意图是按照上述语义显示包含关系,添加了一个中间概念以强调包含。