2012-03-23 15 views
3

die folgende Klasse Stellen Sie sich vor, die eine Ressource verwaltet (meine Frage ist nur über den Umzug Zuweisungsoperator):Fragen über den Umzug Zuweisungsoperator

struct A 
{ 
    std::size_t s; 
    int* p; 
    A(std::size_t s) : s(s), p(new int[s]){} 
    ~A(){delete [] p;} 
    A(A const& other) : s(other.s), p(new int[other.s]) 
    {std::copy(other.p, other.p + s, this->p);} 
    A(A&& other) : s(other.s), p(other.p) 
    {other.s = 0; other.p = nullptr;} 
    A& operator=(A const& other) 
    {A temp = other; std::swap(*this, temp); return *this;} 
    // Move assignment operator #1 
    A& operator=(A&& other) 
    { 
     std::swap(this->s, other.s); 
     std::swap(this->p, other.p); 
     return *this; 
    } 
    // Move assignment operator #2 
    A& operator=(A&& other) 
    { 
     delete [] p; 
     s = other.s; 
     p = other.p; 
     other.s = 0; 
     other.p = nullptr; 
     return *this; 
    } 
}; 

Frage:

Was sind die Vor- und Nachteile der zwei Bewegungszuweisungsoperatoren # 1 und # 2 oben? Ich glaube, der einzige Unterschied, den ich sehen kann ist, dass std::swap den Speicher der LHs bewahrt, aber ich sehe nicht, wie das nützlich wäre, da rvalues ​​sowieso zerstört würden. Vielleicht wäre die einzige Zeit mit etwas wie a1 = std::move(a2);, aber selbst in diesem Fall sehe ich keinen Grund, # 1 zu verwenden.

+0

Ich verstehe nicht ganz Ihre Frage. Warum benutzen Sie nicht einfach ein 'std :: unique_ptr ' Mitglied (anstelle von 'int *') und haben die betreffenden Operatoren entweder automatisch generiert oder '= default'? – Walter

+0

@Walter: Die Frage war ein Lernexperiment und nicht etwas, was ich in der Produktion verwenden würde. Ich würde stattdessen 'std :: vector' wählen. Zum Zeitpunkt des Schreibens wurde 'default' nicht von MSVC implementiert. –

+0

Gut genug, aber "MSVC" war nicht unter den Tags. – Walter

Antwort

7

Dies ist ein Fall, in dem Sie wirklich messen sollten.

Und ich freue mich auf dem Kopie Zuordnung Betreiber OP und zu sehen, Ineffizienz:

A& operator=(A const& other) 
    {A temp = other; std::swap(*this, temp); return *this;} 

Was passiert, wenn *this und other hat die gleichen s?

Es scheint mir, dass eine intelligentere Kopie Zuordnung könnte eine Reise auf den Haufen zu vermeiden, wenn s == other.s. zu tun haben, alle wäre es ist die Kopie:

A& operator=(A const& other) 
{ 
    if (this != &other) 
    { 
     if (s != other.s) 
     { 
      delete [] p; 
      p = nullptr; 
      s = 0; 
      p = new int[other.s]; 
      s = other.s; 
     } 
     std::copy(other.p, other.p + s, this->p); 
    } 
    return *this; 
} 

Wenn Sie nicht starke Ausnahme Sicherheit benötigen, nur grundlegende Ausnahme Sicherheit auf Kopie-Zuordnung (wie std::string, std::vector, etc.), dann gibt es eine mögliche Leistungsverbesserung mit den oben genannten. Wie viel? Messen.

Ich habe diese Klasse drei Möglichkeiten codiert:

Entwurf 1:

Verwenden Sie das obige Kopierzuweisungsoperator und dem Umzug Zuweisungsoperator # des OP 1.

Design 2:

Verwenden Sie das obige Kopierzuweisungsoperator und die 2 bewegen Zuweisungsoperator # der OP.

Design 3:

DeadMG Kopie Zuweisungsoperator für beide Kopie und Zuordnung bewegen.

Hier ist der Code, den ich zu Test verwendet:

#include <cstddef> 
#include <algorithm> 
#include <chrono> 
#include <iostream> 

struct A 
{ 
    std::size_t s; 
    int* p; 
    A(std::size_t s) : s(s), p(new int[s]){} 
    ~A(){delete [] p;} 
    A(A const& other) : s(other.s), p(new int[other.s]) 
    {std::copy(other.p, other.p + s, this->p);} 
    A(A&& other) : s(other.s), p(other.p) 
    {other.s = 0; other.p = nullptr;} 
    void swap(A& other) 
    {std::swap(s, other.s); std::swap(p, other.p);} 
#if DESIGN != 3 
    A& operator=(A const& other) 
    { 
     if (this != &other) 
     { 
      if (s != other.s) 
      { 
       delete [] p; 
       p = nullptr; 
       s = 0; 
       p = new int[other.s]; 
       s = other.s; 
      } 
      std::copy(other.p, other.p + s, this->p); 
     } 
     return *this; 
    } 
#endif 
#if DESIGN == 1 
    // Move assignment operator #1 
    A& operator=(A&& other) 
    { 
     swap(other); 
     return *this; 
    } 
#elif DESIGN == 2 
    // Move assignment operator #2 
    A& operator=(A&& other) 
    { 
     delete [] p; 
     s = other.s; 
     p = other.p; 
     other.s = 0; 
     other.p = nullptr; 
     return *this; 
    } 
#elif DESIGN == 3 
    A& operator=(A other) 
    { 
     swap(other); 
     return *this; 
    } 
#endif 
}; 

int main() 
{ 
    typedef std::chrono::high_resolution_clock Clock; 
    typedef std::chrono::duration<float, std::nano> NS; 
    A a1(10); 
    A a2(10); 
    auto t0 = Clock::now(); 
    a2 = a1; 
    auto t1 = Clock::now(); 
    std::cout << "copy takes " << NS(t1-t0).count() << "ns\n"; 
    t0 = Clock::now(); 
    a2 = std::move(a1); 
    t1 = Clock::now(); 
    std::cout << "move takes " << NS(t1-t0).count() << "ns\n"; 
} 

Hier wird der Ausgang bekam ich:

$ clang++ -std=c++11 -stdlib=libc++ -O3 -DDESIGN=1 test.cpp 
$ a.out 
copy takes 55ns 
move takes 44ns 
$ a.out 
copy takes 56ns 
move takes 24ns 
$ a.out 
copy takes 53ns 
move takes 25ns 
$ clang++ -std=c++11 -stdlib=libc++ -O3 -DDESIGN=2 test.cpp 
$ a.out 
copy takes 74ns 
move takes 538ns 
$ a.out 
copy takes 59ns 
move takes 491ns 
$ a.out 
copy takes 61ns 
move takes 510ns 
$ clang++ -std=c++11 -stdlib=libc++ -O3 -DDESIGN=3 test.cpp 
$ a.out 
copy takes 666ns 
move takes 304ns 
$ a.out 
copy takes 603ns 
move takes 446ns 
$ a.out 
copy takes 619ns 
move takes 317ns 

DESIGN 1 sieht für mich ziemlich gut.

Caveat: Wenn die Klasse Ressourcen hat, den ausgeplant „schnell“ werden müssen, wie Mutex-Sperre Eigentum oder Open-Staatseigentums Datei, der Design-2 bewegt Zuweisungsoperator von einer Korrektheit Sicht könnte besser sein. Aber wenn die Ressource einfach Speicher ist, ist es oft vorteilhaft, die Freigabe so lange wie möglich zu verzögern (wie im Anwendungsfall des OP).

Vorbehalt 2: Wenn Sie andere Anwendungsfälle kennen, die Ihnen wichtig sind, messen Sie sie. Sie könnten zu anderen Schlüssen kommen als ich hier habe.

Hinweis: Ich schätze die Leistung gegenüber "DRY". Der gesamte Code wird in einer Klasse gekapselt (struct A). Machen Sie struct A so gut wie Sie können. Und wenn Sie eine ausreichend hohe Qualität Job, dann Ihre Kunden von struct A (die Sie selbst sein können) wird nicht versucht werden, "RIA" (Reinvent It Again). Ich bevorzuge es, ein wenig Code innerhalb einer Klasse zu wiederholen, anstatt die Implementierung ganzer Klassen immer wieder zu wiederholen.

+0

Danke, das war sehr informativ. Ich war auch überrascht über den Umzug in Design # 2. –

+2

Es sieht so aus, als ob der Leistungsvorteil von Design 1 gegenüber Design 2 dadurch verursacht wird, dass das Testkabelbaum die Destruktoraufrufe nicht zeitlich steuert - wenn Sie das in Ihren Timing-Kabelbaum aufnehmen, würde ich erwarten, dass der Leistungsunterschied verschwindet. Ich bin auch etwas überrascht, dass LLVM a1 und a2 nicht vollständig optimiert. –

+0

Ich unterstütze stark Richard Smiths Kommentar.Dieses Timing ist ** unfair ** und vergleicht nicht wirklich die gleichen Dinge. Der getaktete Bereich sollte die Zerstörung des bewegten Objektes beinhalten, denn in der Praxis wird dies fast immer der Bewegung folgen. Ich bin erstaunt, dass du das nicht getan hast. – Walter

7

Es ist sinnvoller, # 1 als # 2 zu verwenden, denn wenn Sie # 2 verwenden, verletzen Sie DRY und duplizieren Ihre Destruktorlogik. Zweitens sollten Sie den folgenden Zuweisungsoperator:

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

Dies ist sowohl Kopie und Zuweisungsoperator für keine duplizierten Code- eine ausgezeichnete Form bewegen.

+0

Um des Fragestellers willen, was ist DRY? –

+0

Danke, das war ein Winkel, den ich nicht in Betracht zog. –

+1

DRY = "wiederhole dich nicht" –

3

Der von DeadMG gepostete Zuweisungsoperator macht alles richtig, wenn swap() betroffene Objekte nicht werfen können. Leider kann dies nicht immer garantiert werden! Insbesondere wenn Sie Stateful Allokatoren haben und dies nicht funktioniert. Wenn die Zuweiser unterscheiden können es scheint, dass Sie separate Kopie und verschieben Zuordnung wollen: die Copykonstruktor unbedingt eine Kopie übergeben den Allocator schaffen würde: würde testen

T& T::operator=(T const& other) { 
    T(other, this->get_allocator()).swap(*this); 
    return * this; 
} 

Der Umzug Zuordnung identisch sind und, wenn die Zuweiser wenn ja, nur swap() die beiden Objekte und sonst rufen Sie einfach die Kopie Zuordnung:

T& operator= (T&& other) { 
    if (this->get_allocator() == other.get_allocator()) { 
     this->swap(other); 
    } 
    else { 
     *this = other; 
    } 
    return *this; 
} 

die Version Wert ist eine viel einfachere Alternative nehmen, die, wenn die noexcept(v.swap(*this))true bevorzugt werden sollte.

Dies implizit auch die ursprüngliche qurstion: in Anwesenheit von werfen swap() und verschieben Zuordnung sind beide Implementierungen falsch, da sie nicht grundlegende Ausnahme sicher sind. Unter der Annahme, dass die einzige Quelle von Ausnahmen in swap() nicht übereinstimmende Zuordner sind, ist die obige Implementierung stark ausnahmesicher.