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

时间:2019-10-22 17:02:15

标签: c++ c++20 concept

看着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相同?不是多余的吗?

3 个答案:

答案 0 :(得分:14)

有趣的问题。我最近观看了安德鲁·萨顿(Andrew Sutton)关于概念的演讲,在问答环节中,有人提出了以下问题(以下链接中的时间戳): 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?安德鲁回答是,但指出了一个事实,即编译器具有一些内部方法(对用户透明),可以将概念分解为原子逻辑命题({{1 }}用安德鲁(Andrew)的话说),并检查它们是否相等。

现在看看cppreference对atomic constraints的看法:

  

std::same_as包含std::same_as<T, U>,反之亦然。

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

我的推测是,这里的原子约束是std::same_as<U, T>。编译器对待std::is_same_v<T, U>的方式可能会使他们认为std::is_same_vstd::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>会“爆炸”到不同的原子约束而变得不相等。

那么,为什么编译器会在意呢?

考虑this example

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; } 包含my_same_as<T, U> && std::integral<T>;因此,编译器应该选择第二个模板特化,除了...否则:编译器会发出错误my_same_as<U, T>

其背后的原因是,由于error: call of overloaded 'foo(int, int)' is ambiguousmy_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>;

代码会编译。

答案 1 :(得分:4)

[concept.same] 已更改为 LWG issue 3182 的一部分(在 Same 概念根据 P1754R1 重命名为 is_same 之前)[强调我的]:

<块引用>

3182。相同的规范可能更清楚

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

讨论:

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

<块引用>
template<class T, class U>
  concept Same = is_same_v<T, U>;
  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>;
  1. [注意:Same<T, U> 包含 Same<U, T>,反之亦然。 — 尾注]

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

<块引用>

OP:第二个是为什么 same_as 检查 T 是否与 U 相同以及 U 是否与 {{1} 相同}?不是多余的吗?

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

<块引用>

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

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

>

作为切线(但与回答 OP 的第一个问题相关),根据 [temp.constr.order],尤其是 [temp.constr.order]/1[temp.constr.order]/3

,这对于通过约束进行偏序排序可能很重要 <块引用>

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

/3 声明 A 至少与声明 D1 if

一样受到约束
  • (3.1) D2D1 都是约束声明D2关联约束包含了 {{ 1}};或
  • (3.2) D2 没有相关的约束。

例如在以下示例中:

D1

D2 的调用是明确的(将调用 #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 处的约束包含 {{ 处的约束1}}、#1,但反之则不然。

然而,我们可以深入研究 [temp.constr.order] 和 [temp.constr.atomic] 以证明即使在旧的 C1<T> && C2<T> 实现中:

#2

C1<T> 仍然包含 same_as,反之亦然;然而,这并非微不足道。

因此,与其选择 “添加注释以解释此处实际发生的情况” 选项来解决 LWG 3182,[concept.same] 改为将库实现更改为在表单中定义对“普通读者”有更清晰的语义

// 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>// A and B are concepts concept same_as = A ^ B ,而 same_as 和 {{1 }} 不包含 A


<块引用>

OP:第一个问题是为什么需要 B 概念?

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

B

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

答案 2 :(得分:1)

std::is_same仅在以下情况下定义为true:

  

T和U用相同的简历资格来命名相同的类型

据我所知,标准并没有定义“相同类型”的含义,但是在自然语言和逻辑中,“相同”是等价关系,因此是可交换的。

鉴于这个假设,我认为is_same_v<T, U> && is_same_v<U, V>确实是多余的。但是same_­as并未以is_same_v的形式指定;那只是为了说明。

对这两者的显式检查使same-as-impl的实现可以满足same_­as而无需交换。以这种方式指定它可以精确描述该概念的行为方式,而不会限制其实现方式。

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