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


19

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


仅仅因为SameHelper<T, U>可能是真实的并不意味着SameHelper<U, T>可能就会如此。
一些程序员花花公子

1
关键是,如果a等于b,b等于a,不是吗?
user7769147

@ user7769147是的,这正在定义该关系。
弗朗索瓦·安德列

4
嗯,std :: is_same的文档甚至说“满足通讯性,即,is_same<T, U>::value == true当且仅当满足T和U的任何两个类型is_same<U, T>::value == true。” 这意味着不需要进行双重检查
凯文

1
不,这是错误的,std :: is_same说:当且仅当条件成立时,两种类型是可交换的。不一定是这样。但是我找不到两个非交换类型的例子。
Nemanja Boric

Answers:


16

有趣的问题。我最近看了安德鲁·萨顿(Andrew Sutton)关于概念的演讲,在问答环节中,有人问了以下问题(以下链接中的时间戳): CppCon 2018:安德鲁·萨顿“ 60岁的概念:您需要知道的一切,而您不需要的一切”

因此问题归结为: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>将“爆炸”到不同的原子约束,并且变得不等效。

好吧,为什么编译器会关心?

考虑这个例子

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

代码会编译。


same_as <T,U>和same_as <U,T>也可以是不同的原子约束,但是它们的结果仍然相同。为什么编译器非常关心将same_as定义为两个不同的原子约束,从逻辑的角度来看它们是相同的?
user7769147

2
编译器需要考虑任何两个表达式作为不同的约束包容,但可以考虑参数显而易见的方式给他们。因此,我们不仅需要两个方向(因此在比较约束时它们的命名顺序无关紧要),我们还需要SameHelper:它使来自同一表达式的两种用法is_same_v可以使用
戴维斯·鲱鱼

@ user7769147请参阅更新的答案。
Rin Kaenbyou

1
在概念平等方面,传统观念似乎是错误的。与与is_same<T, U>相同的模板不同is_same<U, T>,除非两个原子约束也由同一表达式形成,否则它们不会被视为相同。因此,两者都需要。
AndyG '19

are_same_astemplate<typename T, typename U0, typename... Un> concept are_same_as = SameAs<T, U0> && (SameAs<T, Un> && ...);在某些情况下会失败。例如are_same_as<T, U, int>,它等同于are_same_as<T, int, U>但不等同于are_same_as<U, T, int>
user7769147 '19

2

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我不知道为什么选择这种方法而不是根据进行指定。所选择的方法的优点可以说是两个定义是解耦的。一个不依赖另一个。


2
我同意你的看法,但是最后一个论点有点牵强。对我来说,这听起来像是:“嘿,我有这个可重用的组件,它告诉我两种类型是否相同。现在,我有另一个组件,需要知道类型是否相同,但是不要重用我以前的组件,我将只针对这种情况创建一个临时解决方案。现在,我已经将需要平等定义的人与具有平等定义的人“分离”了。
卡西欧雷南

1
@CássioRenan好的。就像我说的,我不知道为什么,这是我可以提出的最好的理由。作者可能有更好的理由。
eerorika
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.