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

wfypjpf4  于 2023-03-05  发布在  其他
关注(0)|答案(3)|浏览(155)

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

mrzz3bfm

mrzz3bfm1#

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

3182.同规格可更清楚

  • 章节:18.4.2 [概念相同]
  • 状态:WP
  • [...]
    • 讨论:**

18.4.2 [概念相同]中相同概念的规范:

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 [概念相同]如下:

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:**第二个是为什么same_as检查T是否与U相同,U是否与T相同?这不是多余的吗?

根据上文强调的最后一部分:
[...]考虑到对称包含习惯用法有一个简单的库实现,后一种选择似乎更可取。
对CWG3182的解决方案是重新定义库规范以使用两个对称约束,特别是以(语义上)自然的方式来满足两者之间的包含关系(如果你愿意,可以称之为"对称包含习惯用法")。
作为切线(但与回答OP的第一个问题相关),这对于按temp.constr.order(https://timsong-cpp.github.io/cppwp/n4861/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**一样受约束,如果

  • (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<int>()的调用不是二义性的(将调用#1),因为#1C1<T> && C2<T>处的约束包含#2C1<T>处的约束,但反之亦然。
然而,我们可以通过[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<T, U>仍将包含same_as<U, T>,反之亦然;然而,这并非完全无关紧要。
因此,[concept. same]没有选择 "添加一个注解来解释这里实际发生了什么" 来解决LWG 3182,而是将库实现改为以一种对 "临时读者" 来说 * 具有更清晰 * 语义 * 的形式来定义:

// 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.,因为类型特征不能包含类型特征。当解决LWG 3182时,意图是语义上显示如上所述的包含关系,添加中间概念以强调包含。

nwlqm0z1

nwlqm0z12#

std::is_same定义为真,当且仅当:
T和U使用相同的cv限定命名相同的类型
据我所知,标准没有定义“同类”的含义,但在自然语言和逻辑中,“相同”是一种等价关系,因而是可交换的。
给定我所归属的这个假设,is_same_v<T, U> && is_same_v<U, V>确实是多余的,但是same_­as不是用is_same_v来指定的;那只是为了说明。
对两者的显式检查允许same-as-impl的实现满足same_­as,而不是交换的。以这种方式指定它精确地描述了概念的行为,而不限制它的实现方式。
我不知道为什么选择这种方法而不是用is_same_v来定义,这种方法的优点是两个定义是分离的,一个不依赖于另一个。

gcxthw6b

gcxthw6b3#

有意思的问题。我最近看了Andrew萨顿关于概念的演讲,在问答环节有人问了下面的问题(时间戳在下面的链接中):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? 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>;

那么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>;因此,编译器应该选择第二个模板专门化,除非...它没有:编译器发出错误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>;

代码编译完毕。

相关问题