Überladen von std :: swap ()

115

std::swap()wird von vielen Standardcontainern (wie std::listund std::vector) beim Sortieren und sogar beim Zuweisen verwendet.

Die Standardimplementierung von swap()ist jedoch sehr verallgemeinert und für benutzerdefinierte Typen eher ineffizient.

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

Adam
quelle
Verwandte Informationen: en.cppreference.com/w/cpp/concept/Swappable
Pharap
Die Swappable- Seite wurde auf en.cppreference.com/w/cpp/named_req/Swappable
Andrew Keeton

Antworten:

135

Der richtige Weg, um Swap zu überladen, besteht darin, ihn in denselben Namespace wie den Swap zu schreiben, damit er über die argumentabhängige Suche (ADL) gefunden werden kann . 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);
        // ...
    }
};
Dave Abrahams
quelle
11
In C ++ 2003 ist es bestenfalls unterbestimmt. Die meisten Implementierungen verwenden ADL, um Swap zu finden, aber nein, es ist nicht vorgeschrieben, sodass Sie sich nicht darauf verlassen können. Sie können std :: swap auf einen bestimmten Betontyp spezialisieren, wie vom OP gezeigt. Erwarten Sie nur nicht, dass diese Spezialisierung verwendet wird, z. B. für abgeleitete Klassen dieses Typs.
Dave Abrahams
15
Es würde mich überraschen, dass Implementierungen ADL immer noch nicht verwenden, um den richtigen Swap zu finden. Dies ist ein altes Thema im Ausschuss. Wenn Ihre Implementierung ADL nicht zum Auffinden von Swap verwendet, reichen Sie einen Fehlerbericht ein.
Howard Hinnant
3
@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. keine / keine Mitgliedsfunktionen haben, std :: sort et. al. eine freie Funktion verwenden müssen; Sie legen das Protokoll fest. Zweitens weiß ich nicht, warum Sie zwei Implementierungen ablehnen, aber die meisten Klassen sind dazu verdammt, ineffizient sortiert zu werden, wenn Sie einen Nicht-Mitglieder-Swap nicht akzeptieren können. Überladungsregeln stellen sicher, dass, wenn beide Deklarationen angezeigt werden, die spezifischere (diese) ausgewählt wird, wenn der Swap ohne Qualifikation aufgerufen wird.
Dave Abrahams
5
@ Mozza314: Es kommt darauf an. A std::sort, das ADL zum Austauschen von Elementen verwendet, ist nicht konform mit C ++ 03, aber konform mit C ++ 11. Warum -1 eine Antwort, die auf der Tatsache basiert, dass Clients möglicherweise nicht-idiomatischen Code verwenden?
JoeG
4
@curiousguy: Wenn das Lesen des Standards nur eine einfache Sache des Lesens des Standards wäre, hätten Sie Recht :-). Leider ist die Absicht der Autoren wichtig. Wenn also die ursprüngliche Absicht war, dass ADL verwendet werden könnte oder sollte, ist es nicht spezifiziert. Wenn nicht, dann ist es nur eine alte Änderung für C ++ 0x, weshalb ich "bestenfalls" unterbestimmt geschrieben habe.
Dave Abrahams
70

Achtung Mozza314

Hier ist eine Simulation der Auswirkungen eines generischen std::algorithmAufrufs std::swap, bei der der Benutzer seinen Swap im Namespace std bereitstellt. Da dies ein Experiment ist, verwendet diese Simulation namespace expanstelle von 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);
}

Für mich druckt dies aus:

generic exp::swap

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

Wenn Ihr Compiler konform ist (mit C ++ 98/03/11), gibt er dieselbe Ausgabe aus, die ich zeige. Und in diesem Fall passiert genau das, was Sie befürchten. Und das Einfügen Ihres swapNamespace std( exp) hat dies nicht verhindert.

Dave und ich sind beide Komiteemitglieder und arbeiten seit einem Jahrzehnt in diesem Bereich des Standards (und sind uns nicht immer einig). Dieses Problem ist jedoch schon lange gelöst, und wir sind uns beide einig, wie es gelöst wurde. Ignorieren Sie Daves Expertenmeinung / Antwort in diesem Bereich auf eigene Gefahr.

Dieses Problem trat nach der Veröffentlichung von C ++ 98 auf. Ab ungefähr 2001 begannen Dave und ich, diesen Bereich zu bearbeiten . 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

Es wurde festgestellt, dass:

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

}

funktioniert! Warum also nicht das benutzen?

Stellen Sie sich den Fall vor, dass es sich bei Ihrer AKlasse um eine Klassenvorlage handelt:

// 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 funktioniert es nicht mehr. :-(

Sie können also den swapNamespace std eingeben und es funktionieren lassen. Aber Sie müssen sich daran zu erinnern setzen swapin A‚s - Namensraum für den Fall , wenn Sie eine Vorlage haben: A<T>. Und da beiden Fälle werden funktionieren , wenn Sie setzen swapin A‚s - Namensraum ist es einfach leichter zu merken (und zu lehren andere) zu tun es einfach , dass ein Weg.

Howard Hinnant
quelle
4
Vielen Dank für die ausführliche Antwort. Ich bin eindeutig weniger darüber informiert und habe mich tatsächlich gefragt, wie Überlastung und Spezialisierung zu unterschiedlichem Verhalten führen können. Ich schlage jedoch keine Überlastung vor, sondern eine Spezialisierung. Wenn ich template <>Ihr erstes Beispiel eingebe, erhalte ich eine Ausgabe exp::swap(A, A)von gcc. Warum also nicht lieber spezialisieren?
Voltrevo
1
Beeindruckend! Das ist wirklich aufschlussreich. Du hast mich definitiv überzeugt. Ich denke, ich werde Ihren Vorschlag leicht ändern und die In-Class-Syntax von Dave Abrahams verwenden (hey, ich kann dies auch für den Operator << verwenden! :-)), es sei denn, Sie haben einen Grund, dies ebenfalls zu vermeiden (außer beim Kompilieren) separat). Denken Sie vor diesem Hintergrund auch, dass dies using std::swapeine Ausnahme von der Regel "Anweisungen niemals in Header-Dateien einfügen" darstellt? Warum nicht using std::swaphineinstecken <algorithm>? Ich nehme an, es könnte eine winzige Minderheit des Codes von Menschen brechen. Vielleicht die Unterstützung ablehnen und sie irgendwann einsetzen?
Voltrevo
3
Die Syntax von Freunden in der Klasse sollte in Ordnung sein. Ich würde versuchen, using std::swapden Funktionsumfang in Ihren Headern zu beschränken. Ja, swapist fast ein Schlüsselwort. Aber nein, es ist kein Schlüsselwort. Exportieren Sie es also am besten nicht in alle Namespaces, bis Sie es wirklich müssen. swapist sehr ähnlich operator==. Der größte Unterschied besteht darin, dass niemand daran denkt, operator==mit qualifizierter Namespace-Syntax aufzurufen (das wäre einfach zu hässlich).
Howard Hinnant
15
@NielKirk: Was Sie als Komplikation sehen, sind einfach zu viele falsche Antworten. Die richtige Antwort von Dave Abrahams ist nicht kompliziert: "Der richtige Weg, den Swap zu überladen, besteht darin, ihn im gleichen Namespace wie den Swap zu schreiben, damit er über die argumentabhängige Suche (ADL) gefunden werden kann."
Howard Hinnant
2
@codeshot: Entschuldigung. Herb versucht seit 1998, diese Botschaft zu vermitteln: gotw.ca/publications/mill02.htm In diesem Artikel erwähnt er Swap nicht. Dies ist jedoch nur eine weitere Anwendung des Herb-Schnittstellenprinzips.
Howard Hinnant
53

Es ist Ihnen (nach dem C ++ - Standard) nicht gestattet, std :: swap zu überladen. Sie dürfen jedoch speziell dem std-Namespace Vorlagenspezialisierungen für Ihre eigenen Typen hinzufügen. Z.B

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

Dann wählen die Verwendungen in den Standardcontainern (und anderswo) Ihre Spezialisierung anstelle der allgemeinen.

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

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

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

Dies funktioniert für Basisklassen. Wenn Sie jedoch versuchen, zwei abgeleitete Objekte auszutauschen, wird die generische Version von std verwendet, da der Vorlagenaustausch genau übereinstimmt (und das Problem vermieden wird, nur die Basisteile Ihrer abgeleiteten Objekte auszutauschen ).

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

Wilka
quelle
1
Meistens ein guter Rat, aber ich muss -1 wegen der subtilen Unterscheidung, die Puetzk zwischen der Spezialisierung einer Vorlage auf den Standard-Namespace (was nach dem C ++ - Standard zulässig ist) und der Überladung (was nicht der Fall ist) feststellt.
j_random_hacker
11
Abgestimmt, weil der richtige Weg zum Anpassen des Swaps darin besteht, dies in Ihrem eigenen Namespace zu tun (wie Dave Abrahams in einer anderen Antwort hervorhebt).
Howard Hinnant
2
Meine Gründe für das Downvoting sind die gleichen wie bei Howard
Dave Abrahams am
13
@ HowardHinnant, Dave Abrahams: Ich bin anderer Meinung. Auf welcher Grundlage behaupten Sie, Ihre Alternative sei der "richtige" Weg? Wie Puetzk aus dem Standard zitiert, ist dies ausdrücklich erlaubt. Obwohl ich neu in diesem Thema bin, mag ich die von Ihnen empfohlene Methode wirklich nicht, denn wenn ich Foo definiere und auf diese Weise austausche, 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
5
@ Mozza314: Aufgrund der Platz- und Formatierungsbeschränkungen im Kommentarbereich konnte ich Ihnen nicht vollständig antworten. Bitte lesen Sie die Antwort mit dem Titel "Achtung Mozza314".
Howard Hinnant
29

Während es richtig ist, dass man dem std :: -Namensraum im Allgemeinen nichts hinzufügen sollte, ist das Hinzufügen von Vorlagenspezialisierungen für benutzerdefinierte Typen ausdrücklich zulässig. Überladen der Funktionen ist nicht. Das ist ein subtiler Unterschied :-)

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

Eine Spezialisierung von std :: swap würde folgendermaßen aussehen:

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

Ohne das Template <> -Bit wäre es eher eine undefinierte Überladung als eine zulässige Spezialisierung. @ Wilkas Vorschlag, den Standard-Namespace zu ändern, funktioniert möglicherweise mit Benutzercode (da Koenig Lookup die Version ohne Namespace bevorzugt), dies ist jedoch nicht garantiert und sollte es auch nicht sein (die STL-Implementierung sollte den vollständigen verwenden) -qualifizierter std :: swap).

Es gibt einen Thread zu comp.lang.c ++. Moderiert mit einer langen Diskussion des Themas. Das meiste davon handelt jedoch von einer teilweisen Spezialisierung (was derzeit nicht gut möglich ist).

Puetzk
quelle
7
Ein Grund, warum es falsch ist, dafür eine Funktionsvorlagenspezialisierung zu verwenden (oder etwas anderes): Es interagiert auf schlechte Weise mit Überladungen, von denen es viele für den Austausch gibt. Wenn Sie beispielsweise den regulären std :: swap auf std :: vector <mytype> & spezialisieren, wird Ihre Spezialisierung nicht gegenüber dem vektorspezifischen Swap des Standards ausgewählt, da Spezialisierungen bei der Überlastungsauflösung nicht berücksichtigt werden.
Dave Abrahams
4
Dies empfiehlt Meyers auch in Effective C ++ 3ed (Punkt 25, S. 106-112).
JWW
2
@DaveAbrahams: Wenn Sie (ohne explizite Template - Argumente) spezialisiert ist , teilweise Ordnung wird es dazu führen , eine Spezialisierung zu sein von der vectorVersion und es wird verwendet werden .
Davis Herring
1
@DavisHerring eigentlich, nein, wenn Sie das tun, spielt die Teilbestellung keine Rolle. Das Problem ist nicht, dass man es überhaupt nicht nennen kann; es ist das, was bei scheinbar weniger spezifischen Überladungen von Swap passiert
Dave Abrahams
2
@ DaveAbrahams: Die Teilreihenfolge besteht darin , die zu spezialisierende Funktionsvorlage auszuwählen, wenn die explizite Spezialisierung mit mehr als einer übereinstimmt. Die von ::swapIhnen hinzugefügte Überlastung ist spezialisierter als die std::swapÜberlastung vector, sodass der Anruf erfasst wird und keine Spezialisierung der letzteren relevant ist. Ich bin mir nicht sicher, wie das ein praktisches Problem ist (aber ich behaupte auch nicht, dass dies eine gute Idee ist!).
Davis Herring