Interesujące pytanie. Niedawno obserwowałem przemówienie Andrew Suttona na temat koncepcji, a podczas sesji pytań i odpowiedzi ktoś zadał następujące pytanie (znacznik czasu w poniższym linku):
CppCon 2018: Andrew Sutton „Koncepcje w 60 roku: wszystko, co musisz wiedzieć, a nic, czego nie wiesz”
Pytanie sprowadza się więc do: If I have a concept that says A && B && C, another says C && B && A, would those be equivalent?
Andrew odpowiedział tak, ale zwrócił uwagę na fakt, że kompilator ma pewne wewnętrzne metody (które są przejrzyste dla użytkownika) do dekompozycji pojęć na zdania logiczne atomowe ( atomic constraints
jak to sformułował Andrew) i sprawdzenie, czy są odpowiednik.
Teraz spójrz na to, co mówi cppreference std::same_as
:
std::same_as<T, U>
obejmuje std::same_as<U, T>
i na odwrót.
Jest to w zasadzie relacja „jeśli i tylko jeśli”: implikują się nawzajem. (Równoważność logiczna)
Moje przypuszczenie jest takie, że tutaj są ograniczenia atomowe std::is_same_v<T, U>
. Sposób, w jaki kompilatory traktują, std::is_same_v
może zmusić ich do myślenia std::is_same_v<T, U>
i std::is_same_v<U, T>
jako dwóch różnych ograniczeń (są to różne byty!). Więc jeśli implementujesz std::same_as
używając tylko jednego z nich:
template< class T, class U >
concept same_as = detail::SameHelper<T, U>;
Wtedy std::same_as<T, U>
i std::same_as<U, T>
„eksplodowałby” do różnych ograniczeń atomowych i nie stałby się równoważny.
Dlaczego kompilator się przejmuje?
Rozważ ten przykład :
#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;
}
Idealnie my_same_as<T, U> && std::integral<T>
podbiera my_same_as<U, T>
; dlatego kompilator powinien wybrać drugą specjalizację szablonu, z wyjątkiem ... nie robi tego: kompilator emituje błąd error: call of overloaded 'foo(int, int)' is ambiguous
.
Powodem tego jest to, że ponieważ my_same_as<U, T>
i my_same_as<T, U>
nie podciągnięcia siebie my_same_as<T, U> && std::integral<T>
i my_same_as<U, T>
stają się nieporównywalne (na częściowy porządek ograniczeń ze względu na relację subsumcji).
Jeśli jednak wymienisz
template< class T, class U >
concept my_same_as = SameHelper<T, U>;
z
template< class T, class U >
concept my_same_as = SameHelper<T, U> && SameHelper<U, T>;
Kod się kompiluje.
SameHelper<T, U>
może być prawdą, nie oznacza,SameHelper<U, T>
że może być.