2010-01-09 13 views
6

ich kürzlich gelesen (und leider vergessen, wo), dass der beste Weg Operator zu schreiben = ist wie folgt:expliziter Copykonstruktor oder implizite Parameter von Wert

foo &operator=(foo other) 
{ 
    swap(*this, other); 
    return *this; 
} 

statt dem:

foo &operator=(const foo &other) 
{ 
    foo copy(other); 
    swap(*this, copy); 
    return *this; 
} 

Die Idee ist, dass, wenn Operator = mit einem Rvalue aufgerufen wird, die erste Version die Konstruktion einer Kopie optimieren kann. Wenn also mit einem rvalue aufgerufen wird, ist die erste Version schneller und wenn sie mit einem lvalue aufgerufen wird, sind die beiden gleichwertig.

Ich bin neugierig, was andere Leute darüber denken? Würden die Leute die erste Version wegen mangelnder Deutlichkeit meiden? Habe ich Recht, dass die erste Version besser sein kann und niemals schlechter sein kann?

+0

Was ist 'swap'? Wenn es 'foo temp = x ist; x = y; y = temp; 'Sie haben die unendliche Rekursion der Funktion' operator = 'und' swap'. –

+1

Ich schrieb ein Programm, um die Theorie von @Alexey Malistov zu testen, und er war richtig - ich habe unendliche Rekursion. – nobar

+0

Jede Klasse, die ein Kopier- und Swap-Idiom implementiert, kann nicht auf die Standard-Implementierung 'std :: swap' in ihrem Kopierzuweisungsoperator zurückgreifen. Das ist so ziemlich selbstverständlich. –

Antwort

4

Sie lesen vermutlich aus: http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/

Ich habe nicht viel zu sagen, da ich die Verbindung denke, die Gründe ziemlich gut erklärt. Anekdotisch kann ich bestätigen, dass die erste Form in meinen Builds mit MSVC weniger Kopien ergibt, was sinnvoll ist, da Compiler möglicherweise nicht in der Lage sind, Kopien auf dem zweiten Formular zu machen. Ich stimme zu, dass die erste Form eine strenge Verbesserung ist und niemals schlechter als die zweite ist.

Edit: Die erste Form könnte ein bisschen weniger idiomatisch sein, aber ich denke nicht, dass es viel weniger klar ist. (IMO, es ist nicht mehr überraschend, als die Copy-and-Swap-Implementierung des Zuweisungsoperators zum ersten Mal zu sehen.)

Edit # 2: Ups, ich meinte, Kopier-Elision, nicht RVO zu schreiben.

+1

Ich stimme nicht damit überein, dass das erste idiomatisch ist. Dies ist die weniger verbreitete Version. Ich denke, wenn Sie für eine große Firma arbeiten, die viele Codeüberprüfungen durchführt, werden Sie wieder zurückkommen, um auf die normale Weise zu tun. Normal ist besser für die Wartung, da die Leute nicht doppelt vorgehen müssen, um herauszufinden, was passiert. Ich denke, die erste Version ist nur ein schöner Partytrick, um zu zeigen, wie cool du bist. –

+1

Ich habe nicht ganz gesagt, dass die erste Form idiomatisch war (und ich habe gesagt, dass die zweite Form mehr war). Aber Idiome müssen irgendwo anfangen, und ich weiß, dass die zweite Form mich dazu brachte, das erste Mal, als ich es sah, eine Doppelaufnahme zu machen. – jamesdlin

+1

@Martin: Wie Herb Sutter sagte, wenn die Konvention mit der guten Praxis kollidiert, müssen wir uns fragen, ob wir die Konvention zugunsten guter Praktiken ablehnen oder gute Praktiken zugunsten der Konvention verwerfen sollten. –

-3

denke ich, dass Sie den Unterschied zwischen könnte verwirrend sein:

foo &operator=(const foo &other); und
const foo &operator=(const foo &other);

Die erste Form verwendet werden soll, ermöglichen: (a = b) = c;

+4

Ich glaube nicht, dass er diese verwirrte - seine Frage ergibt durchaus Sinn. –

-2

Diese beiden sind eigentlich die gleichen. Der einzige Unterschied ist, wo Sie in einem Debugger auf "Step In" drücken. Und Sie sollten im Voraus wissen, wo es zu tun ist.

2

Ich bevorzuge in der Regel die zweite aus Lesbarkeit und 'am wenigsten Überraschung' Sicht jedoch anerkenne ich, dass die erste effizienter sein kann, wenn der Parameter ein temporärer ist.

Die erste kann wirklich zu keine Kopien führen, nicht nur die einzelne Kopie und es ist denkbar, dass dies ein echtes Problem in extremen Situationen sein kann.

z. Nimm dieses Testprogramm. gcc -O3 -S (GCC-Version 4.4.2 20091222 (Red Hat 4.4.2-20) (GCC)) generiert einen Aufruf von Bs Kopierkonstruktor, aber keine Aufrufe von A's Kopierkonstruktor für die Funktion f (der Zuweisungsoperator ist sowohl für A als auch für B inline)). A und B können beide als sehr einfache String-Klassen angesehen werden. Die Zuweisung und das Kopieren für data würde in den Konstruktoren und der Zuordnung in dem Destruktor auftreten.

#include <algorithm> 

class A 
{ 
public: 
    explicit A(const char*); 
    A& operator=(A val)  { swap(val); return *this; } 
    void swap(A& other)  { std::swap(data, other.data); } 
    A(const A&); 
    ~A(); 

private: 
    const char* data; 
}; 

class B 
{ 
public: 
    explicit B(const char*); 
    B& operator=(const B& val) { B tmp(val); swap(tmp); return *this; } 
    void swap(B& other)   { std::swap(data, other.data); } 
    B(const B&); 
    ~B(); 

private: 
    const char* data; 
}; 

void f(A& a, B& b) 
{ 
    a = A("Hello"); 
    b = B("World"); 
} 
+0

Es fällt auf, dass keine Klasse Datenelemente hat. Könnten Sie Ihr Beispiel auch bearbeiten, um den vertikalen Leerraum etwas zu minimieren? –

+0

@Neil: Datenmitglieder machen keinen Unterschied, ich habe nur eine 'lange Daten [32]; zu beiden Strukturen in meinem Test-Kabelbaum hinzugefügt. Theoretisch könnte das Datenelement von den Konstruktoren berührt werden, die ich nicht definiert habe. Die Aufrufe, die für die Konstruktoren generiert werden, bleiben gleich, ein Aufruf von 'B :: B (const B &)', null Aufrufe an 'A :: A (const A &)'. –

+0

Ah, ich denke ich sehe was du meinst. Ich fügte ein weiteres Datenelement des Klassentyps (Klasse C) mit deklariertem aber undefiniertem Konstruktor, Kopierkonstruktor, Destruktor und einem void * -Datenelement hinzu. Es wurde zwar ein zusätzlicher Code für die Ausnahmebehandlung erzeugt und einige Aufrufe von 'C :: ~ C()' an den erwarteten Stellen, aber der Unterschied zwischen dem Aufrufen von 'B :: B (const B &)' und 'A :: A (const A &) '. Ob dies wirklich einen Unterschied macht, ist offen für Diskussionen, aber die Möglichkeit ist sicherlich da. –

0

diese wie diese es einfacher für einen Compiler könnte

foo bar; 
bar = f(); 

foo &foo::operator=(foo other) {/*...*/ return *this;} 
foo f(); 

in Code Aufgrund der Aufruf des Kopierkonstruktors zu beseitigen. Mit RVO könnte es die Adresse des Operators other Parameter als die Stelle für f() verwenden, um seinen Rückgabewert bei zu konstruieren.

Es scheint, dass diese Optimierung auch für den zweiten Fall möglich ist, obwohl ich glaube, dass es schwieriger sein könnte. (Besonders wenn der Bediener nicht inline ist.)