为什么 same_as concept 检查类型相等性两次?
Why does same_as concept check type equality twice?
在 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
相同?不是多余的吗?
std::is_same
定义为真当且仅当:
T and U name the same type with the same cv-qualifications
据我所知,标准没有定义"same type"的含义,但在自然语言和逻辑中"same"是一个等价关系,因此是可交换的。
鉴于我归因于这个假设,is_same_v<T, U> && is_same_v<U, V>
确实是多余的。但是same_as
在is_same_v
方面没有指定;那只是为了说明。
对两者的显式检查允许 same-as-impl
的实现满足 same_as
而无需交换。以这种方式指定它准确地描述了概念的行为方式,而不限制它的实现方式。
我不知道为什么选择这种方法而不是根据 is_same_v
进行指定。所选方法的一个优点可以说是这两个定义是分离的。一个不依赖另一个。
有趣的问题。最近看了Andrew Sutton关于Concepts的演讲,在问答环节有人问了下面的问题(时间戳在下面link):
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?
安德鲁回答是,但指出编译器有一些内部方法(对用户透明)将概念分解为原子逻辑命题(atomic constraints
正如 Andrew 所说的那样)并检查它们是否等价。
现在看看cppreference是怎么说的 std::same_as
:
std::same_as<T, U>
subsumes std::same_as<U, T>
and vice versa.
这基本上是一种“如果且仅当”的关系:它们相互暗示。 (逻辑等价)
我猜想这里的原子约束是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>
;因此,编译器应该 select 第二个模板专业化,除了......它没有:编译器发出错误 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>;
代码编译。
[concept.same] 已更改为 LWG issue 3182 (before the concept Same
was renamed to is_same
as per P1754R1 的一部分)[强调 我的]:
3182. Specification of Same could be clearer
- Section: 18.4.2 [concept.same]
- Status: WP
- [...]
Discussion:
The specification of the Same concept in 18.4.2 [concept.same]:
template<class T, class U>
concept Same = is_same_v<T, U>;
Same<T, U>
subsumes Same<U, T>
and vice versa.
似乎自相矛盾。单从概念定义来看,并不是
Same<T, U>
包含 Same<U, T>
的情况,反之亦然。段落
1 试图告诉我们 有一些魔法可以提供
声明的包含关系,但对于一个不经意的reader来说似乎
是一个错误注释的笔记。我们应该添加注释来解释
这里实际发生了什么,或者以这种方式定义概念
它自然地提供了指定的包含关系。
鉴于对称包含习语有一个简单的库实现,后一种选择似乎更可取。
[...]
提议的决议:
这个写法是相对于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>;
- [Note:
Same<T, U>
subsumes Same<U, T>
and vice versa. — end note]
我将开始解决 OP 的第二个问题(因为第一个问题的答案将随之而来):
OP: The second is why same_as
checks if T
is the same as U
and U
the same as T
? Isn't it redundant?
根据上面强调的最后一部分:
[...] Given that there's a straightforward library implementation of the symmetric subsumption idiom, the latter option seems preferable.
CWG 3182 的决议是重新定义库规范以使用两个对称约束,以(语义上)自然的方式专门实现两者之间的包含关系(“对称包含习语”,如果您愿意的话)。
作为切线(但与回答 OP 的第一个问题相关),这对于约束的部分排序很重要,根据 [temp.constr.order], particularly [temp.constr.order]/1 and [temp.constr.order]/3
/1 A constraint P
subsumes a constraint Q
if and only if, [...] [ Example: Let A and B be atomic constraints. The constraint A ∧ B
subsumes A
, but A
does not subsume A ∧ B
. The constraint A
subsumes A ∨ B
, but A ∨ B
does not subsume A
. Also note that every constraint subsumes itself. — end example ]
/3 A declaration D1
is at least as constrained as a declaration D2
if
- (3.1)
D1
and D2
are both constrained declarations and D1
's associated constraints subsume those of D2
; or
- (3.2) D2 has no associated constraints.
例如在下面的例子中:
#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
、C1<T> && C2<T>
处的约束包含在#2
、C1<T>
,反之则不然。
然而,我们可以深入 [temp.constr.order] 和 [temp.constr.atomic] 的兔子洞,以证明即使在 same_as
:[=72= 的旧实现中]
// 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>
,反之亦然;然而,这并不完全是微不足道的。
因此,不是选择 的选项“添加注释来解释这里实际发生的事情” 来解决 LWG 3182,[concept.same] 而是更改了库以语义比“随意reader”:
更清晰的形式定义实现
// A and B are concepts
concept same_as = A ^ B
根据上面的(切线)部分,我们可能还注意到 same_as
包含概念 A
和 B
两个孤立的概念,而 A
和 B
孤立地不包含 same_as
.
OP: The first question is why a SameHelper
concept is nedded?
根据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 时,意图是按照上述语义显示包含关系,添加了一个中间概念以强调包含。
在 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
相同?不是多余的吗?
std::is_same
定义为真当且仅当:
T and U name the same type with the same cv-qualifications
据我所知,标准没有定义"same type"的含义,但在自然语言和逻辑中"same"是一个等价关系,因此是可交换的。
鉴于我归因于这个假设,is_same_v<T, U> && is_same_v<U, V>
确实是多余的。但是same_as
在is_same_v
方面没有指定;那只是为了说明。
对两者的显式检查允许 same-as-impl
的实现满足 same_as
而无需交换。以这种方式指定它准确地描述了概念的行为方式,而不限制它的实现方式。
我不知道为什么选择这种方法而不是根据 is_same_v
进行指定。所选方法的一个优点可以说是这两个定义是分离的。一个不依赖另一个。
有趣的问题。最近看了Andrew Sutton关于Concepts的演讲,在问答环节有人问了下面的问题(时间戳在下面link): 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?
安德鲁回答是,但指出编译器有一些内部方法(对用户透明)将概念分解为原子逻辑命题(atomic constraints
正如 Andrew 所说的那样)并检查它们是否等价。
现在看看cppreference是怎么说的 std::same_as
:
std::same_as<T, U>
subsumesstd::same_as<U, T>
and vice versa.
这基本上是一种“如果且仅当”的关系:它们相互暗示。 (逻辑等价)
我猜想这里的原子约束是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>
;因此,编译器应该 select 第二个模板专业化,除了......它没有:编译器发出错误 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>;
代码编译。
[concept.same] 已更改为 LWG issue 3182 (before the concept Same
was renamed to is_same
as per P1754R1 的一部分)[强调 我的]:
3182. Specification of Same could be clearer
- Section: 18.4.2 [concept.same]
- Status: WP
- [...]
Discussion:
The specification of the Same concept in 18.4.2 [concept.same]:
template<class T, class U> concept Same = is_same_v<T, U>;
Same<T, U>
subsumesSame<U, T>
and vice versa.似乎自相矛盾。单从概念定义来看,并不是
Same<T, U>
包含Same<U, T>
的情况,反之亦然。段落 1 试图告诉我们 有一些魔法可以提供 声明的包含关系,但对于一个不经意的reader来说似乎 是一个错误注释的笔记。我们应该添加注释来解释 这里实际发生了什么,或者以这种方式定义概念 它自然地提供了指定的包含关系。鉴于对称包含习语有一个简单的库实现,后一种选择似乎更可取。
[...]
提议的决议:
这个写法是相对于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>;
- [Note:
Same<T, U>
subsumesSame<U, T>
and vice versa. — end note]
我将开始解决 OP 的第二个问题(因为第一个问题的答案将随之而来):
OP: The second is why
same_as
checks ifT
is the same asU
andU
the same asT
? Isn't it redundant?
根据上面强调的最后一部分:
[...] Given that there's a straightforward library implementation of the symmetric subsumption idiom, the latter option seems preferable.
CWG 3182 的决议是重新定义库规范以使用两个对称约束,以(语义上)自然的方式专门实现两者之间的包含关系(“对称包含习语”,如果您愿意的话)。
作为切线(但与回答 OP 的第一个问题相关),这对于约束的部分排序很重要,根据 [temp.constr.order], particularly [temp.constr.order]/1 and [temp.constr.order]/3
/1 A constraint
P
subsumes a constraintQ
if and only if, [...] [ Example: Let A and B be atomic constraints. The constraintA ∧ B
subsumesA
, butA
does not subsumeA ∧ B
. The constraintA
subsumesA ∨ B
, butA ∨ B
does not subsumeA
. Also note that every constraint subsumes itself. — end example ]/3 A declaration
D1
is at least as constrained as a declarationD2
if
- (3.1)
D1
andD2
are both constrained declarations andD1
's associated constraints subsume those ofD2
; or- (3.2) D2 has no associated constraints.
例如在下面的例子中:
#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
、C1<T> && C2<T>
处的约束包含在#2
、C1<T>
,反之则不然。
然而,我们可以深入 [temp.constr.order] 和 [temp.constr.atomic] 的兔子洞,以证明即使在 same_as
:[=72= 的旧实现中]
// 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>
,反之亦然;然而,这并不完全是微不足道的。
因此,不是选择 的选项“添加注释来解释这里实际发生的事情” 来解决 LWG 3182,[concept.same] 而是更改了库以语义比“随意reader”:
更清晰的形式定义实现// A and B are concepts
concept same_as = A ^ B
根据上面的(切线)部分,我们可能还注意到 same_as
包含概念 A
和 B
两个孤立的概念,而 A
和 B
孤立地不包含 same_as
.
OP: The first question is why a
SameHelper
concept is nedded?
根据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 时,意图是按照上述语义显示包含关系,添加了一个中间概念以强调包含。