Domanda

Mi chiedo la forma migliore per i miei costruttori. Ecco alcuni esempi di codice:

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)
}

Da quello che ho capito, questo è il meglio che il compilatore può fare in ogni situazione.

(1a) y viene copiato in x1.m_y (1 copia)

(1b) y vengono copiati nel parametro del costruttore di X, e poi copiato nella x1.m_y (2 copie)

(1c) y viene spostata in x1.m_y (1 move)

(2a) risultato di f () viene copiato nella x2.m_y (1 copia)

(2b) f () è realizzata nel parametro del costruttore, e poi copiato x2.m_y (1 copia)

(2c) f () viene creata in pila, e poi si trasferisce in x2.m_y (1 move)

Ora un paio di domande:

  1. In entrambi i casi, passare per riferimento const non è peggiore, e, a volte meglio di passaggio per valore. Questo sembra andare contro la discussione su "Vuoi Velocità? Passo per valore. ". Per C ++ (non C ++ 0x), devo attaccare con passaggio per riferimento const per questi costruttori, o devo andare passare per valore? E per C ++ 0x, devo fare passare per riferimento rvalue sopra passaggio per valore?

  2. Per (2), preferirei se il provvisorio è stato costruito direttamente in x.m_y. Anche la versione rvalue Credo che richiede una mossa che, a meno che l'oggetto alloca la memoria dinamica, è tanto lavoro come una copia. Esiste un modo di codificare questo in modo che il compilatore è consentito per evitare queste copie e si muove?

  3. Ho fatto un sacco di ipotesi sia quello che penso il compilatore può fare meglio e nelle mie domande stesse. Si prega di correggere uno di questi se non sono corretti.

È stato utile?

Soluzione

Ho gettato insieme alcuni esempi. Ho usato GCC 4.4.4 in tutto questo.

caso semplice, senza -std=c++0x

Per prima cosa, ho messo insieme un esempio molto semplice, con due classi che accettano un std::string ciascuna.

#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;
  }

L'output di questo programma su stdout è la seguente:

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

Conclusione

GCC è stato in grado di ottimizzare ogni A temporanea o B via. Ciò è coerente con la C ++ FAQ . In sostanza, GCC può (ed è disposto a) del codice che costrutti a, a2, b, b2 al posto , anche se viene chiamata una funzione che restituisce appearantly per valore. In tal modo GCC può evitare molti dei provvisori di cui un'esistenza avrebbe potuto "dedurre", cercando in codice.

La prossima cosa che vogliamo vedere è quanto spesso std::string è in realtà copiato nell'esempio di cui sopra. Cerchiamo di sostituire std::string con qualcosa che possiamo osservare meglio e vedere.

caso realistica, senza -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;
  }

E l'uscita, purtroppo, incontra le aspettative:

  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>

Conclusione

GCC è stato non in grado di ottimizzare il via S temporanea creata da costruttore di B. Utilizzando il costruttore di copia di default di S non ha cambiato questo. Cambiare f, g di essere

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

ha avuto l'effetto indicato. Sembra che GCC è disposto a costruire l'argomento per il costruttore di B in atto, ma riluttanti a costruire membro di B a posto. Fare nota che A ancora temporanea o B sono creati. Ciò significa a, a2, b, b2 sono ancora in fase di realizzazione al posto . Raffreddare.

Diamo ora studiare come la nuova semantica mossa può influenzare il secondo esempio.

caso realistico, con -std=c++0x

Considerare l'aggiunta del seguente costruttore per S

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

E cambiando il costruttore di B a

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

Riceviamo questa uscita

  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>

Quindi, siamo stati in grado di sostituire quattro copie con due mosse utilizzando passaggio da rvalue.

Ma in realtà abbiamo costruito un programma di rotto.

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; }

The marcati mostra il problema. Una mossa è stata fatta su un oggetto che non è una temporanea. Questo perché i riferimenti rvalue si comportano come riferimenti lvalue tranne essi possono anche legano al provvisori. Quindi non dobbiamo dimenticare al costruttore di sovraccarico di B con uno che prende un riferimento valore assegnabile costante.

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

È quindi notare che sia g, g2 causa "constructor2" per essere chiamato, in quanto il simbolo s in entrambi i casi è una migliore vestibilità per un riferimento const che per un riferimento rvalue. Siamo in grado di persuadere il compilatore di fare una mossa in g in uno dei due modi:

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

Conclusioni

Do ritorno per valore. Il codice sarà più leggibile di "riempire un riferimento io ti do" codice e più veloce e forse anche di più un'eccezione di sicurezza.

È consigliabile modificare f a

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

in grado di soddisfare la forte garanzia solo se l'assegnazione del A fornisce. La copia in result non può essere saltata, né può tmp essere costruito in luogo di result, poiché result non è in costruzione. Così, è più lento di prima, dove era necessaria alcuna copia. C ++ 0x compilatori e operatori di assegnazione mossa WOUld ridurre l'overhead, ma è ancora più lento rispetto al ritorno per valore.

Return-by-value fornisce la garanzia forte più facilmente. L'oggetto viene costruito in posizione. Se una parte di questo non riesce e altre parti sono già stati costruiti, lo svolgimento normale pulirà e, fintanto che il costruttore di S soddisfa la garanzia di base per quanto riguarda i propri membri e la forte garanzia per quanto riguarda gli elementi a livello mondiale, l'intero Return- processo per valore fornisce in realtà la garanzia forte.

passare sempre per valore, se avete intenzione di copia (in pila) in ogni caso

Come discusso nel velocità volere di più? Passaggio per valore. . Il compilatore può generare il codice che costruisce, se possibile, l'argomento del chiamante sul posto, eliminando la copia, il che non può fare quando si prende come riferimento e quindi copiare manualmente. Esempio principale: Fare non scrivere questo (tratto da un articolo citato)

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 
}

ma sempre preferire questo

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
}

Se si desidera copiare un non-stack passaggio posizione di frame con riferimento const pre C ++ 0x e inoltre passare per rvalue posta riferimento C ++ 0x

Abbiamo già visto questo. Passaggio per riferimento causa meno copie avvenire quando in luogo di costruzione è impossibile che passaggio per valore. E la semantica mossa di C ++ 0x possono sostituire molte copie con un minor numero di mosse e meno costosi. Ma tenete a mente che lo spostamento farà uno zombie fuori dell'oggetto che è stato spostato da. In movimento non è la copia. Basta fornire un costruttore che accetta i riferimenti rvalue possono rompere le cose, come indicato sopra.

Se si desidera copiare in una posizione di frame non-stack e hanno swap, considera passaggio per valore in ogni caso (pre C ++ 0x)

Se si dispone la costruzione di default a buon mercato, che combinato con un swap possono essere più efficiente di copiare roba in giro. Si consideri il costruttore di S di essere

    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;
      }
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top