【问题标题】:Why does same_as concept check type equality twice?为什么 same_as 概念会两次检查类型相等性?
【发布时间】:2020-02-18 21:24:18
【问题描述】:

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

【问题讨论】:

  • SameHelper&lt;T, U&gt; 可能是真的并不意味着 SameHelper&lt;U, T&gt; 可能是。
  • 这就是重点,如果a等于b,b等于a不是吗?
  • @user7769147 是的,这就是定义这种关系。
  • 嗯,std::is_same 的文档甚至说“满足交换性,即对于任何两种类型 T 和 U,is_same&lt;T, U&gt;::value == true 当且仅当 is_same&lt;U, T&gt;::value == true。”这意味着不需要这种双重检查
  • 不,这是错误的,std::is_same 说:当且仅当条件成立时,两种类型是可交换的。不一定如此。但是我找不到两个非交换类型的例子。

标签: c++ language-lawyer c++20 concept


【解决方案1】:

有趣的问题。我最近看了 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 &amp;&amp; B &amp;&amp; C, another says C &amp;&amp; B &amp;&amp; A, would those be equivalent?Andrew 回答是,但指出编译器有一些内部方法(对用户透明)将概念分解为原子逻辑命题(atomic constraints as Andrew措辞)并检查它们是否等效。

现在看看 cppreference 对std::same_as 的评价:

std::same_as&lt;T, U&gt; 包含std::same_as&lt;U, T&gt;,反之亦然。

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

我的猜想是这里的原子约束是std::is_same_v&lt;T, U&gt;。编译器对待std::is_same_v 的方式可能会让他们认为std::is_same_v&lt;T, U&gt;std::is_same_v&lt;U, T&gt; 是两个不同的约束(它们是不同的实体!)。因此,如果您只使用其中一个来实现std::same_as

template< class T, class U >
concept same_as = detail::SameHelper<T, U>;

然后std::same_as&lt;T, U&gt;std::same_as&lt;U, T&gt; 将“爆炸”到不同的原子约束并变得不等价。

那么,为什么编译器会关心?

考虑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&lt;T, U&gt; &amp;&amp; std::integral&lt;T&gt; 包含 my_same_as&lt;U, T&gt;;因此,编译器应该选择第二个模板特化,除了......它没有:编译器发出错误error: call of overloaded 'foo(int, int)' is ambiguous

这背后的原因是由于my_same_as&lt;U, T&gt;my_same_as&lt;T, U&gt;不相互包含,my_same_as&lt;T, U&gt; &amp;&amp; std::integral&lt;T&gt;my_same_as&lt;U, T&gt;变得不可比(在包含关系下的偏序约束集上)。

但是,如果你替换

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

代码编译。

【讨论】:

  • same_as 和 same_as 也可以是不同的原子约束,但它们的结果仍然相同。为什么编译器如此关心将 same_as 定义为从逻辑角度来看是相同的两个不同的原子约束?
  • 编译器要求any两个表达式视为不同的约束包含,但它可以考虑arguments他们以明显的方式。所以我们不仅需要两个方向(以便在比较约束时它们的命名顺序无关紧要),我们还需要 SameHelper:它使两个 使用 of is_same_v 派生自同一个表达式。
  • 在概念平等方面,传统智慧似乎是错误的。与is_same&lt;T, U&gt;is_same&lt;U, T&gt; 相同的模板不同,两个原子约束不被视为相同,除非它们也由相同的表达式形成。因此两者都需要。
  • are_same_as 怎么样? template&lt;typename T, typename U0, typename... Un&gt; concept are_same_as = SameAs&lt;T, U0&gt; &amp;&amp; (SameAs&lt;T, Un&gt; &amp;&amp; ...); 在某些情况下会失败。例如are_same_as&lt;T, U, int&gt; 将等同于are_same_as&lt;T, int, U&gt; 但不等同于are_same_as&lt;U, T, int&gt;
  • 此外,概念不能递归引用自身,因此不允许使用 template&lt;typename T, typename U0, typename... Un&gt; concept are_same_as = SameAs&lt;T, U0&gt; &amp;&amp; (SameAs&lt;T, Un&gt; &amp;&amp; ...) &amp;&amp; (sizeof...(Un) == 0 || are_same_as&lt;U, Un...&gt;);
【解决方案2】:

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

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

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

讨论:

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

template<class T, class U>
  concept Same = is_same_v<T, U>;
  1. Same&lt;T, U&gt; 包含 Same&lt;U, T&gt;,反之亦然。

似乎矛盾。仅从概念定义来看,它不是 Same&lt;T, U&gt; 包含 Same&lt;U, T&gt; 的情况,反之亦然。段落 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&lt;T, U&gt; 包含Same&lt;U, T&gt;,反之亦然。 ——尾注]

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

OP:第二个是为什么same_as会检查T是否与U相同,U是否与T相同?是不是多余的?

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

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

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

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

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

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

一样受约束
  • (3.1) D1D2 都是受约束的声明D1相关约束包含 D2 的约束;或
  • (3.2) 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&lt;int&gt;() 的调用是不明确的(将调用#1),因为#1C1&lt;T&gt; &amp;&amp; C2&lt;T&gt; 的约束包含#2C1&lt;T&gt; 的约束,但不是反之亦然。

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

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

same_as&lt;T, U&gt; 仍将包含same_as&lt;U, T&gt;,反之亦然;然而,这并不完全是微不足道的。

因此,为了解决 LWG 3182,[concept.same] 没有选择 “添加注释以解释此处实际发生的情况”的选项,而是将库实现更改为以表格形式定义对“休闲读者”来说具有更清晰的语义

// A and B are concepts
concept same_as = A ^ B

根据上面的(切线)部分,我们还可以注意到,same_as 包含两个孤立的概念 AB,而孤立的 AB 不包含 same_as .


OP:第一个问题是为什么要引入SameHelper 概念?

根据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.对于&amp;&amp;,因为类型特征不能包含类型特征。当 LWG 3182 被解析时,意图是在语义上显示上述的包含关系,添加了一个中间概念来强调包含。

【讨论】:

【解决方案3】:

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

T 和 U 用相同的 cv-qualifications 命名相同的类型

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

鉴于我认为的这个假设,is_same_v&lt;T, U&gt; &amp;&amp; is_same_v&lt;U, V&gt; 确实是多余的。但是same_­as不是以is_same_v的形式指定的;那只是为了展示。

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

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

【讨论】:

  • 我同意你的观点,但最后一个论点有点牵强。对我来说,这听起来像:“嘿,我有这个可重用的组件,它告诉我两种类型是否相同。现在我有另一个需要知道类型是否相同的组件,但是,而不是重用我以前的组件,我将创建一个专门针对这种情况的临时解决方案。现在我已经将需要平等定义的人与有平等定义的人“分离”了。耶!”
  • @CássioRenan 当然。就像我说的,我不知道为什么,这只是我能想到的最好的推理。作者可能有更好的理由。
猜你喜欢
  • 2020-10-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-28
相关资源
最近更新 更多