在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条答案
按热度按时间mrzz3bfm1#
[concept. same]作为LWG issue 3182的一部分进行了更改(在根据P1754R1将概念
Same
重命名为is_same
之前)[强调我的]:3182.同规格可更清楚
18.4.2 [概念相同]中相同概念的规范:
Same<T, U>
包含Same<U, T>
,反之亦然。似乎是矛盾的。单从概念定义来看,
Same<T, U>
包含Same<U, T>
,反之亦然,都不是这样的情况。第1段试图告诉我们有某种魔力提供了所述的包含关系,但对于一个不经意的读者来说,这似乎是一个错误的注解。我们要么添加一个注解来解释这里实际发生了什么,* * 或以自然提供指定包含关系的方式定义概念。**[...]
该措辞与N4791相关。
变更18.4.2 [概念相同]如下:
1.[注:
Same<T, U>
包含Same<U, T>
,反之亦然。-结束注]我将开始讨论《任择议定书》的第二个问题(因为第一个问题的答案将由此而来):
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
**一样受约束,如果D1
和D2
都是约束声明,并且D1
的关联约束包含D2
的关联约束;或使得在下面的示例中:
对例如
f<int>()
的调用不是二义性的(将调用#1
),因为#1
、C1<T> && C2<T>
处的约束包含#2
、C1<T>
处的约束,但反之亦然。然而,我们可以通过[temp.constr.order]和[temp.constr.atomic]的兔子洞来证明,即使在
same_as
的旧实现中:same_as<T, U>
仍将包含same_as<U, T>
,反之亦然;然而,这并非完全无关紧要。因此,[concept. same]没有选择 "添加一个注解来解释这里实际发生了什么" 来解决LWG 3182,而是将库实现改为以一种对 "临时读者" 来说 * 具有更清晰 * 语义 * 的形式来定义:
按照上面的(切向的)部分,我们还可以注意到,
same_as
包含孤立的A
和B
这两个概念,而孤立的A
和B
不包含same_as
。SameHelper
概念?根据temp. constr. order ]/1,只能包含概念。因此,对于概念的旧实现,其中直接使用
is_same
转换特性(不是概念),特性本身不属于包含规则。实现的含义如下:将真正包含
&&
的冗余r.h.s.,因为类型特征不能包含类型特征。当解决LWG 3182时,意图是语义上显示如上所述的包含关系,添加中间概念以强调包含。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
来定义,这种方法的优点是两个定义是分离的,一个不依赖于另一个。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
:那么
std::same_as<T, U>
和std::same_as<U, T>
将“爆炸”到不同的原子约束,变得不等价。那么,编译器为什么要关心呢?
考虑this example:
理想情况下,
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>
变得不可比(在包含关系下的约束的偏序集上)。但是,如果您将
与
代码编译完毕。