Vantaggi di pass-by-value e std :: move rispetto a pass-by-reference


105

Al momento sto imparando il C ++ e cerco di evitare di prendere cattive abitudini. Da quello che ho capito, clang-tidy contiene molte "best practice" e cerco di rispettarle nel miglior modo possibile (anche se non capisco necessariamente perché siano ancora considerate buone), ma non sono sicuro se capire cosa è consigliato qui.

Ho usato questa classe dal tutorial:

class Creature
{
private:
    std::string m_name;

public:
    Creature(const std::string &name)
            :  m_name{name}
    {
    }
};

Questo porta a un suggerimento da parte di clang-tidy che dovrei passare per valore invece di riferimento e utilizzare std::move. Se lo faccio, ricevo il suggerimento di fare nameun riferimento (per assicurarmi che non venga copiato ogni volta) e l'avviso che std::movenon avrà alcun effetto perchéname è un constquindi dovrei rimuoverlo.

L'unico modo in cui non ricevo un avviso è rimuovere del consttutto:

Creature(std::string name)
        :  m_name{std::move(name)}
{
}

Il che sembra logico, poiché l'unico vantaggio di constera evitare di fare confusione con la stringa originale (cosa che non accade perché ho passato per valore). Ma ho letto su CPlusPlus.com :

Sebbene si noti che -nella libreria standard- lo spostamento implica che l'oggetto spostato-da sia lasciato in uno stato valido ma non specificato. Ciò significa che, dopo tale operazione, il valore dell'oggetto spostato deve essere solo distrutto o assegnato a un nuovo valore; accedendovi altrimenti si ottiene un valore non specificato.

Ora immagina questo codice:

std::string nameString("Alex");
Creature c(nameString);

Poiché nameStringviene passato per valore, std::moveinvaliderà solo nameall'interno del costruttore e non toccherà la stringa originale. Ma quali sono i vantaggi di questo? Sembra che il contenuto venga copiato solo una volta, comunque, se passo per riferimento quando chiamom_name{name} , se passo per valore quando lo passo (e poi viene spostato). Capisco che questo è meglio che passare per valore e non usare std::move(perché viene copiato due volte).

Quindi due domande:

  1. Ho capito bene cosa sta succedendo qui?
  2. C'è qualche vantaggio nell'usare std::moveil passaggio per riferimento e solo per chiamare m_name{name}?

3
Con il passaggio per riferimento, Creature c("John");crea una copia extra
user253751

1
Questo collegamento potrebbe essere una lettura preziosa, copre anche il passaggio std::string_viewe l'SSO.
lubgr

Ho scoperto che clang-tidyè un ottimo modo per farmi ossessionare da microottimizzazioni non necessarie a scapito della leggibilità. La domanda da porsi qui, prima di ogni altra cosa, è quante volte chiamiamo effettivamente il Creaturecostruttore.
cz

Risposte:


36
  1. Ho capito bene cosa sta succedendo qui?

Sì.

  1. C'è qualche vantaggio nell'usare std::moveil passaggio per riferimento e solo per chiamare m_name{name}?

Una firma di funzione facile da comprendere senza sovraccarichi aggiuntivi. La firma rivela immediatamente che l'argomento verrà copiato: ciò evita ai chiamanti di chiedersi se un const std::string&riferimento possa essere memorizzato come membro di dati, diventando eventualmente un riferimento sospeso in seguito. E non è necessario sovraccaricare gli argomenti std::string&& namee const std::string&per evitare copie non necessarie quando vengono passati rvalues ​​alla funzione. Superare un lvalue

std::string nameString("Alex");
Creature c(nameString);

alla funzione che prende il suo argomento per valore provoca una copia e una mossa. Passaggio di un rvalue alla stessa funzione

std::string nameString("Alex");
Creature c(std::move(nameString));

provoca due costruzioni di movimento. Al contrario, quando il parametro della funzione èconst std::string& , ci sarà sempre una copia, anche quando si passa un argomento rvalue. Questo è chiaramente un vantaggio fintanto che il tipo di argomento è economico da spostare-costruire (questo è il caso di std::string).

Ma c'è uno svantaggio da considerare: il ragionamento non funziona per le funzioni che assegnano l'argomento della funzione a un'altra variabile (invece di inizializzarlo):

void setName(std::string name)
{
    m_name = std::move(name);
}

provocherà una deallocazione della risorsa a cui m_namefa riferimento prima che venga riassegnata. Consiglio di leggere l'articolo 41 in Effective Modern C ++ e anche questa domanda .


Ciò ha senso, soprattutto perché rende la dichiarazione più intuitiva da leggere. Non sono sicuro di aver compreso appieno la parte di deallocazione della tua risposta (e di capire il thread collegato), quindi solo per controllare Se uso move, lo spazio viene deallocato. Se non lo uso move, viene deallocato solo se lo spazio allocato è troppo piccolo per contenere la nuova stringa, con conseguente miglioramento delle prestazioni. È corretto?
Blackbot

1
Sì, è esattamente così. Quando si assegna a m_nameda un const std::string&parametro, la memoria interna viene riutilizzata fintanto che si m_nameadatta. Quando si assegna lo spostamento m_name, la memoria deve essere rilasciata in anticipo. Altrimenti, era impossibile "rubare" le risorse dal lato destro dell'incarico.
lubgr

Quando diventa un riferimento penzolante? Penso che l'elenco di inizializzazione utilizzi la copia profonda.
Li Taiji

103
/* (0) */ 
Creature(const std::string &name) : m_name{name} { }
  • Un valore passato si lega a name, quindi viene copiato in m_name.

  • Un valore passato si lega a name, quindi viene copiato in m_name.


/* (1) */ 
Creature(std::string name) : m_name{std::move(name)} { }
  • Un lvalue passato viene copiato in name, quindi è spostato in m_name.

  • Un passato rvalue viene spostato in name, poi viene spostato in m_name.


/* (2) */ 
Creature(const std::string &name) : m_name{name} { }
Creature(std::string &&rname) : m_name{std::move(rname)} { }
  • Un valore passato si lega a name, quindi ècopiato in m_name.

  • Un valore passato si lega a rname, quindi viene spostato in m_name.


Poiché le operazioni di spostamento sono generalmente più veloci delle copie, (1) è migliore di (0) se si superano molti provvisori. (2) è ottimale in termini di copie / mosse, ma richiede la ripetizione del codice.

La ripetizione del codice può essere evitata con un inoltro perfetto :

/* (3) */
template <typename T,
          std::enable_if_t<
              std::is_convertible_v<std::remove_cvref_t<T>, std::string>, 
          int> = 0
         >
Creature(T&& name) : m_name{std::forward<T>(name)} { }

Facoltativamente, si potrebbe voler limitare Tper limitare il dominio dei tipi con cui questo costruttore può essere istanziato (come mostrato sopra). C ++ 20 mira a semplificare questo con Concepts .


In C ++ 17, i prvalues sono influenzati dall'elisione della copia garantita , che - quando applicabile - ridurrà il numero di copie / spostamenti quando si passano argomenti alle funzioni.


Per (1) il valore pr e il caso xvalue non sono identici poiché c ++ 17 no?
Oliv

1
Nota che in questo caso non hai bisogno dello SFINAE per perfezionare il forward. Serve solo per chiarire le ambiguità. È plausibilmente utile per i potenziali messaggi di errore quando si passano argomenti
errati

@Oliv Sì. xvalues ​​deve essere spostato, mentre i prvalues ​​possono essere eliminati :)
Rakete1111

1
Possiamo scrivere: Creature(const std::string &name) : m_name{std::move(name)} { }nella (2) ?
skytree

4
@skytree: non puoi muoverti da un oggetto const, poiché lo spostamento muta la sorgente. Questo verrà compilato, ma ne farà una copia.
Vittorio Romeo

1

Il modo in cui passi non è l'unica variabile qui, ciò che passi fa la grande differenza tra i due.

In C ++, abbiamo tutti i tipi di categorie di valori e questo "idioma" esiste per i casi in cui passi un rvalue (come "Alex-string-literal-that-constructs-temporary-std::string"o std::move(nameString)), che si traduce in 0 copie da std::stringfare (il tipo non deve nemmeno essere costruibile tramite copia per gli argomenti rvalue) e utilizza solo std::stringil costruttore di spostamento di.

Domande e risposte piuttosto correlate .


1

Esistono diversi svantaggi dell'approccio passaggio per valore e movimento rispetto al riferimento passaggio per passaggio (rv):

  • provoca la generazione di 3 oggetti invece di 2;
  • passare un oggetto in base al valore può portare a un sovraccarico di stack aggiuntivo, perché anche una normale classe di stringhe è tipicamente almeno 3 o 4 volte più grande di un puntatore;
  • la costruzione degli oggetti argomento verrà eseguita sul lato del chiamante, causando un aumento del codice;

Puoi chiarire perché causerebbe la generazione di 3 oggetti? Da quello che ho capito posso semplicemente passare "Peter" come una stringa. Questo verrebbe generato, copiato e poi spostato, no? E lo stack non sarebbe stato utilizzato a un certo punto a prescindere? Non nel punto della chiamata del costruttore, ma nella m_name{name}parte in cui viene copiato?
Blackbot

@Blackbot mi riferivo al tuo esempio std::string nameString("Alex"); Creature c(nameString);un oggetto è nameString, un altro è l'argomento della funzione e il terzo è un campo di classe.
user7860670

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.