Frage

Ich frage mich die beste Form für meine Konstrukteure. Hier ist ein Beispielcode:

class Y { ... }

class X
{
public:
  X(const Y& y) : m_y(y) {} // (a)
  X(Y y) : m_y(y) {} // (b)
  X(Y&& y) : m_y(std::forward<Y>(y)) {} // (c)

  Y m_y;
}

Y f() { return ... }

int main()
{
  Y y = f();
  X x1(y); // (1)
  X x2(f()); // (2)
}

Von dem, was ich verstehe, ist dies die beste die Compiler in jeder Situation tun.

(1a) y kopiert wird in x1.m_y (1 Kopie)

(1 b) y in das Argument des Konstruktors von X kopiert wird, und dann in x1.m_y kopiert (2 Kopien)

(1c) y bewegt wird, in x1.m_y (1 move)

(2a) Ergebnis f () ist in x2.m_y kopiert (1 Kopie)

(2b) f () in das Argument des Konstruktors aufgebaut ist, und dann an x2.m_y kopiert (1 Kopie)

(2c) f () wird auf dem Stapel angelegt und dann zog in x2.m_y (1 move)

Jetzt ein paar Fragen:

  1. In beiden Punkten durch const Verweis übergeben ist nicht schlechter, und manchmal besser als Pass von Wert. Dies scheint gegen die Diskussion zu gehen auf "Want Speed? Pass von Wert. ". Für C ++ (nicht C ++ 0x), soll ich mit Pass-Stick durch konstante Referenz für diese Konstrukteure, oder sollte ich von Wert Go Pass? Und für C ++ 0x, sollte ich von rvalue Referenz über Pass von Wert übergeben Sie?

  2. Für (2), ich würde es vorziehen, wenn die temporären direkt in x.m_y errichtet wurde. Auch die rvalue Version erfordert ich denke, eine Bewegung, die, es sei denn, das Objekt dynamische Speicher reserviert, so viel Arbeit wie eine Kopie ist. Gibt es eine Möglichkeit, um Code dies so dass der Compiler diese Kopien und bewegt sich zu vermeiden, ist erlaubt?

  3. Ich habe eine Menge von Annahmen in beide gemacht, was ich denke, die Compiler am besten können und in meinen Fragen selbst. Bitte korrigieren Sie alle diese, wenn sie falsch sind.

War es hilfreich?

Lösung

Ich habe einige Beispiele zusammen geworfen. Ich verwendete GCC 4.4.4 in all dies.

Einfacher Fall ohne -std=c++0x

Zuerst habe ich zusammen ein sehr einfaches Beispiel mit zwei Klassen, die ein std::string akzeptieren jeden.

#include <string>
#include <iostream>

struct A /* construct by reference */
  {
    std::string s_;

    A (std::string const &s) : s_ (s)
      {
        std::cout << "A::<constructor>" << std::endl;
      }
    A (A const &a) : s_ (a.s_)
      {
        std::cout << "A::<copy constructor>" << std::endl;
      }
    ~A ()
      {
        std::cout << "A::<destructor>" << std::endl;
      }
  };

struct B /* construct by value */
  {
    std::string s_;

    B (std::string s) : s_ (s)
      {
        std::cout << "B::<constructor>" << std::endl;
      }
    B (B const &b) : s_ (b.s_)
      {
        std::cout << "B::<copy constructor>" << std::endl;
      }
    ~B ()
      {
        std::cout << "B::<destructor>" << std::endl;
      }
  };

static A f () { return A ("string"); }
static A f2 () { A a ("string"); a.s_ = "abc"; return a; }
static B g () { return B ("string"); }
static B g2 () { B b ("string"); b.s_ = "abc"; return b; }

int main ()
  {
    A a (f ());
    A a2 (f2 ());
    B b (g ());
    B b2 (g2 ());

    return 0;
  }

Die Ausgabe dieses Programms auf stdout ist wie folgt:

A::<constructor>
A::<constructor>
B::<constructor>
B::<constructor>
B::<destructor>
B::<destructor>
A::<destructor>
A::<destructor>

Fazit

war GCC Lage, jeden temporären A oder B weg zu optimieren. Dies steht im Einklang mit der C ++ FAQ . Grundsätzlich GCC kann (und ist bereit,) Code zu generieren, dass Konstrukte a, a2, b, b2 anstelle , auch wenn eine Funktion, die appearantly kehrt nach Wert bezeichnet wird. Dadurch kann GCC viele der Provisorien vermeiden, dessen Existenz könnte durch einen Blick auf den Code „geschlossen“ haben.

Das nächste, was wir sehen wollen, ist, wie oft std::string tatsächlich in dem obigen Beispiel kopiert wird. Lassen Sie uns ersetzen std::string mit etwas, das wir besser beobachten und sehen.

Realistische Fall ohne -std=c++0x

#include <string>
#include <iostream>

struct S
  {
    std::string s_;

    S (std::string const &s) : s_ (s)
      {
        std::cout << "  S::<constructor>" << std::endl;
      }
    S (S const &s) : s_ (s.s_)
      {
        std::cout << "  S::<copy constructor>" << std::endl;
      }
    ~S ()
      {
        std::cout << "  S::<destructor>" << std::endl;
      }
  };

struct A /* construct by reference */
  {
    S s_;

    A (S const &s) : s_ (s) /* expecting one copy here */
      {
        std::cout << "A::<constructor>" << std::endl;
      }
    A (A const &a) : s_ (a.s_)
      {
        std::cout << "A::<copy constructor>" << std::endl;
      }
    ~A ()
      {
        std::cout << "A::<destructor>" << std::endl;
      }
  };

struct B /* construct by value */
  {
    S s_;

    B (S s) : s_ (s) /* expecting two copies here */
      {
        std::cout << "B::<constructor>" << std::endl;
      }
    B (B const &b) : s_ (b.s_)
      {
        std::cout << "B::<copy constructor>" << std::endl;
      }
    ~B ()
      {
        std::cout << "B::<destructor>" << std::endl;
      }
  };

/* expecting a total of one copy of S here */
static A f () { S s ("string"); return A (s); }

/* expecting a total of one copy of S here */
static A f2 () { S s ("string"); s.s_ = "abc"; A a (s); a.s_.s_ = "a"; return a; }

/* expecting a total of two copies of S here */
static B g () { S s ("string"); return B (s); }

/* expecting a total of two copies of S here */
static B g2 () { S s ("string"); s.s_ = "abc"; B b (s); b.s_.s_ = "b"; return b; }

int main ()
  {
    A a (f ());
    std::cout << "" << std::endl;
    A a2 (f2 ());
    std::cout << "" << std::endl;
    B b (g ());
    std::cout << "" << std::endl;
    B b2 (g2 ());
    std::cout << "" << std::endl;

    return 0;
  }

Und die Ausgabe leider erfüllt die Erwartungen:

  S::<constructor>
  S::<copy constructor>
A::<constructor>
  S::<destructor>

  S::<constructor>
  S::<copy constructor>
A::<constructor>
  S::<destructor>

  S::<constructor>
  S::<copy constructor>
  S::<copy constructor>
B::<constructor>
  S::<destructor>
  S::<destructor>

  S::<constructor>
  S::<copy constructor>
  S::<copy constructor>
B::<constructor>
  S::<destructor>
  S::<destructor>

B::<destructor>
  S::<destructor>
B::<destructor>
  S::<destructor>
A::<destructor>
  S::<destructor>
A::<destructor>
  S::<destructor>

Fazit

GCC war nicht kann die temporäre S von B Konstruktor erstellt optimieren entfernt. den Standard-Copykonstruktor von S Mit nicht ändern. Ändern f, g sein

static A f () { return A (S ("string")); } // still one copy
static B g () { return B (S ("string")); } // reduced to one copy!

hat die angegebene Wirkung haben. Es scheint, dass GCC bereit ist, das Argument zu B Konstruktor an seinem Platz zu konstruieren, aber zögerlich B-Mitglied an seinem Platz zu konstruieren. Zu beachten ist, dass nach wie vor keine temporären A oder B erstellt werden. Das heißt a, a2, b, b2 noch gebaut werden anstelle . Abkühlen lassen.

Lassen Sie uns nun untersuchen, wie sich die neue Bewegung Semantik das zweite Beispiel beeinflussen können.

Realistische Fall mit -std=c++0x

Erwägen Sie den folgenden Konstruktor zu S

    S (S &&s) : s_ ()
      {
        std::swap (s_, s.s_);
        std::cout << "  S::<move constructor>" << std::endl;
      }

Und B Konstruktor Wechsel zu

    B (S &&s) : s_ (std::move (s)) /* how many copies?? */
      {
        std::cout << "B::<constructor>" << std::endl;
      }

Wir bekommen diese Ausgabe

  S::<constructor>
  S::<copy constructor>
A::<constructor>
  S::<destructor>

  S::<constructor>
  S::<copy constructor>
A::<constructor>
  S::<destructor>

  S::<constructor>
  S::<move constructor>
B::<constructor>
  S::<destructor>

  S::<constructor>
  S::<move constructor>
B::<constructor>
  S::<destructor>

B::<destructor>
  S::<destructor>
B::<destructor>
  S::<destructor>
A::<destructor>
  S::<destructor>
A::<destructor>
  S::<destructor>

So konnten wir ersetzen vier Kopien mit zwei Zügen nach Pass von rvalue verwendet wird.

Aber wir ein gebrochenes Programm tatsächlich gebaut.

Recall g, g2

static B g ()  { S s ("string"); return B (s); }
static B g2 () { S s ("string"); s.s_ = "abc"; B b (s); /* s is zombie now */ b.s_.s_ = "b"; return b; }

Die markierte Stelle zeigt das Problem. Ein Umzug wurde auf einem Objekt durchgeführt, die keine vorübergehende ist. Das ist, weil rvalue Referenzen verhalten sich wie lvalue Referenzen außer sie können auch binden an Provisorien. Also haben wir mit einer Überlastung B Konstruktor dürfen nicht vergessen, dass ein konstanten lvalue Bezug nimmt.

    B (S const &s) : s_ (s)
      {
        std::cout << "B::<constructor2>" << std::endl;
      }

Sie werden dann feststellen, dass beide g, g2 Ursache „constructor2“ zu nennen, da das Symbol s in jedem Fall eine bessere Passform für eine konstante Referenz ist als für eine R-Wert Referenz. Wir können den Compiler überzeugen, eine Bewegung in g in einem von zwei Wegen zu tun:

static B g ()  { return B (S ("string")); }
static B g ()  { S s ("string"); return B (std::move (s)); }

Schlussfolgerungen

Do Return-by-Wert. Der Code wird besser lesbar als "einen Verweis füllen gebe ich Ihnen" Code und schneller und vielleicht sogar mehr Ausnahme sicher.

Betrachten f zu

Ändern
static void f (A &result) { A tmp; /* ... */ result = tmp; } /* or */
static void f (A &result) { /* ... */ result = A (S ("string")); }

Das wird treffen die starke Garantie nur, wenn A die Zuordnung es bietet. Die Kopie in result kann nicht übersprungen werden, noch kann tmp anstelle von result konstruiert werden, da result nicht gebaut. Somit ist es langsamer als zuvor, in dem kein Kopiervorgang erforderlich war. C ++ 0x-Compiler und bewegen Zuweisungsoperatoren would reduziert den Aufwand, aber es ist immer noch langsamer als auf Rückkehr-by-Wert.

Return-by-Wert stellt die starke Garantie leichter. Das Objekt wird an Ort und Stelle aufgebaut. Wenn ein Teil davon ausfällt und andere Teile sind bereits aufgebaut worden ist, wird die normale Abwicklung aufzuräumen und, solange S Konstruktor die grundlegende Garantie für ihre eigenen Mitglieder und der starke Garantie im Hinblick auf globale Elemente erfüllt, die ganze Rücklauf- by-value-Prozess bietet tatsächlich die starke Garantie.

Immer nach Wert übergeben, wenn Sie Kopie fahren (auf den Stapel) ohnehin

Wie bereits erwähnt in Want Geschwindigkeit? Übergeben von Wert. . Der Compiler kann Code generieren, dass Konstrukte, wenn möglich, die Argument der Anrufer vorhanden, um die Kopie zu beseitigen, die es nicht tun, wenn Sie mit Bezug nehmen und dann manuell kopieren. Hauptbeispiel: Haben nicht schreiben diese (aus zitierten Artikel genommen)

T& T::operator=(T const& x) // x is a reference to the source
{ 
    T tmp(x);          // copy construction of tmp does the hard work
    swap(*this, tmp);  // trade our resources for tmp's
    return *this;      // our (old) resources get destroyed with tmp 
}

aber immer bevorzugen diese

T& T::operator=(T x)    // x is a copy of the source; hard work already done
{
    swap(*this, x);  // trade our resources for x's
    return *this;    // our (old) resources get destroyed with x
}

Wenn Sie durch const Verweis auf einen nicht-Stack-Frame Stelle Pass kopieren pre C ++ 0x und zusätzlich durch rvalue Bezugsposten C geben ++ 0x

Wir sahen schon. Pass durch Verweis verursacht weniger Kopien zu, wenn anstelle Bau ist unmöglich als Pass von Wert. Und C ++ 0x Umzug Semantik ersetzen können viele Kopien mit weniger und billiger bewegt. Aber tun bedenken Sie, dass ein Zombie machen bewegt, wird das Objekt aus, das von verschoben wurde. Ein Umzug ist das Kopieren nicht. Nur einen Konstruktor bereitstellt, die rvalue Referenzen akzeptiert Dinge brechen kann, wie oben gezeigt.

Wenn Sie einen Nicht-Stack-Frame Speicherort kopieren möchten, und haben swap, sollten nach Wert vorbei sowieso (pre C ++ 0x)

Wenn Sie billigen Standard-Aufbau haben, dass mit einem swap kombiniert kann effizienter sein als Sachen zu kopieren um. Erwägen S Konstruktor sein

    S (std::string s) : s_ (/* is this cheap for your std::string? */)
      {
        s_.swap (s); /* then this may be faster than copying */
        std::cout << "  S::<constructor>" << std::endl;
      }
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top