Patrząc na możliwą implementację koncepcji same_as na https://en.cppreference.com/w/cpp/concepts/same_as zauważyłem, że dzieje się coś dziwnego.
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>;
Pierwsze pytanie brzmi: dlaczego SameHelper
koncepcja jest potrzebna? Po drugie, dlaczego same_as
sprawdza, czy T
jest taki sam jak U
i U
taki sam T
? Czy to nie jest zbędne?
SameHelper<T, U>
może być prawdą, nie oznacza,SameHelper<U, T>
że może być.is_same<T, U>::value == true
jeśli i tylko jeśliis_same<U, T>::value == true
”. Oznacza to, że ta podwójna kontrola nie jest koniecznaOdpowiedzi:
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
: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śleniastd::is_same_v<T, U>
istd::is_same_v<U, T>
jako dwóch różnych ograniczeń (są to różne byty!). Więc jeśli implementujeszstd::same_as
używając tylko jednego z nich:Wtedy
std::same_as<T, U>
istd::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 :
Idealnie
my_same_as<T, U> && std::integral<T>
podbieramy_same_as<U, T>
; dlatego kompilator powinien wybrać drugą specjalizację szablonu, z wyjątkiem ... nie robi tego: kompilator emituje błąderror: call of overloaded 'foo(int, int)' is ambiguous
.Powodem tego jest to, że ponieważ
my_same_as<U, T>
imy_same_as<T, U>
nie podciągnięcia siebiemy_same_as<T, U> && std::integral<T>
imy_same_as<U, T>
stają się nieporównywalne (na częściowy porządek ograniczeń ze względu na relację subsumcji).Jeśli jednak wymienisz
z
Kod się kompiluje.
źródło
SameHelper
: to sprawia, że dwa zastosowania zis_same_v
wywodzą się z tego samego wyrazu.is_same<T, U>
jest identycznyis_same<U, T>
, dwa wiązania atomowe nie są uważane za identyczne, chyba że są również utworzone z tego samego wyrażenia. Stąd potrzeba obu.are_same_as
?template<typename T, typename U0, typename... Un> concept are_same_as = SameAs<T, U0> && (SameAs<T, Un> && ...);
w niektórych przypadkach zawiedzie. Na przykładare_same_as<T, U, int>
byłoby równoważne,are_same_as<T, int, U>
ale nie doare_same_as<U, T, int>
std::is_same
jest zdefiniowane jako prawda wtedy i tylko wtedy, gdy:O ile mi wiadomo, standard nie definiuje znaczenia „tego samego typu”, ale w języku naturalnym i logice „ten sam” jest relacją równoważności, a zatem jest przemienny.
Biorąc pod uwagę to założenie, które przypisuję,
is_same_v<T, U> && is_same_v<U, V>
rzeczywiście byłoby zbędne. Alesame_as
nie jest określone w kategoriachis_same_v
; to tylko na wystawę.Wyraźne sprawdzenie obu pozwala na
same-as-impl
spełnienie implementacjisame_as
bez przemienności. Określenie go w ten sposób opisuje dokładnie, jak zachowuje się koncepcja, bez ograniczania jej implementacji.Nie wiem dokładnie, dlaczego wybrano to podejście zamiast sprecyzować
is_same_v
. Zaletą wybranego podejścia jest prawdopodobnie to, że dwie definicje są rozdzielone. Jedno nie zależy od drugiego.źródło