Ich studiere diese faszinierende Antwort zu subtile Frage in Bezug auf die bewährte Methode zum Implementieren der Funktion swap
für benutzerdefinierte Typen . (Meine Frage wurde ursprünglich durch eine Diskussion über die Illegalität des Hinzufügens von Typen zum Namespace std
motiviert.)
Ich werde das Code-Snippet nicht von der oben verlinkten Antwort hier ausdrucken.
Stattdessen möchte ich die Antwort verstehen .
Die Antwort, die ich oben verlinkt habe, gibt unter dem ersten Codeausschnitt an, überladen swap
in namespace std
(anstatt darauf zu spezialisieren ) Namensraum):
Wenn Ihr Compiler etwas anderes ausgibt, ist es das nicht "Zwei-Phasen-Lookup" für Vorlagen richtig implementieren.
Die Antwort geht dann darauf hinaus, dass specializing swap
in namespace std
(im Gegensatz zu overloading es) ein anderes Ergebnis erzeugt (das gewünschte Ergebnis im Falle von Spezialisierung ).
Die Antwort wird jedoch mit einem zusätzlichen Fall fortgesetzt: spezialisierter Swap für eine benutzerdefinierte Vorlage Klasse - in diesem Fall wird wiederum das gewünschte Ergebnis nicht erreicht.
Leider sagt die Antwort einfach die Fakten ; es erklärt nicht warum .
Kann jemand diese Antwort näher erläutern und den Suchvorgang in den zwei spezifischen Code-Snippets beschreiben, die in dieser Antwort enthalten sind:
Überladen von swap
in namespace std
für eine benutzerdefinierte Nicht-Template-Klasse (wie im ersten Code-Snippet der verknüpften Antwort)
spezialisiert auf swap
in namespace std
für eine benutzerdefinierte Vorlagenklasse (wie im endgültigen Codeausschnitt der verknüpften Antwort)
In beiden Fällen wird das generische std::swap
und nicht das benutzerdefinierte swap
aufgerufen. Warum?
(Dies wird Aufschluss geben über die Art der Zweiphasen-Suche und den Grund für die Best Practice zur Implementierung von benutzerdefinierten swap
; danke.)
Der Aufruf von swap()
im Beispiel führt zu einem abhängigen Namen, da seine Argumente begin[0]
und begin[1]
vom Vorlagenparameter T
der umgebenden algorithm()
-Funktionsvorlage abhängen. Die Suche nach zweiphasigen Namen für solche abhängigen Namen ist im Standard wie folgt definiert:
14.6.4.2 Kandidatenfunktionen [temp.dep.candidate]
1 Für einen Funktionsaufruf, bei dem der Postfix-Ausdruck ein abhängiger Name ist, Die Kandidatenfunktionen werden mit den üblichen Suchregeln gefunden (3.4.1, 3.4.2) außer dass:
- Für den Teil der Suche, der die Verwendung von unqualifiziertem Namen (3.4.1) verwendet, werden nur Funktionsdeklarationen aus der Templatendefinition verwendet Kontext gefunden werden.
- Für den Teil des Lookup, der assoziierte verwendet Namespaces (3.4.2), nur Funktionsdeklarationen, die entweder in der Template De fi nition Context oder der Template Instanziierungskontext sind gefunden.
Unqualifizierte Suche ist definiert durch
3.4.1 Unqualifizierte Namenssuche [basic.lookup.unqual]
1 In allen Fällen, die in 3.4.1 aufgeführt sind, werden die Bereiche nach a durchsucht Erklärung in der Reihenfolge, die in jeder der jeweiligen Kategorien aufgeführt ist; Name-Suche endet, sobald eine Deklaration gefunden wird für den Namen. Wenn nein Erklärung gefunden, das Programm ist schlecht gebildet.
und argumentabhängige Suche (ADL) als
3.4.2 Argumentabhängige Namenssuche [basic.lookup.argdep]
1 Wenn der postfix-Ausdruck in einem Funktionsaufruf (5.2.2) ist unqualifizierte ID , andere Namespaces werden während des üblichen nicht berücksichtigt unqualifizierte Suche (3.4.1) kann gesucht werden, und in diesen Namespaces, Namespace-Bereich Friend-Funktion oder Funktion Vorlage Deklarationen (11.3) kann nicht anders sichtbar gefunden werden. Diese Änderungen an der Suche hängt von den Typen der Argumente ab (und für die Vorlagenvorlage) Argumente, der Namespace des Template-Arguments).
Das erste Beispiel ruft exp::swap()
auf. Dies ist kein abhängiger Name und erfordert keine zweiphasige Namenssuche. Da der Aufruf zum Auslagern qualifiziert ist, findet eine gewöhnliche Suche statt, die nur die generische swap(T&, T&)
-Funktionsvorlage findet.
Das zweite Beispiel (was @HowardHinnant "die moderne Lösung" nennt) ruft swap()
auf und hat auch eine Überladung swap(A&, A&)
im selben Namespace wie wo class A
lebt (der globale Namespace) in diesem Fall). Da der Aufruf zum Swap unqualifiziert ist, finden gewöhnliches Nachschlagen und ADL zum Zeitpunkt der Definition statt (wobei wiederum nur das generische swap(T&, T&)
gefunden wird), aber ein anderes ADL findet am Instanziierungspunkt statt (dh wo exp::algorithm()
aufgerufen wird main()
) und dies nimmt swap(A&, A&)
auf, was eine bessere Übereinstimmung bei der Überladungsauflösung ist.
So weit, so gut. Nun zur Zugabe: Das dritte Beispiel ruft swap()
auf und hat eine Spezialisierung template<> swap(A&, A&)
in namespace exp
. Die Suche ist die gleiche wie im zweiten Beispiel, aber ADL greift die Template-Spezialisierung nicht auf, da sie nicht in einem zugeordneten Namespace von class A
enthalten ist. Obwohl die Spezialisierung template<> swap(A&, A&)
bei der Überladungsauflösung keine Rolle spielt, wird sie dennoch am Verwendungsort instanziiert.
Schließlich ruft das vierte Beispiel swap()
auf und hat eine Überladung template<class T> swap(A<T>&, A<T>&)
innerhalb namespace exp
für template<class T> class A
lebt im globalen Namespace. Die Suche ist die gleiche wie im dritten Beispiel, und ADL greift die Überladung swap(A<T>&, A<T>&)
nicht erneut auf, da sie nicht in einem zugeordneten Namespace der Klassenvorlage A<T>
enthalten ist. Und in diesem Fall gibt es auch keine Spezialisierung, die am Verwendungsort instanziiert werden muss, so dass hier das generische swap(T&, T&)
aufgerufen wird.
Obwohl Sie keine neuen Überladungen zu namespace std
hinzufügen dürfen und nur explizite Spezialisierungen, würde es aufgrund der verschiedenen Feinheiten der zweiphasigen Namenssuche nicht einmal funktionieren.
Es ist unmöglich, swap
in namespace std
für einen benutzerdefinierten Typ zu überladen. Einleitung eine Überlastung (im Gegensatz zu einer Spezialisierung) in namespace std
ist undefiniertes Verhalten (illegal unter dem Standard, keine Diagnose erforderlich).
Es ist unmöglich, eine Funktion im Allgemeinen für eine Klasse template
zu spezialisieren (im Gegensatz zu einer template
-Klasse Instanz - dh std::vector<int>
ist eine Instanz, während std::vector<T>
ist die gesamte Klasse template
). Was als Spezialisierung erscheint, ist eigentlich eine Überlastung. Also gilt der erste Absatz.
Die beste Methode zum Implementieren von benutzerdefinierten swap
ist das Einführen einer swap
Funktion oder Überladung in denselben Namespace, in dem Ihre template
oder class
lebt.
Wenn swap
dann im richtigen Kontext ( using std::swap; swap(a,b);
) aufgerufen wird, wie es in std
library aufgerufen wird, wird ADL aktiviert, und Ihre Überladung wird gefunden.
Die andere Option ist eine vollständige Spezialisierung von swap
in std
für Ihren speziellen Typ. Dies ist unmöglich (oder unpraktisch) für template
-Klassen, da Sie sich für jede Instanz Ihrer vorhandenen Klasse template
spezialisieren müssen. Für andere Klassen ist es fragil, da Spezialisierung nur für diesen bestimmten Typ gilt: Unterklassen müssen ebenfalls in std
angepasst werden.
Im Allgemeinen ist die Spezialisierung von Funktionen äußerst fragil, und Sie sollten besser Außerkraftsetzungen einführen. Da du keine Überschreibungen in std
einführen kannst, ist der einzige Ort, an dem sie zuverlässig gefunden werden, dein eigenes namespace
. Solche Überschreibungen in Ihrem eigenen Namespace werden gegenüber Overrides in std
ebenfalls bevorzugt.
Es gibt zwei Möglichkeiten, ein swap
in Ihren Namespace zu injizieren. Beide arbeiten für diesen Zweck:
Das erste ist es, es direkt in namespace
zu injizieren, das zweite ist, es als inline friend
einzuschließen. Die Inline friend
für "externe Operatoren" ist ein allgemeines Muster, das im Grunde bedeutet, dass Sie swap
nur über ADL finden können, aber in diesem speziellen Kontext nicht viel hinzufügen.
Tags und Links c++ c++11 templates swap argument-dependent-lookup