Why does same_as concept check type equality twice?

為{幸葍}努か 提交于 2019-12-03 23:52:34

Interesting question. I have recently watched Andrew Sutton's talk on Concepts, and in the Q&A session someone asked the following question (timestamp in the following link): CppCon 2018: Andrew Sutton “Concepts in 60: Everything you need to know and nothing you don't”

So the question boils down to: If I have a concept that says A && B && C, another says C && B && A, would those be equivalent? Andrew answered yes, but pointed out the fact the compiler has some internal methods (that is transparent to the user) to decompose the concepts into atomic logical propositions (atomic constraints as Andrew worded the term) and check whether they are equivalent.

Now look at what cppreference says about std::same_as:

std::same_as<T, U> subsumes std::same_as<U, T> and vice versa.

It is basically an "if-and-only-if" relationship: they imply each other. (Logical Equivalence)

My conjecture is that here the atomic constraints are std::is_same_v<T, U>. The way compilers treat std::is_same_v might make them think std::is_same_v<T, U> and std::is_same_v<U, T> as two different constraints (they are different entities!). So if you implement std::same_as using only one of them:

template< class T, class U >
concept same_as = detail::SameHelper<T, U>;

Then std::same_as<T, U> and std::same_as<U, T> would "explode" to different atomic constrains and become not equivalent.

Well, why does the compiler care?

Consider 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 >
// 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;
}

Ideally, my_same_as<T, U> && std::integral<T> subsumes my_same_as<U, T>; therefore, the compiler should select the second template specialization, except ... it does not: the compiler emits an error error: call of overloaded 'foo(int, int)' is ambiguous.

The reason behind this is that since my_same_as<U, T> and my_same_as<T, U> does not subsume each other, my_same_as<T, U> && std::integral<T> and my_same_as<U, T> become incomparable (on the partially ordered set of constraints under the relation of subsumption).

However, if you replace

template< class T, class U >
concept my_same_as = SameHelper<T, U>;

with

template< class T, class U >
concept my_same_as = SameHelper<T, U> && SameHelper<U, T>;

The code compiles.

std::is_same is defined as true if and only if:

T and U name the same type with the same cv-qualifications

As far as I know, standard doesn't define the meaning of "same type", but in natural language and logic "same" is an equivalence relation and thus is commutative.

Given this assumption, which I ascribe to, is_same_v<T, U> && is_same_v<U, V> would indeed be redundant. But same_­as is not specified in terms of is_same_v; that is only for exposition.

The explicit check for both allows for the implementation for same-as-impl to satisfy same_­as without being commutative. Specifying it this way describes exactly how the concept behaves without restricting how it could be implemented.

Exactly why this approach was chosen instead of specifying in terms of is_same_v, I don't know. An advantage of the chosen approach is arguably that the two definitions are de-coupled. One does not depend on the other.

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!