Public Friend Swap Member Funktion

169

In der schönen Antwort auf die Copy-and-Swap-Sprache gibt es einen Code, für den ich ein bisschen Hilfe brauche:

class dumb_array
{
public:
    // ...
    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        using std::swap; 
        swap(first.mSize, second.mSize); 
        swap(first.mArray, second.mArray);
    }
    // ...
};

und er fügt eine Notiz hinzu

Es gibt andere Behauptungen, dass wir std :: swap für unseren Typ spezialisieren, einen Swap in der Klasse neben einem Swap mit freien Funktionen bereitstellen sollten usw. Dies ist jedoch alles unnötig: Jede ordnungsgemäße Verwendung des Swaps erfolgt durch einen nicht qualifizierten Aufruf und unsere Funktion wird über ADL gefunden. Eine Funktion reicht aus.

Da friendich ein bisschen "unfreundlich" bin, muss ich zugeben. Meine Hauptfragen sind also:

  • sieht aus wie eine freie Funktion , aber es ist innerhalb des Klassenkörpers?
  • Warum ist das nicht swapstatisch ? Es werden offensichtlich keine Mitgliedsvariablen verwendet.
  • "Jede ordnungsgemäße Verwendung von Swap wird Swap über ADL herausfinden" ? ADL durchsucht die Namespaces, oder? Aber schaut es auch in Klassen? Oder kommt hier was friendrein?

Nebenfragen:

  • Soll ich mit C ++ 11 mein swaps mit markieren noexcept?
  • Mit C ++ 11 und dessen Bereichs für , soll ich setzen friend iter begin()und auf friend iter end()die gleiche Weise in der Klasse? Ich denke das friendwird hier nicht gebraucht, oder?
Towi
quelle
In Anbetracht der Nebenfrage zu bereichsbasiert für: Es ist besser, Elementfunktionen zu schreiben und den Bereichszugriff auf begin () und end () im Standard-Namespace (§24.6.5) zu belassen, bereichsbasiert für interne Verwendungen dieser von global oder Standard-Namespace (siehe §6.5.4). Es hat jedoch den Nachteil, dass diese Funktionen Teil des <iterator> -Headers sind. Wenn Sie sie nicht einschließen, möchten Sie sie möglicherweise selbst schreiben.
Vitus
2
Warum ist es nicht statisch - weil eine friendFunktion überhaupt keine Mitgliedsfunktion ist?
Aschepler

Antworten:

175

Es gibt verschiedene Möglichkeiten zu schreiben swap, einige besser als andere. Im Laufe der Zeit wurde jedoch festgestellt, dass eine einzige Definition am besten funktioniert. Lassen Sie uns überlegen, wie wir über das Schreiben einer swapFunktion nachdenken könnten .


Wir sehen zuerst, dass Container wie std::vector<>eine Elementfunktion mit einem Argument haben swap, wie zum Beispiel:

struct vector
{
    void swap(vector&) { /* swap members */ }
};

Natürlich sollte unsere Klasse das auch, oder? Nicht wirklich. Die Standardbibliothek enthält alle möglichen unnötigen Dinge , und ein Mitglied swapist eines davon. Warum? Lass uns weiter gehen.


Was wir tun sollten, ist herauszufinden, was kanonisch ist und was unsere Klasse tun muss , um damit zu arbeiten. Und die kanonische Methode des Austauschs ist mit std::swap. Aus diesem Grund sind Mitgliedsfunktionen nicht nützlich: Sie sind nicht die Art und Weise, wie wir Dinge im Allgemeinen austauschen sollten, und haben keinen Einfluss auf das Verhalten von std::swap.

Nun, um std::swapArbeit zu machen , sollten wir std::vector<>eine Spezialisierung von bereitstellen (und hätten bereitstellen sollen) std::swap, richtig?

namespace std
{
    template <> // important! specialization in std is OK, overloading is UB
    void swap(myclass&, myclass&)
    {
        // swap
    }
}

Nun, das würde in diesem Fall sicherlich funktionieren, aber es hat ein eklatantes Problem: Funktionsspezialisierungen können nicht partiell sein. Das heißt, wir können keine Vorlagenklassen damit spezialisieren, sondern nur bestimmte Instanziierungen:

namespace std
{
    template <typename T>
    void swap<T>(myclass<T>&, myclass<T>&) // error! no partial specialization
    {
        // swap
    }
}

Diese Methode funktioniert manchmal, aber nicht immer. Es muss einen besseren Weg geben.


Es gibt! Wir können eine friendFunktion verwenden und sie über ADL finden :

namespace xyz
{
    struct myclass
    {
        friend void swap(myclass&, myclass&);
    };
}

Wenn wir etwas tauschen möchten, assoziieren wir std::swap und tätigen dann einen unqualifizierten Anruf:

using std::swap; // allow use of std::swap...
swap(x, y); // ...but select overloads, first

// that is, if swap(x, y) finds a better match, via ADL, it
// will use that instead; otherwise it falls back to std::swap

Was ist eine friendFunktion? In diesem Bereich herrscht Verwirrung.

Bevor C ++ standardisiert wurde, haben friendFunktionen eine sogenannte "Friend Name Injection" ausgeführt, bei der sich der Code so verhielt, als ob die Funktion in den umgebenden Namespace geschrieben worden wäre. Zum Beispiel waren dies äquivalente Vorstandards:

struct foo
{
    friend void bar()
    {
        // baz
    }
};

// turned into, pre-standard:    

struct foo
{
    friend void bar();
};

void bar()
{
    // baz
}

Als ADL erfunden wurde, wurde dies jedoch entfernt. Die friendFunktion konnte dann nur über ADL gefunden werden; Wenn Sie es als freie Funktion haben wollten, musste es als solches deklariert werden ( siehe dies zum Beispiel). Aber siehe da! Es gab ein Problem.

Wenn Sie nur verwenden std::swap(x, y), wird Ihre Überlastung nie gefunden, weil Sie ausdrücklich gesagt haben "schauen Sie rein stdund nirgendwo anders"! Aus diesem Grund schlugen einige Leute vor, zwei Funktionen zu schreiben: eine als Funktion, die über ADL gefunden werden kann , und die andere, um explizite std::Qualifikationen zu handhaben .

Aber wie wir gesehen haben, kann dies nicht in allen Fällen funktionieren und wir haben ein hässliches Durcheinander. Stattdessen ging das idiomatische Tauschen den anderen Weg: Anstatt es zum Job der Klassen std::swapzu machen, ist es die Aufgabe der Swapper, sicherzustellen, dass sie keine qualifizierten Mitarbeiter swapwie oben verwenden. Und das funktioniert normalerweise ziemlich gut, solange die Leute davon wissen. Aber darin liegt das Problem: Es ist nicht intuitiv, einen unqualifizierten Anruf verwenden zu müssen!

Um dies zu vereinfachen, haben einige Bibliotheken wie Boost die Funktion boost::swap, die nur einen unqualifizierten Aufruf ausführt swap, std::swapals zugehörigen Namespace bereitgestellt . Dies hilft, die Dinge wieder kurz zu machen, aber es ist immer noch ein Mist.

Beachten Sie, dass sich in C ++ 11 das Verhalten von nicht ändert std::swap, was ich und andere fälschlicherweise für möglich gehalten haben. Wenn Sie davon gebissen wurden, lesen Sie hier .


Kurz gesagt: Die Elementfunktion ist nur Rauschen, die Spezialisierung ist hässlich und unvollständig, aber die friendFunktion ist vollständig und funktioniert. Und wenn Sie tauschen, verwenden Sie entweder boost::swapoder nicht qualifiziert swapmit std::swapverbunden.


† Informell ist ein Name zugeordnet, wenn er bei einem Funktionsaufruf berücksichtigt wird. Einzelheiten finden Sie in §3.4.2. In diesem Fall wird std::swapnormalerweise nicht berücksichtigt; aber wir können assoziieren es (fügen Sie den Satz von Überlastungen durch unqualifizierte betrachtet swap), so dass es gefunden werden.

GManNickG
quelle
10
Ich bin nicht der Meinung, dass die Mitgliedsfunktion nur Rauschen ist. Eine Mitgliedsfunktion erlaubt zB std::vector<std::string>().swap(someVecWithData);, was mit einer swapfreien Funktion nicht möglich ist, da beide Argumente als nicht konstante Referenz übergeben werden.
ildjarn
3
@ildjarn: Du kannst es in zwei Zeilen machen. Die Member-Funktion verstößt gegen das DRY-Prinzip.
GManNickG
4
@GMan: Das DRY-Prinzip gilt nicht, wenn eines in Bezug auf das andere implementiert ist. Andernfalls würde niemand eine Klasse mit Implementierungen von operator=, operator+und befürworten operator+=, aber es ist klar, dass diese Operatoren für relevante Klassen für Symmetrie akzeptiert / erwartet werden. Gleiches gilt meiner Meinung nach für Mitglieder swap+ Namespace-Bereiche swap.
ildjarn
3
@GMan Ich denke, es werden zu viele Funktionen in Betracht gezogen. Wenig bekannt, aber selbst ein function<void(A*)> f; if(!f) { }kann scheitern, nur weil es ein Adeklariert operator!, das fgenauso gut akzeptiert wie fdas eigene operator!(unwahrscheinlich, kann aber passieren). Wenn function<>der Autor dachte "Oh, ich habe einen 'Operator Bool', warum sollte ich 'Operator!' Implementieren? Das würde gegen DRY verstoßen!", Wäre das fatal. Sie müssen nur ein operator!für Aund Aeinen Konstruktor für ein implementieren function<...>, und die Dinge werden kaputt gehen, da beide Kandidaten benutzerdefinierte Konvertierungen erfordern.
Johannes Schaub - litb
1
Lassen Sie uns überlegen, wie wir über das Schreiben einer [Mitglied] -Tauschfunktion nachdenken könnten. Natürlich sollte unsere Klasse das auch, oder? Nicht wirklich. Die Standardbibliothek enthält alle möglichen unnötigen Dinge , und ein Mitgliedertausch ist eine davon. Die verknüpfte GotW befürwortet die Mitgliedertauschfunktion.
Xeverous
7

Dieser Code entspricht (in fast jeder Hinsicht):

class dumb_array
{
public:
    // ...
    friend void swap(dumb_array& first, dumb_array& second);
    // ...
};

inline void swap(dumb_array& first, dumb_array& second) // nothrow
{
    using std::swap; 
    swap(first.mSize, second.mSize); 
    swap(first.mArray, second.mArray);
}

Eine innerhalb einer Klasse definierte Freundfunktion ist:

  • im umschließenden Namespace platziert
  • automatisch inline
  • kann ohne weitere Qualifikation auf statische Mitglieder der Klasse verweisen

Die genauen Regeln finden Sie im Abschnitt [class.friend](ich zitiere die Absätze 6 und 7 des C ++ 0x-Entwurfs):

Eine Funktion kann in einer Friend-Deklaration einer Klasse genau dann definiert werden, wenn die Klasse eine nicht lokale Klasse ist (9.8), der Funktionsname nicht qualifiziert ist und die Funktion einen Namespace-Bereich hat.

Eine solche Funktion ist implizit inline. Eine in einer Klasse definierte Friend-Funktion befindet sich im (lexikalischen) Bereich der Klasse, in der sie definiert ist. Eine außerhalb der Klasse definierte Friend-Funktion ist dies nicht.

Ben Voigt
quelle
2
Tatsächlich werden Friend-Funktionen in Standard-C ++ nicht im umschließenden Namespace platziert. Das alte Verhalten wurde "Injektion von Freundesnamen" genannt, wurde jedoch durch ADL ersetzt, das im ersten Standard ersetzt wurde. Siehe oben auf diesem . (Das Verhalten ist jedoch ziemlich ähnlich.)
GManNickG
1
Nicht wirklich gleichwertig. Der Code in der Frage macht es so, dass swapnur für ADL sichtbar ist. Es ist ein Mitglied des umschließenden Namespace, aber sein Name ist für die anderen Namenssuchformulare nicht sichtbar. EDIT: Ich sehe, dass @GMan wieder schneller war :) @Ben es war immer so in der ISO C ++ :)
Johannes Schaub - litb
2
@Ben: Nein, Friend Injection gab es in einem Standard nie, aber es war weit verbreitet, weshalb die Idee (und die Compiler-Unterstützung) tendenziell weiterging, aber technisch gesehen nicht vorhanden ist. friendFunktionen werden nur von ADL gefunden, und wenn es sich nur um freie Funktionen mit friendZugriff handeln muss, müssen sie sowohl als friendinnerhalb der Klasse als auch als normale Deklaration für freie Funktionen außerhalb der Klasse deklariert werden . Sie können diese Notwendigkeit zum Beispiel in dieser Antwort sehen .
GManNickG
2
@towi: Da sich die Friend-Funktion im Namespace-Bereich befindet, sollten die Antworten auf alle drei Ihrer Fragen klar sein: (1) Es handelt sich um eine kostenlose Funktion und hat Freund-Zugriff auf private und geschützte Mitglieder der Klasse. (2) Es ist überhaupt kein Mitglied, weder eine Instanz noch eine statische. (3) ADL sucht nicht innerhalb von Klassen, aber dies ist in Ordnung, da die Friend-Funktion einen Namespace-Bereich hat.
Ben Voigt
1
@ Ben. In der Spezifikation ist die Funktion ein Namespace-Mitglied, und der Ausdruck "Die Funktion hat einen Namespace-Bereich" kann so interpretiert werden, dass die Funktion ein Namespace-Mitglied ist (dies hängt ziemlich stark vom Kontext einer solchen Anweisung ab). Außerdem wird diesem Namespace ein Name hinzugefügt, der nur für ADL sichtbar ist (tatsächlich widerspricht IIRC teilweise anderen Teilen in der Spezifikation, ob ein Name hinzugefügt wird oder nicht. Das Hinzufügen eines Namens ist jedoch erforderlich, um inkompatible Deklarationen zu erkennen, die dazu hinzugefügt wurden Namensraum, so in der Tat, ein unsichtbarer Name wird hinzugefügt. Beachten Sie den Hinweis auf 3.3.1p4).
Johannes Schaub - litb