看着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
相同?不是多余的吗?
答案 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_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>
会“爆炸”到不同的原子约束而变得不相等。
那么,为什么编译器会在意呢?
考虑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 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>;
代码会编译。
答案 1 :(得分:4)
[concept.same] 已更改为 LWG issue 3182 的一部分(在 Same
概念根据 P1754R1 重命名为 is_same
之前)[强调强>我的]:
讨论:
Same 概念在 18.4.2 [concept.same] 中的规范:
<块引用>template<class T, class U>
concept Same = is_same_v<T, U>;
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>;
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
D2
和 D1
都是约束声明,D2
的关联约束包含了 {{ 1}};或例如在以下示例中:
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
进行指定。所选择的方法的优点可以说是两个定义是解耦的。一个不依赖另一个。