Wie man std::swap() überlädt

Lesezeit: 10 Minuten

Wie man stdswap uberladt
Adam

std::swap() wird von vielen std-Containern verwendet (wie z std::list und std::vector) beim Sortieren und sogar Zuordnen.

Aber die std-Implementierung von swap() ist sehr verallgemeinert und für benutzerdefinierte Typen eher ineffizient.

Somit kann Effizienz durch Überladen gewonnen werden std::swap() mit einer benutzerdefinierten typspezifischen Implementierung. Aber wie können Sie es implementieren, damit es von den std-Containern verwendet wird?

Wie man stdswap uberladt
David Abrahams

Der richtige Weg zur Überlastung std::swapDie Implementierung von (auch bekannt als Spezialisierung) besteht darin, es in denselben Namespace zu schreiben wie das, was Sie austauschen, damit es über gefunden werden kann argumentabhängige Suche (ADL). Eine besonders einfache Sache ist:

class X
{
    // ...
    friend void swap(X& a, X& b)
    {
        using std::swap; // bring in swap for built-in types

        swap(a.base1, b.base1);
        swap(a.base2, b.base2);
        // ...
        swap(a.member1, b.member1);
        swap(a.member2, b.member2);
        // ...
    }
};

  • In C++2003 ist es bestenfalls unterspezifiziert. Die meisten Implementierungen verwenden ADL, um Swap zu finden, aber nein, es ist nicht vorgeschrieben, also können Sie sich nicht darauf verlassen. Sie kann spezialisieren Sie std::swap für einen bestimmten konkreten Typ, wie vom OP gezeigt; Erwarten Sie nur nicht, dass diese Spezialisierung verwendet wird, zB für abgeleitete Klassen dieses Typs.

    – Dave Abrahams

    1. Juni 2010 um 15:52 Uhr

  • Ich wäre überrascht, diese Implementierungen zu finden still Verwenden Sie nicht ADL, um den richtigen Swap zu finden. Das ist ein alt Thema im Ausschuss. Wenn Ihre Implementierung ADL nicht verwendet, um Auslagerungen zu finden, reichen Sie einen Fehlerbericht ein.

    – Howard Hinnant

    23. Februar 2011 um 1:32 Uhr

  • @Sascha: Zuerst definiere ich die Funktion im Namespace-Bereich, da dies die einzige Art von Definition ist, die für generischen Code von Bedeutung ist. Weil int et. Al. darf/kann keine Mitgliedsfunktionen haben, std::sort et. Al. müssen eine freie Funktion verwenden; Sie erstellen das Protokoll. Zweitens weiß ich nicht, warum Sie dagegen sind, zwei Implementierungen zu haben, aber die meisten Klassen sind dazu verdammt, ineffizient sortiert zu werden, wenn Sie einen Nicht-Member-Swap nicht akzeptieren können. Überladungsregeln stellen sicher, dass, wenn beide Deklarationen angezeigt werden, die spezifischere (diese) ausgewählt wird, wenn swap ohne Qualifikation aufgerufen wird.

    – Dave Abrahams

    17. April 2011 um 14:24 Uhr

  • @ Mozza314: Es kommt darauf an. EIN std::sort das ADL zum Austauschen von Elementen verwendet, ist nicht konform zu C++03, aber konform zu C++11. Warum -1 eine Antwort, die auf der Tatsache basiert, dass Clients möglicherweise nicht idiomatischen Code verwenden?

    – JoG

    8. Dezember 2011 um 13:10 Uhr


  • @curiousguy: Wenn das Lesen des Standards nur eine Frage des Lesens des Standards wäre, hätten Sie Recht :-). Leider spielt die Absicht der Autoren eine Rolle. Wenn also die ursprüngliche Absicht war, dass ADL verwendet werden könnte oder sollte, ist es unterspezifiziert. Wenn nicht, dann ist es nur eine einfache alte Breaking Change für C++0x, weshalb ich „bestenfalls“ unterspezifiziert geschrieben habe.

    – Dave Abrahams

    24. Juli 2013 um 23:09 Uhr

1646972411 461 Wie man stdswap uberladt
Howard Hinnant

Achtung Mozza314

Hier ist eine Simulation der Auswirkungen eines Generikums std::algorithm Berufung std::swap, und der Benutzer muss seinen Swap im Namespace std bereitstellen. Da es sich um ein Experiment handelt, verwendet diese Simulation namespace exp anstatt namespace std.

// simulate <algorithm>

#include <cstdio>

namespace exp
{

    template <class T>
    void
    swap(T& x, T& y)
    {
        printf("generic exp::swap\n");
        T tmp = x;
        x = y;
        y = tmp;
    }

    template <class T>
    void algorithm(T* begin, T* end)
    {
        if (end-begin >= 2)
            exp::swap(begin[0], begin[1]);
    }

}

// simulate user code which includes <algorithm>

struct A
{
};

namespace exp
{
    void swap(A&, A&)
    {
        printf("exp::swap(A, A)\n");
    }

}

// exercise simulation

int main()
{
    A a[2];
    exp::algorithm(a, a+2);
}

Bei mir druckt das aus:

generic exp::swap

Wenn Ihr Compiler etwas anderes ausgibt, implementiert er die “Zwei-Phasen-Suche” für Vorlagen nicht korrekt.

Wenn Ihr Compiler konform ist (zu einem von C++98/03/11), gibt er die gleiche Ausgabe aus, die ich zeige. Und in diesem Fall passiert genau das, was Sie befürchten. Und setzen Sie Ihre swap in den Namensraum std (exp) hat das nicht verhindert.

Dave und ich sind beide Ausschussmitglieder und arbeiten seit einem Jahrzehnt in diesem Bereich des Standards (und sind uns nicht immer einig). Aber diese Frage ist seit langem geregelt, und wir sind uns beide einig, wie sie geregelt wurde. Ignorieren Sie Daves Expertenmeinung/Antwort in diesem Bereich auf eigene Gefahr.

Dieses Problem trat auf, nachdem C++98 veröffentlicht wurde. Etwa ab 2001 begannen Dave und ich damit arbeite diesen Bereich. Und das ist die moderne Lösung:

// simulate <algorithm>

#include <cstdio>

namespace exp
{

    template <class T>
    void
    swap(T& x, T& y)
    {
        printf("generic exp::swap\n");
        T tmp = x;
        x = y;
        y = tmp;
    }

    template <class T>
    void algorithm(T* begin, T* end)
    {
        if (end-begin >= 2)
            swap(begin[0], begin[1]);
    }

}

// simulate user code which includes <algorithm>

struct A
{
};

void swap(A&, A&)
{
    printf("swap(A, A)\n");
}

// exercise simulation

int main()
{
    A a[2];
    exp::algorithm(a, a+2);
}

Ausgabe ist:

swap(A, A)

Aktualisieren

Eine Beobachtung wurde gemacht:

namespace exp
{    
    template <>
    void swap(A&, A&)
    {
        printf("exp::swap(A, A)\n");
    }

}

funktioniert! Warum also nicht das nutzen?

Betrachten Sie den Fall, dass Ihr A ist eine Klassenvorlage:

// simulate user code which includes <algorithm>

template <class T>
struct A
{
};

namespace exp
{

    template <class T>
    void swap(A<T>&, A<T>&)
    {
        printf("exp::swap(A, A)\n");
    }

}

// exercise simulation

int main()
{
    A<int> a[2];
    exp::algorithm(a, a+2);
}

Jetzt geht es wieder nicht. 🙁

Also könnte man setzen swap in namespace std und es funktioniert. Aber Sie müssen daran denken, zu setzen swap in A‘s Namespace für den Fall, wenn Sie eine Vorlage haben: A<T>. Und da beide Fälle funktionieren, wenn Sie setzen swap in A‘s Namensraum, ist es einfach einfacher, sich daran zu erinnern (und anderen beizubringen), es einfach so zu machen.

  • Vielen Dank für die ausführliche Antwort. Ich kenne mich damit eindeutig weniger aus und habe mich eigentlich gefragt, wie Überladung und Spezialisierung zu unterschiedlichem Verhalten führen können. Ich schlage jedoch keine Überladung vor, sondern Spezialisierung. Wenn ich lege template <> In Ihrem ersten Beispiel bekomme ich eine Ausgabe exp::swap(A, A) von gcc. Warum also nicht eine Spezialisierung bevorzugen?

    – Voltrevo

    9. Dezember 2011 um 0:26 Uhr

  • Die Syntax für Freunde in der Klasse sollte in Ordnung sein. Ich würde versuchen einzuschränken using std::swap zum Funktionsumfang innerhalb Ihrer Header. Jawohl, swap ist fast ein Stichwort. Aber nein, es ist nicht ganz ein Schlüsselwort. Exportieren Sie es also am besten nicht in alle Namespaces, bis Sie wirklich müssen. swap ist sehr ähnlich operator==. Der größte Unterschied besteht darin, dass niemand jemals an einen Anruf denkt operator== mit qualifizierter Namespace-Syntax (es wäre einfach zu hässlich).

    – Howard Hinnant

    9. Dezember 2011 um 1:17 Uhr

  • @NielKirk: Was Sie als Komplikation sehen, sind einfach zu viele falsche Antworten. Die richtige Antwort von Dave Abrahams ist nicht kompliziert: “Der richtige Weg, Swap zu überladen, besteht darin, es in denselben Namensraum zu schreiben wie das, was Sie tauschen, damit es über argumentabhängige Suche (ADL) gefunden werden kann.”

    – Howard Hinnant

    2. Oktober 2013 um 14:23 Uhr

  • @codeshot: Entschuldigung. Herb versucht seit 1998, diese Botschaft zu vermitteln: gotw.ca/publications/mill02.htm Er erwähnt Swap in diesem Artikel nicht. Aber dies ist nur eine weitere Anwendung des Interface-Prinzips von Herb.

    – Howard Hinnant

    22. August 2015 um 21:20 Uhr

  • Visual Studio implementiert die in C++98 eingeführten zweiphasigen Suchregeln noch nicht korrekt. Das bedeutet, dass VS in diesem Beispiel das Falsche aufruft swap. Dies fügt eine neue Falte hinzu, die ich zuvor nicht berücksichtigt hatte: Im Fall von template<class T> struct Asetzen Sie Ihre swap in den Namensraum std macht Ihren Code nicht portierbar. Probieren Sie Ihr Beispiel auf Wandbox aus, um zu sehen, wie gcc und clang damit umgehen.

    – Howard Hinnant

    5. Mai 2018 um 15:59 Uhr

1646972411 551 Wie man stdswap uberladt
Wilka

Sie dürfen (nach dem C++-Standard) std::swap nicht überladen, aber Sie dürfen ausdrücklich Template-Spezialisierungen für Ihre eigenen Typen zum std-Namespace hinzufügen. Z.B

namespace std
{
    template<>
    void swap(my_type& lhs, my_type& rhs)
    {
       // ... blah
    }
}

dann wählen die Verwendungen in den std-Containern (und überall sonst) Ihre Spezialisierung anstelle der allgemeinen aus.

Beachten Sie auch, dass die Bereitstellung einer Basisklassenimplementierung von swap für Ihre abgeleiteten Typen nicht gut genug ist. ZB wenn Sie haben

class Base
{
    // ... stuff ...
}
class Derived : public Base
{
    // ... stuff ...
}

namespace std
{
    template<>
    void swap(Base& lha, Base& rhs)
    {
       // ...
    }
}

Dies funktioniert für Basisklassen, aber wenn Sie versuchen, zwei abgeleitete Objekte auszutauschen, wird die generische Version von std verwendet, da der Vorlagenaustausch eine genaue Übereinstimmung ist (und das Problem vermeidet, dass nur die „Basis“-Teile Ihrer abgeleiteten Objekte ausgetauscht werden ).

HINWEIS: Ich habe dies aktualisiert, um die falschen Bits aus meiner letzten Antwort zu entfernen. D’oh! (danke pützk und j_random_hacker für den hinweis)

  • Abgewertet, weil der richtige Weg zum Anpassen von Swap darin besteht, dies in Ihrem eigenen Namensraum zu tun (wie Dave Abrahams in einer anderen Antwort betont).

    – Howard Hinnant

    23. Februar 2011 um 1:30 Uhr

  • Ist es verboten zu überladen std::swap (oder irgendetwas anderes), aber außerhalb von std::swap Namensraum?

    – Kos

    28. Juli 2011 um 14:07 Uhr

  • @HowardHinnant, Dave Abrahams: Ich bin anderer Meinung. Auf welcher Grundlage behaupten Sie, dass Ihre Alternative der “richtige” Weg ist? Wie Pützk aus der Norm zitiert, ist dies ausdrücklich erlaubt. Obwohl ich neu in diesem Problem bin, mag ich die Methode, die Sie befürworten, wirklich nicht, denn wenn ich Foo definiere und auf diese Weise tausche, verwendet wahrscheinlich jemand anderes, der meinen Code verwendet, std::swap (a, b) anstelle von swap ( a, b) auf Foo, das stillschweigend die ineffiziente Standardversion verwendet.

    – Voltrevo

    8. Dezember 2011 um 12:39 Uhr

  • @Mozza314: Die Platz- und Formatierungsbeschränkungen des Kommentarbereichs erlaubten mir nicht, Ihnen vollständig zu antworten. Bitte sehen Sie sich die Antwort an, die ich mit dem Titel “Achtung Mozza314” hinzugefügt habe.

    – Howard Hinnant

    8. Dezember 2011 um 23:53 Uhr

  • @HowardHinnant, gehe ich richtig in der Annahme, dass diese Technik auch leicht gegen die One-Definition-Rule verstoßen könnte? Wenn eine Übersetzungseinheit und eine Vorwärtsdeklaration der Klasse Base; Während ein anderer den obigen Header enthält, haben Sie zwei verschiedene Instanzen von std::swap. Ich erinnere mich, dass dies in einem konformen Programm verboten ist, aber die Verwendung dieser Technik bedeutet, dass Sie Benutzer Ihrer Klasse erfolgreich daran hindern müssen, eine Vorwärtsdeklaration zu schreiben – sie müssen irgendwie gezwungen werden, immer Ihren Header einzuschließen, um ihre Ziele zu erreichen. Es erweist sich als unpraktisch, dies im Maßstab zu erreichen.

    – Codeschuss

    6. November 2016 um 15:25 Uhr


1646972412 424 Wie man stdswap uberladt
pützk

Es ist zwar richtig, dass dem std::-Namespace generell nichts hinzugefügt werden sollte, aber das Hinzufügen von Template-Spezialisierungen für benutzerdefinierte Typen ist ausdrücklich erlaubt. Überladen der Funktionen ist nicht. Das ist ein kleiner Unterschied 🙂

17.4.3.1/1 Es ist für ein C++-Programm undefiniert, Deklarationen oder Definitionen zu Namespace std oder Namespaces mit Namespace std hinzuzufügen, sofern nicht anders angegeben. Ein Programm kann Template-Spezialisierungen für jedes Standardbibliotheks-Template zum Namensraum std hinzufügen. Eine solche (vollständige oder teilweise) Spezialisierung einer Standardbibliothek führt zu undefiniertem Verhalten, es sei denn, die Deklaration hängt von einem benutzerdefinierten Namen einer externen Verknüpfung ab und die Vorlagenspezialisierung erfüllt die Anforderungen der Standardbibliothek für die ursprüngliche Vorlage.

Eine Spezialisierung von std::swap würde wie folgt aussehen:

namespace std
{
    template<>
    void swap(myspace::mytype& a, myspace::mytype& b) { ... }
}

Ohne das Bit template<> wäre es eher eine undefinierte Überladung als eine erlaubte Spezialisierung. Der von @Wilka vorgeschlagene Ansatz, den Standard-Namespace zu ändern, funktioniert möglicherweise mit Benutzercode (aufgrund der Koenig-Suche, die die Version ohne Namespace bevorzugt), dies ist jedoch nicht garantiert und soll dies auch nicht wirklich (die STL-Implementierung sollte die verwenden) vollständig -qualifizierter std::swap).

Da ist ein Thread auf comp.lang.c++.moderated mit einer lang Diskussion des Themas. Das meiste davon dreht sich jedoch um eine teilweise Spezialisierung (was derzeit nicht gut möglich ist).

989840cookie-checkWie man std::swap() überlädt

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy