Warum überprüft das Konzept same_as die Typgleichheit zweimal?

19

Als ich mir die mögliche Implementierung des same_as-Konzepts unter https://en.cppreference.com/w/cpp/concepts/same_as ansah, bemerkte ich, dass etwas Seltsames passiert.

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

Die erste Frage ist, warum ein SameHelperKonzept nedded ist. Der zweite Grund ist, warum same_asüberprüft wird, ob Tes dasselbe ist wie Uund Udasselbe wie T? Ist es nicht überflüssig?

user7769147
quelle
Nur weil SameHelper<T, U>es wahr sein könnte, heißt das nicht, dass es wahr sein SameHelper<U, T>könnte.
Einige Programmierer Typ
1
das ist der Punkt, wenn a gleich b ist, ist b gleich a, nicht wahr?
user7769147
@ user7769147 Ja, und dies definiert diese Beziehung.
François Andrieux
4
Hmm, in der Dokumentation zu std :: is_same steht sogar "Kommutativität ist erfüllt, dh für zwei beliebige Typen T und U, is_same<T, U>::value == truewenn und nur wenn is_same<U, T>::value == true." Dies impliziert, dass diese doppelte Überprüfung nicht notwendig ist
Kevin
1
Nein, das ist falsch, sagt std :: is_same: Wenn und nur wenn die Bedingung erfüllt ist, sind zwei Typen kommutativ. Dies ist nicht unbedingt so. Aber ich finde kein Beispiel für zwei nicht kommutative Typen.
Nemanja Boric

Antworten:

16

Interessante Frage. Ich habe kürzlich Andrew Suttons Vortrag über Konzepte gesehen und in der Q & A-Sitzung hat jemand die folgende Frage gestellt (Zeitstempel im folgenden Link): CppCon 2018: Andrew Sutton „Konzepte in 60: Alles, was Sie wissen müssen und nichts, was Sie nicht wissen“

Die Frage läuft also darauf hinaus: If I have a concept that says A && B && C, another says C && B && A, would those be equivalent?Andrew antwortete mit Ja, wies jedoch darauf hin, dass der Compiler über einige interne Methoden verfügt (die für den Benutzer transparent sind), um die Konzepte in atomare logische Sätze zu zerlegen ( atomic constraintswie Andrew den Begriff formulierte) und zu prüfen, ob dies der Fall ist Äquivalent.

Schauen Sie sich nun an, was cppreference über Folgendes sagt std::same_as:

std::same_as<T, U>subsumiert std::same_as<U, T>und umgekehrt.

Es ist im Grunde eine "Wenn-und-Nur-Wenn" -Beziehung: Sie implizieren sich gegenseitig. (Logische Äquivalenz)

Meine Vermutung ist, dass hier die atomaren Zwänge sind std::is_same_v<T, U>. Die Art und Weise, wie Compiler damit umgehen, std::is_same_vkönnte sie zum Nachdenken anregen std::is_same_v<T, U>und std::is_same_v<U, T>als zwei verschiedene Einschränkungen (sie sind verschiedene Einheiten!). Wenn Sie also std::same_asnur eine davon implementieren :

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

Dann std::same_as<T, U>und std::same_as<U, T>würde zu verschiedenen atomaren Zwängen "explodieren" und nicht gleichwertig werden.

Warum kümmert es den Compiler?

Betrachten Sie dieses Beispiel :

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

Idealerweise my_same_as<T, U> && std::integral<T>subsumiert my_same_as<U, T>; Daher sollte der Compiler die zweite Vorlagenspezialisierung auswählen, außer ... nicht: Der Compiler gibt einen Fehler aus error: call of overloaded 'foo(int, int)' is ambiguous.

Der Grund dafür ist , dass da my_same_as<U, T>und my_same_as<T, U>nicht sie nicht subsumieren, my_same_as<T, U> && std::integral<T>und my_same_as<U, T>unvergleichlich werden (auf dem teilweise geordneten Satz von Einschränkungen nach der Beziehung von Subsumtion).

Wenn Sie jedoch ersetzen

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

mit

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

Der Code wird kompiliert.

Rin Kaenbyou
quelle
same_as <T, U> und same_as <U, T> könnten auch unterschiedliche atomare Beschränkungen sein, aber ihr Ergebnis wäre immer noch dasselbe. Warum ist es dem Compiler so wichtig, same_as als zwei verschiedene atomare Einschränkungen zu definieren, die aus logischer Sicht gleich sind?
user7769147
2
Der Compiler ist erforderlich zu prüfen , alle zwei Ausdrücke im Unterschied zur Einschränkung Subsumtion, aber es kann prüfen Argumente , um sie in der offensichtlichen Art und Weise. Wir brauchen also nicht nur beide Richtungen (damit es beim Vergleich von Einschränkungen keine Rolle spielt, in welcher Reihenfolge sie benannt werden), SameHelpersondern auch : Die beiden Verwendungen von is_same_vleiten sich aus demselben Ausdruck ab.
Davis Herring
@ user7769147 Siehe aktualisierte Antwort.
Rin Kaenbyou
1
Es scheint, dass konventionelle Weisheit in Bezug auf die Konzeptgleichheit falsch ist. Im Gegensatz zu Vorlagen, mit denen is_same<T, U>identisch ist is_same<U, T>, werden zwei atomare Einschränkungen nur dann als identisch angesehen, wenn sie auch aus demselben Ausdruck gebildet werden. Daher die Notwendigkeit für beide.
AndyG
Was ist mit are_same_as? template<typename T, typename U0, typename... Un> concept are_same_as = SameAs<T, U0> && (SameAs<T, Un> && ...);würde in einigen Fällen scheitern. Zum Beispiel are_same_as<T, U, int>wäre äquivalent zu, are_same_as<T, int, U>aber nicht zuare_same_as<U, T, int>
user7769147
2

std::is_same wird genau dann als wahr definiert, wenn:

T und U nennen den gleichen Typ mit den gleichen Lebenslaufqualifikationen

Soweit ich weiß, definiert Standard nicht die Bedeutung von "gleichem Typ", aber in natürlicher Sprache und Logik ist "gleich" eine Äquivalenzbeziehung und daher kommutativ.

Angesichts dieser Annahme, die ich zuschreibe, is_same_v<T, U> && is_same_v<U, V>wäre dies in der Tat überflüssig. Ist same_­asaber nicht spezifiziert in Bezug auf is_same_v; das ist nur zur Darstellung.

Die explizite Prüfung für beide ermöglicht es, dass die Implementierung same-as-implerfüllt wird, same_­asohne kommutativ zu sein. Wenn Sie es auf diese Weise angeben, wird genau beschrieben, wie sich das Konzept verhält, ohne die Implementierung einzuschränken.

is_same_vIch weiß nicht genau, warum dieser Ansatz gewählt wurde, anstatt ihn zu spezifizieren . Ein Vorteil des gewählten Ansatzes besteht wohl darin, dass die beiden Definitionen entkoppelt sind. Eins hängt nicht vom anderen ab.

Eerorika
quelle
2
Ich stimme Ihnen zu, aber dieses letzte Argument ist ein bisschen langwierig. Für mich klingt es so: "Hey, ich habe diese wiederverwendbare Komponente, die mir sagt, ob zwei Typen gleich sind. Jetzt habe ich diese andere Komponente, die wissen muss, ob die Typen gleich sind, aber anstatt meine vorherige Komponente wiederzuverwenden Ich werde nur eine Ad-hoc-Lösung für diesen Fall erstellen. Jetzt habe ich den Mann, der die Definition von Gleichheit benötigt, von dem Mann, der die Definition von Gleichheit hat, "entkoppelt". Yay! "
Cássio Renan
1
@ CássioRenan Sicher. Wie ich schon sagte, ich weiß nicht warum, das ist nur die beste Argumentation, die ich mir einfallen lassen könnte. Die Autoren haben möglicherweise eine bessere Begründung.
Eerorika