Dlaczego koncepcja same_as sprawdza dwukrotnie równość typu?

19

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 SameHelperkoncepcja jest potrzebna? Po drugie, dlaczego same_assprawdza, czy Tjest taki sam jak Ui Utaki sam T? Czy to nie jest zbędne?

użytkownik7769147
źródło
To, że SameHelper<T, U>może być prawdą, nie oznacza, SameHelper<U, T>że może być.
Jakiś programista koleś
1
o to chodzi, jeśli a jest równe b, b jest równe a, prawda?
user7769147,
@ user7769147 Tak, i to definiuje tę relację.
François Andrieux
4
Hmm dokumentacja dla std :: is_same mówi nawet: „ Komutatywność jest spełniona, tj. Dla dowolnych dwóch typów T i U, is_same<T, U>::value == truejeśli i tylko jeśli is_same<U, T>::value == true”. Oznacza to, że ta podwójna kontrola nie jest konieczna
Kevin
1
Nie, to źle, std :: is_same mówi: jeśli tylko warunek się utrzymuje, dwa typy są przemienne. Niekoniecznie tak jest. Ale nie udało mi się znaleźć przykładu dwóch typów nieprzemiennych.
Nemanja Boric

Odpowiedzi:

16

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 constraintsjak 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_vmoż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_asuż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.

Rin Kaenbyou
źródło
same_as <T, U> i same_as <U, T> mogą również być różnymi wiązaniami atomowymi, ale ich wynik byłby nadal taki sam. Dlaczego kompilatorowi zależy na zdefiniowaniu tego samego jako dwóch różnych ograniczeń atomowych, które z logicznego punktu widzenia są takie same?
user7769147,
2
Kompilator jest zobowiązany do uwzględnienia dowolnych dwóch wyrażeń jako odrębnych dla subsumcji ograniczeń, ale może w sposób oczywisty rozważyć argumenty dla nich. Więc nie tylko musimy w obu kierunkach (tak, że nie ma znaczenia, w jakiej kolejności są one nazwane przy porównywaniu ograniczeń), musimy także SameHelper: to sprawia, że dwa zastosowania z is_same_vwywodzą się z tego samego wyrazu.
Davis Herring
@ user7769147 Zobacz zaktualizowaną odpowiedź.
Rin Kaenbyou,
1
Wydaje się, że konwencjonalna mądrość jest błędna w odniesieniu do równości koncepcji. W przeciwieństwie do szablonów, w których is_same<T, U>jest identyczny is_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.
AndyG,
Co 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ład are_same_as<T, U, int>byłoby równoważne, are_same_as<T, int, U>ale nie doare_same_as<U, T, int>
user7769147
2

std::is_same jest zdefiniowane jako prawda wtedy i tylko wtedy, gdy:

T i U wymieniają ten sam typ z tymi samymi kwalifikacjami cv

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. Ale same_­asnie jest określone w kategoriach is_same_v; to tylko na wystawę.

Wyraźne sprawdzenie obu pozwala na same-as-implspełnienie implementacji same_­asbez 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.

eerorika
źródło
2
Zgadzam się z tobą, ale ten ostatni argument jest trochę rozciągnięty. Dla mnie brzmi to tak: „Hej, mam ten komponent wielokrotnego użytku, który mówi mi, czy dwa typy są takie same. Teraz mam ten inny komponent, który musi wiedzieć, czy typy są takie same, ale zamiast ponownego użycia mojego poprzedniego komponentu , Po prostu stworzę rozwiązanie ad hoc właściwe dla tego przypadku. Teraz „oddzieliłem” faceta, który potrzebuje definicji równości, od faceta, który ma definicję równości. Tak! ”
Cássio Renan
1
@ CássioRenan Sure. Tak jak powiedziałem, nie wiem dlaczego, to po prostu najlepsze rozumowanie, jakie mogłem wymyślić. Autorzy mogą mieć lepsze uzasadnienie.
eerorika