Che cos'è la semantica di movimento?


1703

Ho appena finito di ascoltare l' intervista al podcast della radio Software Engineering con Scott Meyers riguardo al C ++ 0x . La maggior parte delle nuove funzionalità ha avuto senso per me, e ora sono davvero entusiasta di C ++ 0x, con l'eccezione di una. Non riesco ancora a ottenere la semantica delle mosse ... Che cos'è esattamente?


20
Ho trovato [l'articolo del blog di Eli Bendersky] ( eli.thegreenplace.net/2011/12/15/… ) su valori e valori in C e C ++ piuttosto informativo. Cita anche riferimenti a valori in C ++ 11 e li introduce con piccoli esempi.
Nils,


19
Ogni anno mi chiedo di cosa tratta la "nuova" semantica di spostamento in C ++, la cerco su Google e arrivo a questa pagina. Ho letto le risposte, il mio cervello si è spento. Torno a C e dimentico tutto! Sono bloccato.
cielo

7
@sky Considerare std :: vector <> ... Da qualche parte c'è un puntatore a un array sull'heap. Se si copia questo oggetto, è necessario allocare un nuovo buffer e i dati dal buffer devono essere copiati nel nuovo buffer. C'è qualche circostanza in cui sarebbe giusto rubare semplicemente il puntatore? La risposta è SÌ, quando il compilatore sa che l'oggetto è temporaneo. La semantica di spostamento ti consente di definire il modo in cui le tue classi possono essere spostate e rilasciate in un oggetto diverso quando il compilatore sa che l'oggetto da cui stai spostando sta per scomparire.
dicroce,

L'unico riferimento che posso capire: learncpp.com/cpp-tutorial/… , ovvero il ragionamento originale della semantica di spostamento è da puntatori intelligenti.
jw_

Risposte:


2481

Trovo più semplice comprendere la semantica di spostamento con il codice di esempio. Cominciamo con una classe di stringhe molto semplice che contiene solo un puntatore a un blocco di memoria allocato in heap:

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = std::strlen(p) + 1;
        data = new char[size];
        std::memcpy(data, p, size);
    }

Poiché abbiamo scelto di gestire noi stessi la memoria, dobbiamo seguire la regola del tre . Ho intenzione di rimandare la scrittura dell'operatore di assegnazione e implementare solo il distruttore e il costruttore di copie per ora:

    ~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = std::strlen(that.data) + 1;
        data = new char[size];
        std::memcpy(data, that.data, size);
    }

Il costruttore della copia definisce cosa significa copiare oggetti stringa. Il parametro si const string& thatlega a tutte le espressioni di tipo stringa che consente di eseguire copie nei seguenti esempi:

string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3

Ora arriva l'intuizione chiave nella semantica del movimento. Nota che solo nella prima riga in cui copiamo xquesta copia profonda è davvero necessaria, perché potremmo voler ispezionare xpiù tardi e saremmo molto sorpresi se xfosse cambiato in qualche modo. Hai notato come ho appena detto xtre volte (quattro volte se includi questa frase) e intendevo sempre lo stesso identico oggetto ? Chiamiamo espressioni come x"lvalues".

Gli argomenti nelle righe 2 e 3 non sono valori, ma valori, poiché gli oggetti stringa sottostanti non hanno nomi, quindi il client non ha modo di ispezionarli nuovamente in un secondo momento. i valori indicano oggetti temporanei che vengono distrutti al punto e virgola successivo (per essere più precisi: alla fine dell'espressione completa che contiene lessicamente il valore). Questo è importante perché durante l'inizializzazione di be c, potevamo fare tutto ciò che volevamo con la stringa di origine e il client non poteva fare la differenza !

C ++ 0x introduce un nuovo meccanismo chiamato "riferimento al valore" che, tra le altre cose, ci consente di rilevare gli argomenti del valore tramite il sovraccarico della funzione. Tutto quello che dobbiamo fare è scrivere un costruttore con un parametro di riferimento rvalue. All'interno di quel costruttore possiamo fare tutto ciò che vogliamo con l'origine, purché lo lasciamo in qualche stato valido:

    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }

Cosa abbiamo fatto qui? Invece di copiare in profondità i dati dell'heap, abbiamo appena copiato il puntatore e quindi impostato il puntatore originale su null (per evitare che 'delete []' dal distruttore dell'oggetto di origine rilasci i nostri 'dati appena rubati'). In effetti, abbiamo "rubato" i dati che originariamente appartenevano alla stringa di origine. Ancora una volta, l'intuizione chiave è che il cliente non può in alcun caso rilevare che l'origine è stata modificata. Dato che non ne facciamo davvero una copia qui, chiamiamo questo costruttore un "costruttore di spostamento". Il suo compito è spostare le risorse da un oggetto all'altro invece di copiarle.

Congratulazioni, ora capisci le basi della semantica dei movimenti! Continuiamo implementando l'operatore di assegnazione. Se non hai familiarità con il linguaggio copia e scambia , impara e torna indietro, perché è un fantastico linguaggio C ++ relativo alla sicurezza delle eccezioni.

    string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};

Eh, tutto qui? "Dov'è il riferimento al valore?" potresti chiedere. "Non ne abbiamo bisogno qui!" è la mia risposta :)

Si noti che passiamo il parametro that per valore , quindi thatdeve essere inizializzato proprio come qualsiasi altro oggetto stringa. Esattamente come thatverrà inizializzato? Ai vecchi tempi di C ++ 98 , la risposta sarebbe stata "dal costruttore della copia". In C ++ 0x, il compilatore sceglie tra il costruttore della copia e il costruttore dello spostamento in base al fatto che l'argomento per l'operatore di assegnazione sia un valore lvalue o un valore rvalue.

Quindi, se dici a = b, il costruttore di copie si inizializzerà that(perché l'espressione bè un valore in valore) e l'operatore di assegnazione scambia i contenuti con una copia profonda appena creata. Questa è la definizione stessa del linguaggio di copia e scambio: crea una copia, scambia il contenuto con la copia, quindi elimina la copia lasciando l'ambito. Niente di nuovo qui.

Ma se dici a = x + y, il costruttore di mosse si inizializzerà that(perché l'espressione x + yè un valore), quindi non ci sono copie profonde coinvolte, solo una mossa efficiente. thatè ancora un oggetto indipendente dall'argomento, ma la sua costruzione era banale, poiché i dati dell'heap non dovevano essere copiati, ma semplicemente spostati. Non è stato necessario copiarlo perché x + yè un valore e, di nuovo, è possibile spostarsi da oggetti stringa indicati da valori.

Per riassumere, il costruttore di copie fa una copia profonda, perché la fonte deve rimanere intatta. Il costruttore di mosse, d'altra parte, può semplicemente copiare il puntatore e quindi impostare il puntatore nell'origine su null. Va bene "annullare" l'oggetto sorgente in questo modo, perché il client non ha modo di ispezionare nuovamente l'oggetto.

Spero che questo esempio abbia raggiunto il punto principale. C'è molto di più per valutare i riferimenti e spostare la semantica che ho intenzionalmente escluso per renderlo semplice. Se vuoi maggiori dettagli, vedi la mia risposta supplementare .


40
@Ma se il mio ctor sta ottenendo un valore, che non potrà mai essere usato in seguito, perché devo preoccuparmi di lasciarlo in uno stato coerente / sicuro? Invece di impostare that.data = 0, perché non lasciarlo?
einpoklum,

70
@einpoklum Perché senza that.data = 0, i personaggi verrebbero distrutti troppo presto (quando il temporaneo muore), e anche due volte. Vuoi rubare i dati, non condividerli!
fredoverflow,

19
@einpoklum Il distruttore pianificato regolarmente viene comunque eseguito, quindi è necessario assicurarsi che lo stato post-spostamento dell'oggetto di origine non causi un arresto anomalo. Meglio, dovresti assicurarti che l'oggetto sorgente possa anche essere il destinatario di un compito o altra scrittura.
CTMacUser

12
@pranitkothari Sì, tutti gli oggetti devono essere distrutti, anche spostati dagli oggetti. E poiché non vogliamo che l'array di caratteri venga eliminato quando ciò accade, dobbiamo impostare il puntatore su null.
Fredoverflow,

7
@ Virus721 delete[]su nullptr è definito dallo standard C ++ come no-op.
fredoverflow,

1057

La mia prima risposta è stata un'introduzione estremamente semplificata per spostare la semantica, e molti dettagli sono stati lasciati fuori di proposito per renderlo semplice. Tuttavia, c'è molto di più per spostare la semantica e ho pensato che fosse tempo di una seconda risposta per colmare le lacune. La prima risposta è già piuttosto vecchia e non è giusto sostituirla semplicemente con un testo completamente diverso. Penso che serva ancora bene come prima introduzione. Ma se vuoi approfondire, continua a leggere :)

Stephan T. Lavavej ha avuto il tempo di fornire un prezioso feedback. Grazie mille, Stephan!

introduzione

Spostare la semantica consente a un oggetto, a determinate condizioni, di diventare proprietario di risorse esterne di altri oggetti. Questo è importante in due modi:

  1. Trasformare copie costose in mosse economiche. Vedi la mia prima risposta per un esempio. Si noti che se un oggetto non gestisce almeno una risorsa esterna (direttamente o indirettamente attraverso i suoi oggetti membri), spostare la semantica non offrirà alcun vantaggio rispetto alla semantica della copia. In tal caso, copiare un oggetto e spostare un oggetto significa esattamente la stessa cosa:

    class cannot_benefit_from_move_semantics
    {
        int a;        // moving an int means copying an int
        float b;      // moving a float means copying a float
        double c;     // moving a double means copying a double
        char d[64];   // moving a char array means copying a char array
    
        // ...
    };
    
  2. Implementazione di tipi sicuri "solo mossa"; vale a dire tipi per i quali la copia non ha senso, ma lo spostamento ha. Gli esempi includono blocchi, handle di file e puntatori intelligenti con semantica di proprietà univoca. Nota: in questa risposta viene illustrato std::auto_ptrun modello di libreria standard C ++ 98 obsoleto, che è stato sostituito da std::unique_ptrin C ++ 11. I programmatori di C ++ intermedi hanno probabilmente almeno una certa familiarità con std::auto_ptr, e a causa della "semantica di movimento" che mostra, sembra un buon punto di partenza per discutere della semantica di movimento in C ++ 11. YMMV.

Che cos'è una mossa?

La libreria standard C ++ 98 offre un puntatore intelligente con una semantica di proprietà unica chiamata std::auto_ptr<T>. Nel caso in cui non si abbia familiarità auto_ptr, il suo scopo è garantire che un oggetto allocato in modo dinamico venga sempre rilasciato, anche a fronte di eccezioni:

{
    std::auto_ptr<Shape> a(new Triangle);
    // ...
    // arbitrary code, could throw exceptions
    // ...
}   // <--- when a goes out of scope, the triangle is deleted automatically

La cosa insolita auto_ptrè il suo comportamento "copiante":

auto_ptr<Shape> a(new Triangle);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        |
        |
  +-----|---+
  |   +-|-+ |
a | p | | | |
  |   +---+ |
  +---------+

auto_ptr<Shape> b(a);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        +----------------------+
                               |
  +---------+            +-----|---+
  |   +---+ |            |   +-|-+ |
a | p |   | |          b | p | | | |
  |   +---+ |            |   +---+ |
  +---------+            +---------+

Si noti come l'inizializzazione di bcon anon non copia il triangolo, ma invece trasferisce la proprietà del triangolo da aa b. Diciamo anche " aviene spostato in b " o "il triangolo viene spostato da a a b ". Questo può sembrare confuso perché il triangolo stesso rimane sempre nello stesso posto nella memoria.

Spostare un oggetto significa trasferire la proprietà di alcune risorse che gestisce a un altro oggetto.

Il costruttore di copie di auto_ptrprobabilmente assomiglia a questo (un po 'semplificato):

auto_ptr(auto_ptr& source)   // note the missing const
{
    p = source.p;
    source.p = 0;   // now the source no longer owns the object
}

Mosse pericolose e innocue

La cosa pericolosa auto_ptrè che ciò che sintatticamente assomiglia a una copia è in realtà una mossa. Cercare di chiamare una funzione membro su un spostato auto_ptrinvocherà un comportamento indefinito, quindi è necessario fare molta attenzione a non utilizzare un auto_ptrdopo che è stato spostato da:

auto_ptr<Shape> a(new Triangle);   // create triangle
auto_ptr<Shape> b(a);              // move a into b
double area = a->area();           // undefined behavior

Ma auto_ptrnon è sempre pericoloso. Le funzioni di fabbrica sono un caso d'uso perfetto per auto_ptr:

auto_ptr<Shape> make_triangle()
{
    return auto_ptr<Shape>(new Triangle);
}

auto_ptr<Shape> c(make_triangle());      // move temporary into c
double area = make_triangle()->area();   // perfectly safe

Nota come entrambi gli esempi seguono lo stesso modello sintattico:

auto_ptr<Shape> variable(expression);
double area = expression->area();

Eppure, uno di loro invoca un comportamento indefinito, mentre l'altro no. Quindi qual è la differenza tra le espressioni ae make_triangle()? Non sono entrambi dello stesso tipo? In effetti lo sono, ma hanno diverse categorie di valore .

Categorie di valore

Ovviamente, ci deve essere una profonda differenza tra l'espressione ache indica una auto_ptrvariabile e l'espressione make_triangle()che indica la chiamata di una funzione che restituisce un auto_ptrvalore, creando così un nuovo auto_ptroggetto temporaneo ogni volta che viene chiamato. aè un esempio di un valore , mentre make_triangle()è un esempio di un valore .

Passare da valori come aè pericoloso, perché in seguito potremmo provare a chiamare una funzione membro ainvocando un comportamento indefinito. D'altra parte, passare da valori come make_triangle()è perfettamente sicuro, perché dopo che il costruttore di copie ha fatto il suo lavoro, non possiamo usare nuovamente il temporaneo. Non c'è espressione che denoti detto temporaneo; se semplicemente scriviamo di make_triangle()nuovo, otteniamo un temporaneo diverso . In effetti, il passaggio da temporaneo è già passato alla riga successiva:

auto_ptr<Shape> c(make_triangle());
                                  ^ the moved-from temporary dies right here

Si noti che le lettere le rhanno un'origine storica nel lato lato sinistro e destro di una cessione. Questo non è più vero in C ++, perché ci sono valori che non possono apparire sul lato sinistro di un compito (come array o tipi definiti dall'utente senza un operatore di compito), e ci sono valori che possono (tutti i valori di tipi di classe con un operatore di assegnazione).

Un valore di tipo classe è un'espressione la cui valutazione crea un oggetto temporaneo. In circostanze normali, nessun'altra espressione all'interno dello stesso ambito indica lo stesso oggetto temporaneo.

Riferimenti di valore

Ora capiamo che spostarsi dai valori è potenzialmente pericoloso, ma spostarsi dai valori è innocuo. Se il C ++ avesse il supporto linguistico per distinguere gli argomenti lvalue dagli argomenti rvalue, potremmo o completamente vietare il passaggio dai lvalues ​​o almeno rendere esplicito il passaggio dai lvalues nel sito di chiamata, in modo da non spostarci più per caso.

La risposta di C ++ 11 a questo problema sono i riferimenti di valore . Un riferimento al valore è un nuovo tipo di riferimento che si lega solo ai valori e la sintassi è X&&. Il buon vecchio riferimento X&è ora noto come riferimento lvalue . (Nota che nonX&& è un riferimento a un riferimento; non esiste una cosa del genere in C ++).

Se ci buttiamo constnel mix, abbiamo già quattro diversi tipi di riferimenti. A quali tipi di espressioni Xpossono legare?

            lvalue   const lvalue   rvalue   const rvalue
---------------------------------------------------------              
X&          yes
const X&    yes      yes            yes      yes
X&&                                 yes
const X&&                           yes      yes

In pratica, puoi dimenticartene const X&&. Essere limitati a leggere dai valori non è molto utile.

Un riferimento al valore X&&è un nuovo tipo di riferimento che si lega solo ai valori.

Conversioni implicite

I riferimenti di valore sono passati attraverso diverse versioni. Dalla versione 2.1, un riferimento a rvalue X&&si lega anche a tutte le categorie di valori di un tipo diverso Y, a condizione che vi sia una conversione implicita da Ya X. In tal caso, Xviene creato un temporaneo di tipo e il riferimento al valore è associato a quel temporaneo:

void some_function(std::string&& r);

some_function("hello world");

Nell'esempio sopra, "hello world"è un valore di tipo const char[12]. Poiché esiste una conversione implicita da const char[12]attraverso const char*a std::string, std::stringviene creato un temporaneo di tipo ed rè associato a tale temporaneo. Questo è uno dei casi in cui la distinzione tra valori (espressioni) e temporali (oggetti) è un po 'sfocata.

Sposta i costruttori

Un utile esempio di una funzione con un X&&parametro è il costruttore di spostamento X::X(X&& source) . Il suo scopo è trasferire la proprietà della risorsa gestita dall'origine all'oggetto corrente.

In C ++ 11, std::auto_ptr<T>è stato sostituito dal std::unique_ptr<T>quale sfrutta i riferimenti di valore. Svilupperò e discuterò una versione semplificata di unique_ptr. Innanzitutto, incapsuliamo un puntatore non elaborato e sovraccarichiamo gli operatori ->e *, quindi la nostra classe si sente come un puntatore:

template<typename T>
class unique_ptr
{
    T* ptr;

public:

    T* operator->() const
    {
        return ptr;
    }

    T& operator*() const
    {
        return *ptr;
    }

Il costruttore assume la proprietà dell'oggetto e il distruttore lo elimina:

    explicit unique_ptr(T* p = nullptr)
    {
        ptr = p;
    }

    ~unique_ptr()
    {
        delete ptr;
    }

Ora arriva la parte interessante, il costruttore di mosse:

    unique_ptr(unique_ptr&& source)   // note the rvalue reference
    {
        ptr = source.ptr;
        source.ptr = nullptr;
    }

Questo costruttore di mosse fa esattamente quello che ha fatto il auto_ptrcostruttore di copie, ma può essere fornito solo con valori:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);                 // error
unique_ptr<Shape> c(make_triangle());   // okay

La seconda riga non può essere compilata, poiché aè un lvalue, ma il parametro unique_ptr&& sourcepuò essere associato solo a rvalues. Questo è esattamente quello che volevamo; le mosse pericolose non dovrebbero mai essere implicite. La terza riga si compila bene, perché make_triangle()è un valore. Il costruttore di mosse trasferirà la proprietà dal temporaneo a c. Ancora una volta, questo è esattamente quello che volevamo.

Il costruttore di spostamento trasferisce la proprietà di una risorsa gestita nell'oggetto corrente.

Sposta operatori di assegnazione

L'ultimo pezzo mancante è l'operatore di assegnazione delle mosse. Il suo compito è liberare la vecchia risorsa e acquisire la nuova risorsa dal suo argomento:

    unique_ptr& operator=(unique_ptr&& source)   // note the rvalue reference
    {
        if (this != &source)    // beware of self-assignment
        {
            delete ptr;         // release the old resource

            ptr = source.ptr;   // acquire the new resource
            source.ptr = nullptr;
        }
        return *this;
    }
};

Notare come questa implementazione dell'operatore di assegnazione delle mosse duplica la logica sia del distruttore che del costruttore di mosse. Conosci il linguaggio copia-e-scambia? Può anche essere applicato per spostare la semantica come idioma move-and-swap:

    unique_ptr& operator=(unique_ptr source)   // note the missing reference
    {
        std::swap(ptr, source.ptr);
        return *this;
    }
};

Ora che sourceè una variabile di tipo unique_ptr, verrà inizializzata dal costruttore di mosse; cioè, l'argomento verrà spostato nel parametro. L'argomento deve ancora essere un valore rvalore, poiché lo stesso costruttore di spostamento ha un parametro di riferimento valore rvalore. Quando il flusso di controllo raggiunge il controvento di chiusura operator=, sourceesce dall'ambito, rilasciando automaticamente la vecchia risorsa.

L'operatore di assegnazione degli spostamenti trasferisce la proprietà di una risorsa gestita nell'oggetto corrente, rilasciando la vecchia risorsa. Il linguaggio move-and-swap semplifica l'implementazione.

Passando dai valori

A volte, vogliamo passare dai valori. Cioè, a volte vogliamo che il compilatore tratti un valore come se fosse un valore, quindi può invocare il costruttore di mosse, anche se potrebbe essere potenzialmente pericoloso. A tale scopo, C ++ 11 offre un modello di funzione di libreria standard chiamato std::moveall'interno dell'intestazione <utility>. Questo nome è un po 'sfortunato, perché std::movesemplicemente lancia un valore in un valore; essa non si muove nulla da solo. Si limita consente movimento. Forse avrebbe dovuto essere chiamato std::cast_to_rvalueo std::enable_move, ma ormai siamo bloccati con il nome.

Ecco come ti muovi esplicitamente da un valore:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);              // still an error
unique_ptr<Shape> c(std::move(a));   // okay

Si noti che dopo la terza riga, anon possiede più un triangolo. Va bene, perché scrivendo esplicitamentestd::move(a) , abbiamo chiarito le nostre intenzioni: "Caro costruttore, fai tutto quello che vuoi aper inizializzare c; non mi interessa apiù. Sentiti libero di farcela a."

std::move(some_lvalue) lancia un lvalue su un rvalue, consentendo così una mossa successiva.

XValues

Si noti che anche se std::move(a)è un valore, la sua valutazione non crea un oggetto temporaneo. Questo enigma ha costretto il comitato a introdurre una terza categoria di valore. Qualcosa che può essere associato a un riferimento di valore, anche se non è un valore in senso tradizionale, è chiamato valore x (valore eXpiring). I valori tradizionali sono stati rinominati in valori (valori puri).

Sia i valori che gli x sono valori. Xvalues ​​e lvalues ​​sono entrambi valori (valori generalizzati). Le relazioni sono più facili da comprendere con un diagramma:

        expressions
          /     \
         /       \
        /         \
    glvalues   rvalues
      /  \       /  \
     /    \     /    \
    /      \   /      \
lvalues   xvalues   prvalues

Nota che solo gli xvalori sono davvero nuovi; il resto è solo dovuto alla ridenominazione e al raggruppamento.

I valori di C ++ 98 sono noti come valori in C ++ 11. Sostituisci mentalmente tutte le occorrenze di "rvalue" nei paragrafi precedenti con "prvalue".

Uscire dalle funzioni

Finora abbiamo visto il movimento verso variabili locali e parametri di funzione. Ma muoversi è anche possibile nella direzione opposta. Se una funzione restituisce per valore, alcuni oggetti nel sito di chiamata (probabilmente una variabile locale o temporanea, ma potrebbero essere qualsiasi tipo di oggetto) vengono inizializzati con l'espressione dopo l' returnistruzione come argomento per il costruttore di spostamenti:

unique_ptr<Shape> make_triangle()
{
    return unique_ptr<Shape>(new Triangle);
}          \-----------------------------/
                  |
                  | temporary is moved into c
                  |
                  v
unique_ptr<Shape> c(make_triangle());

Forse sorprendentemente, anche gli oggetti automatici (variabili locali che non sono dichiarate come static) possono essere implicitamente spostati fuori dalle funzioni:

unique_ptr<Shape> make_square()
{
    unique_ptr<Shape> result(new Square);
    return result;   // note the missing std::move
}

Come mai il costruttore di mosse accetta il valore resultcome argomento? Lo scopo di resultsta per finire e verrà distrutto durante lo svolgimento dello stack. Nessuno avrebbe potuto lamentarsi in seguito che resultera cambiato in qualche modo; quando il flusso di controllo torna al chiamante, resultnon esiste più! Per questo motivo, C ++ 11 ha una regola speciale che consente di restituire oggetti automatici dalle funzioni senza dover scrivere std::move. In effetti, non si dovrebbe mai usare std::moveper spostare oggetti automatici fuori dalle funzioni, in quanto ciò inibisce l '"ottimizzazione del valore restituito" (NRVO).

Non usare mai std::moveper spostare oggetti automatici fuori dalle funzioni.

Si noti che in entrambe le funzioni di fabbrica, il tipo restituito è un valore, non un riferimento al valore. I riferimenti a valori sono ancora riferimenti e, come sempre, non si dovrebbe mai restituire un riferimento a un oggetto automatico; il chiamante finirebbe con un riferimento penzolante se inducessi il compilatore ad accettare il tuo codice, in questo modo:

unique_ptr<Shape>&& flawed_attempt()   // DO NOT DO THIS!
{
    unique_ptr<Shape> very_bad_idea(new Square);
    return std::move(very_bad_idea);   // WRONG!
}

Non restituire mai oggetti automatici per riferimento di valore. Lo spostamento viene eseguito esclusivamente dal costruttore dello spostamento, non da std::movee non semplicemente legando un valore a un riferimento valore.

Trasferirsi nei membri

Prima o poi, scriverai codice in questo modo:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(parameter)   // error
    {}
};

Fondamentalmente, il compilatore si lamenterà che parameterè un valore. Se guardi il suo tipo, vedi un riferimento a valore, ma un riferimento a valore significa semplicemente "un riferimento che è associato a un valore"; esso non significa che il riferimento stesso è un rvalue! In effetti, parameterè solo una normale variabile con un nome. Puoi usare tutte parameterle volte che vuoi all'interno del corpo del costruttore e indica sempre lo stesso oggetto. Spostarsi implicitamente da esso sarebbe pericoloso, quindi la lingua lo proibisce.

Un riferimento al valore nominale è un valore lvalico, proprio come qualsiasi altra variabile.

La soluzione è abilitare manualmente lo spostamento:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(std::move(parameter))   // note the std::move
    {}
};

Si potrebbe sostenere che parameternon viene più utilizzato dopo l'inizializzazione di member. Perché non esiste una regola speciale da inserire silenziosamente std::moveproprio come con i valori di ritorno? Probabilmente perché sarebbe troppo onere per gli implementatori del compilatore. Ad esempio, se il corpo del costruttore fosse in un'altra unità di traduzione? Al contrario, la regola del valore restituito deve semplicemente controllare le tabelle dei simboli per determinare se l'identificatore dopo la returnparola chiave indica un oggetto automatico.

Puoi anche passare il parametervalore. Per tipi di solo spostamento come unique_ptr, sembra che non ci sia ancora un linguaggio consolidato. Personalmente, preferisco passare per valore, in quanto causa meno disordine nell'interfaccia.

Funzioni speciali per i membri

C ++ 98 dichiara implicitamente tre funzioni speciali dei membri su richiesta, ovvero quando sono necessarie da qualche parte: il costruttore della copia, l'operatore di assegnazione della copia e il distruttore.

X::X(const X&);              // copy constructor
X& X::operator=(const X&);   // copy assignment operator
X::~X();                     // destructor

I riferimenti di valore sono passati attraverso diverse versioni. Dalla versione 3.0, C ++ 11 dichiara due ulteriori funzioni membro speciali su richiesta: il costruttore di spostamenti e l'operatore di assegnazione di spostamenti. Nota che né VC10 né VC11 sono ancora conformi alla versione 3.0, quindi dovrai implementarli tu stesso.

X::X(X&&);                   // move constructor
X& X::operator=(X&&);        // move assignment operator

Queste due nuove funzioni di membri speciali vengono dichiarate implicitamente solo se nessuna delle funzioni di membri speciali viene dichiarata manualmente. Inoltre, se si dichiara il proprio costruttore di spostamento o operatore di assegnazione di spostamento, né il costruttore di copia né l'operatore di assegnazione di copia verranno dichiarati implicitamente.

Cosa significano in pratica queste regole?

Se scrivi una classe senza risorse non gestite, non è necessario dichiarare tu stesso una delle cinque funzioni speciali dei membri e otterrai la semantica della copia corretta e la muoverai gratuitamente. Altrimenti, dovrai implementare tu stesso le funzioni speciali dei membri. Naturalmente, se la tua classe non beneficia della semantica di movimento, non è necessario implementare le operazioni di spostamento speciali.

Si noti che l'operatore di assegnazione copia e l'operatore di assegnazione di spostamento possono essere fusi in un unico operatore di assegnazione unificato, prendendo il suo argomento in base al valore:

X& X::operator=(X source)    // unified assignment operator
{
    swap(source);            // see my first answer for an explanation
    return *this;
}

In questo modo, il numero di funzioni speciali dei membri da implementare scende da cinque a quattro. C'è un compromesso tra sicurezza delle eccezioni ed efficienza qui, ma non sono un esperto di questo problema.

Riferimenti di inoltro ( precedentemente noti come riferimenti universali )

Considera il seguente modello di funzione:

template<typename T>
void foo(T&&);

Potresti aspettarti T&&di legare solo ai valori, perché a prima vista sembra un riferimento al valore. A quanto pare però, T&&si lega anche ai valori:

foo(make_triangle());   // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a);                 // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&

Se l'argomento è un valore di tipo X, Tsi deduce che lo sia X, quindi T&&significa X&&. Questo è quello che chiunque si aspetterebbe. Ma se l'argomento è un valore di tipo X, a causa di una regola speciale, Tsi ritiene che sia X&, quindi T&&significherebbe qualcosa di simile X& &&. Ma dal momento che C ++ non ha ancora idea di riferimenti a riferimenti, il tipo X& &&è crollato in X&. Questo può sembrare inizialmente confuso e inutile, ma il collasso dei riferimenti è essenziale per un perfetto inoltro (che non verrà discusso qui).

T&& non è un riferimento di valore, ma un riferimento di inoltro. Si lega anche ai valori, nel qual caso Te T&&sono entrambi riferimenti ai valori.

Se vuoi vincolare un modello di funzione ai valori, puoi combinare SFINAE con tratti di tipo:

#include <type_traits>

template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);

Implementazione di mosse

Ora che hai compreso il collasso dei riferimenti, ecco come std::moveviene implementato:

template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

Come puoi vedere, moveaccetta qualsiasi tipo di parametro grazie al riferimento di inoltro T&&e restituisce un riferimento di valore. La std::remove_reference<T>::typechiamata meta-funzione è necessaria perché altrimenti, per i valori di tipo X, sarebbe il tipo restituito X& &&, che collasserebbe X&. Poiché tè sempre un lvalue (ricorda che un riferimento a un rvalue denominato è un lvalue), ma vogliamo associarlo ta un riferimento a rvalue, dobbiamo esplicitamente teseguire il cast al tipo di ritorno corretto. La chiamata di una funzione che restituisce un riferimento al valore è essa stessa un valore x. Ora sai da dove provengono gli xvalues;)

La chiamata di una funzione che restituisce un riferimento di valore, ad esempio std::move, è un valore x.

Si noti che la restituzione per riferimento al valore va bene in questo esempio, perché tnon indica un oggetto automatico, ma invece un oggetto che è stato passato dal chiamante.



24
C'è una terza ragione per cui la semantica del movimento è importante: la sicurezza delle eccezioni. Spesso quando un'operazione di copia può essere lanciata (perché deve allocare risorse e l'allocazione potrebbe non riuscire) un'operazione di spostamento può essere non-lanciante (perché può trasferire la proprietà delle risorse esistenti anziché allocarne di nuove). Avere operazioni che non possono fallire è sempre utile e può essere cruciale quando si scrive codice che fornisce garanzie di eccezione.
Brangdon,

8
Ero con te fino a "Riferimenti universali", ma poi è tutto troppo astratto per seguirlo. Il crollo del riferimento? Inoltro perfetto? Stai dicendo che un riferimento al valore diventa un riferimento universale se il tipo è modellato? Vorrei che ci fosse un modo per spiegarlo in modo da sapere se ho bisogno di capirlo o no! :)
Kylotan,

8
Per favore, scrivi un libro ora ... questa risposta mi ha dato motivo di credere che se avessi coperto altri angoli del C ++ in modo lucido come questo, migliaia di persone lo capiranno.
Halivingston,

12
@halivingston Grazie mille per il tuo gentile feedback, lo apprezzo molto. Il problema con la scrittura di un libro è: è molto più lavoro di quanto tu possa immaginare. Se vuoi approfondire il C ++ 11 e oltre, ti suggerisco di acquistare "Effective Modern C ++" di Scott Meyers.
Fredoverflow,

77

La semantica di spostamento si basa su riferimenti di valore .
Un valore è un oggetto temporaneo, che verrà distrutto alla fine dell'espressione. Nell'attuale C ++, i valori si legano solo ai constriferimenti. C ++ 1x consentirà constriferimenti senza valore, ortografati T&&, che sono riferimenti a oggetti di un valore.
Poiché un valore sta per morire alla fine di un'espressione, puoi rubare i suoi dati . Invece di copiarlo in un altro oggetto, si spostano i suoi dati in esso.

class X {
public: 
  X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
    : data_()
  {
     // since 'x' is an rvalue object, we can steal its data
     this->swap(std::move(rhs));
     // this will leave rhs with the empty data
  }
  void swap(X&& rhs);
  // ... 
};

// ...

X f();

X x = f(); // f() returns result as rvalue, so this calls move-ctor

Nel codice di cui sopra, con i vecchi compilatori il risultato di f()è copiato in xutilizzando X's costruttore di copia. Se il tuo compilatore supporta la semantica di spostamento e Xha un costruttore di mosse, viene invece chiamato. Poiché il suo rhsargomento è un valore , sappiamo che non è più necessario e possiamo rubarne il valore.
Quindi il valore viene spostato dal temporaneo senza nome restituito da f()a x(mentre i dati di x, inizializzati in un vuoto X, vengono spostati nel temporaneo, che verrà distrutto dopo l'assegnazione).


1
nota che dovrebbe essere this->swap(std::move(rhs));perché i riferimenti ai valori nominali sono valori
wmamrak

Questo è un po 'sbagliato, per il commento di @ Tacyt: rhsè un valore nel contesto di X::X(X&& rhs). È necessario chiamare std::move(rhs)per ottenere un valore, ma questo genere rende discutibile la risposta.
Asherah,

Cosa sposta la semantica per i tipi senza puntatori? Spostare la semantica funziona come copia?
Gusev Slava,

@Gusev: non ho idea di cosa tu stia chiedendo.
sabato

60

Supponiamo di avere una funzione che restituisce un oggetto sostanziale:

Matrix multiply(const Matrix &a, const Matrix &b);

Quando scrivi codice in questo modo:

Matrix r = multiply(a, b);

quindi un normale compilatore C ++ creerà un oggetto temporaneo per il risultato di multiply(), chiamerà il costruttore della copia per inizializzare re quindi distruggerà il valore di ritorno temporaneo. La semantica di spostamento in C ++ 0x consente al "costruttore di spostamento" di essere chiamato per l'inizializzazione rcopiando il suo contenuto, quindi scarta il valore temporaneo senza doverlo distruggere.

Ciò è particolarmente importante se (come forse Matrixnell'esempio sopra), l'oggetto da copiare alloca memoria aggiuntiva sull'heap per memorizzare la sua rappresentazione interna. Un costruttore di copie dovrebbe creare una copia completa della rappresentazione interna oppure utilizzare il conteggio dei riferimenti e la semantica della copia su scrittura a livello interiore. Un costruttore di mosse lascerebbe da solo la memoria dell'heap e copierebbe semplicemente il puntatore all'interno Matrixdell'oggetto.


2
In cosa differiscono i costruttori di spostamento e i costruttori di copie?
dicroce,

1
@dicroce: differiscono per sintassi, uno assomiglia a Matrix (const Matrix & src) (copia costruttore) e l'altro assomiglia a Matrix (Matrix && src) (sposta costruttore), controlla la mia risposta principale per un esempio migliore.
snk_kid,

3
@dicroce: uno crea un oggetto vuoto e uno ne fa una copia. Se i dati memorizzati nell'oggetto sono di grandi dimensioni, una copia può essere costosa. Ad esempio, std :: vector.
Billy ONeal,

1
@ kunj2aan: dipende dal tuo compilatore, sospetto. Il compilatore potrebbe creare un oggetto temporaneo all'interno della funzione, quindi spostarlo nel valore restituito del chiamante. In alternativa, potrebbe essere in grado di costruire direttamente l'oggetto nel valore restituito, senza la necessità di utilizzare un costruttore di spostamento.
Greg Hewgill,

2
@Jichao: Questo è un'ottimizzazione chiamato RVO, vedere questa domanda per ulteriori informazioni sulla differenza: stackoverflow.com/questions/5031778/...
Greg Hewgill

30

Se sei veramente interessato a una buona spiegazione approfondita della semantica di movimento, ti consiglio vivamente di leggere il documento originale su di essi, "Una proposta per aggiungere il supporto di semantica di spostamento al linguaggio C ++".

È molto accessibile e facile da leggere e costituisce un ottimo caso per i vantaggi che offrono. Ci sono altri articoli più recenti e aggiornati sulla semantica di traslochi disponibili sul sito web del WG21 , ma questo è probabilmente il più semplice poiché si avvicina alle cose da una vista di livello superiore e non approfondisce molto i dettagli linguistici grintosi.


27

Spostare la semantica significa trasferire risorse piuttosto che copiarle quando nessuno ha più bisogno del valore di origine.

In C ++ 03, gli oggetti vengono spesso copiati, solo per essere distrutti o assegnati prima che qualsiasi codice utilizzi nuovamente il valore. Ad esempio, quando si ritorna per valore da una funzione, a meno che non venga attivato RVO, il valore che si sta restituendo viene copiato nel frame dello stack del chiamante, quindi esce dall'ambito e viene distrutto. Questo è solo uno dei tanti esempi: vedi valore per passaggio quando l'oggetto sorgente è temporaneo, algoritmi come sortquello semplicemente riorganizzano gli elementi, riallocazione vectorquando capacity()viene superato, ecc.

Quando tali coppie di copia / distruzione sono costose, è in genere perché l'oggetto possiede una risorsa pesante. Ad esempio, vector<string>può possedere un blocco di memoria allocato dinamicamente contenente una matrice di stringoggetti, ognuno con la propria memoria dinamica. Copiare un oggetto del genere è costoso: è necessario allocare nuova memoria per ogni blocco allocato dinamicamente nell'origine e copiare tutti i valori. Quindi devi deallocare tutta quella memoria che hai appena copiato. Tuttavia, spostare un oggetto di grandi dimensioni vector<string>significa semplicemente copiare alcuni puntatori (che si riferiscono al blocco di memoria dinamica) sulla destinazione e azzerarli nella sorgente.


23

In termini semplici (pratici):

Copiare un oggetto significa copiare i suoi membri "statici" e chiamare l' newoperatore per i suoi oggetti dinamici. Giusto?

class A
{
   int i, *p;

public:
   A(const A& a) : i(a.i), p(new int(*a.p)) {}
   ~A() { delete p; }
};

Tuttavia, spostare un oggetto (ripeto, dal punto di vista pratico) implica solo copiare i puntatori di oggetti dinamici e non crearne di nuovi.

Ma non è pericoloso? Naturalmente, potresti distruggere un oggetto dinamico due volte (errore di segmentazione). Quindi, per evitarlo, dovresti "invalidare" i puntatori sorgente per evitare di distruggerli due volte:

class A
{
   int i, *p;

public:
   // Movement of an object inside a copy constructor.
   A(const A& a) : i(a.i), p(a.p)
   {
     a.p = nullptr; // pointer invalidated.
   }

   ~A() { delete p; }
   // Deleting NULL, 0 or nullptr (address 0x0) is safe. 
};

Ok, ma se sposto un oggetto, l'oggetto sorgente diventa inutile, no? Certo, ma in certe situazioni è molto utile. Il più evidente è quando chiamo una funzione con un oggetto anonimo (temporale, oggetto rvalue, ..., puoi chiamarlo con nomi diversi):

void heavyFunction(HeavyType());

In tale situazione, viene creato un oggetto anonimo, successivamente copiato nel parametro della funzione e successivamente eliminato. Quindi, qui è meglio spostare l'oggetto, perché non è necessario l'oggetto anonimo e puoi risparmiare tempo e memoria.

Questo porta al concetto di riferimento "rvalue". Esistono in C ++ 11 solo per rilevare se l'oggetto ricevuto è anonimo o meno. Penso che tu sappia già che un "lvalue" è un'entità assegnabile (la parte sinistra =dell'operatore), quindi hai bisogno di un riferimento denominato a un oggetto per essere in grado di agire come un lvalue. Un valore è esattamente l'opposto, un oggetto senza riferimenti nominati. Per questo motivo, oggetto anonimo e valore sono sinonimi. Così:

class A
{
   int i, *p;

public:
   // Copy
   A(const A& a) : i(a.i), p(new int(*a.p)) {}

   // Movement (&& means "rvalue reference to")
   A(A&& a) : i(a.i), p(a.p)
   {
      a.p = nullptr;
   }

   ~A() { delete p; }
};

In questo caso, quando un oggetto di tipo Adeve essere "copiato", il compilatore crea un riferimento lvalue o un riferimento rvalue in base al nome dell'oggetto passato o meno. Altrimenti, viene chiamato il costruttore di mosse e sai che l'oggetto è temporale e puoi spostare i suoi oggetti dinamici invece di copiarli, risparmiando spazio e memoria.

È importante ricordare che gli oggetti "statici" vengono sempre copiati. Non c'è modo di "spostare" un oggetto statico (oggetto in pila e non in pila). Pertanto, la distinzione "sposta" / "copia" quando un oggetto non ha membri dinamici (direttamente o indirettamente) è irrilevante.

Se il tuo oggetto è complesso e il distruttore ha altri effetti secondari, come chiamare la funzione di una libreria, chiamare altre funzioni globali o qualunque cosa sia, forse è meglio segnalare un movimento con una bandiera:

class Heavy
{
   bool b_moved;
   // staff

public:
   A(const A& a) { /* definition */ }
   A(A&& a) : // initialization list
   {
      a.b_moved = true;
   }

   ~A() { if (!b_moved) /* destruct object */ }
};

Quindi, il tuo codice è più breve (non è necessario eseguire un nullptrcompito per ciascun membro dinamico) e più generale.

Altra domanda tipica: qual è la differenza tra A&&e const A&&? Certo, nel primo caso, puoi modificare l'oggetto e nel secondo non, ma, significato pratico? Nel secondo caso, non è possibile modificarlo, quindi non è possibile invalidare l'oggetto (se non con un flag modificabile o qualcosa del genere) e non esiste alcuna differenza pratica per un costruttore di copie.

E cos'è l' inoltro perfetto ? È importante sapere che un "riferimento al valore" è un riferimento a un oggetto denominato nell'ambito del "chiamante". Ma nell'ambito reale, un riferimento al valore è un nome per un oggetto, quindi funge da oggetto denominato. Se si passa un riferimento di valore a un'altra funzione, si passa un oggetto denominato, quindi l'oggetto non viene ricevuto come un oggetto temporale.

void some_function(A&& a)
{
   other_function(a);
}

L'oggetto averrebbe copiato nel parametro effettivo di other_function. Se si desidera che l'oggetto acontinui a essere trattato come un oggetto temporaneo, è necessario utilizzare la std::movefunzione:

other_function(std::move(a));

Con questa linea, std::moveverrà aeseguito il cast su un valore e other_functionriceverà l'oggetto come oggetto senza nome. Naturalmente, se other_functionnon ha un sovraccarico specifico per lavorare con oggetti senza nome, questa distinzione non è importante.

È l'inoltro perfetto? No, ma siamo molto vicini. L'inoltro perfetto è utile solo per lavorare con i modelli, con lo scopo di dire: se devo passare un oggetto a un'altra funzione, ho bisogno che se ricevo un oggetto nominato, l'oggetto viene passato come oggetto nominato e, in caso contrario, Voglio passarlo come un oggetto senza nome:

template<typename T>
void some_function(T&& a)
{
   other_function(std::forward<T>(a));
}

Questa è la firma di una funzione prototipica che utilizza l'inoltro perfetto, implementato in C ++ 11 per mezzo di std::forward. Questa funzione sfrutta alcune regole di istanza del modello:

 `A& && == A&`
 `A&& && == A&&`

Quindi, se Tè un riferimento lvalue a A( T = A &), aanche ( A & && => A &). Se Tè un riferimento di valore a A, aanche (A && && => A &&). In entrambi i casi, aè un oggetto denominato nell'ambito reale, ma Tcontiene le informazioni del suo "tipo di riferimento" dal punto di vista dell'ambito del chiamante. Questa informazione ( T) viene passata come parametro del modello a forwarde 'a' viene spostato o meno in base al tipo di T.


20

È come copiare la semantica, ma invece di dover duplicare tutti i dati che si ottengono per rubare i dati dall'oggetto da "spostare".


13

Sai cosa significa una semantica della copia, giusto? significa che hai tipi che sono copiabili, per tipi definiti dall'utente lo definisci o acquisti esplicitamente scrivendo un costruttore di copia e un operatore di assegnazione o il compilatore li genera implicitamente. Questo farà una copia.

La semantica di spostamento è fondamentalmente un tipo definito dall'utente con costruttore che accetta un riferimento di valore r (nuovo tipo di riferimento usando && (sì due e commerciali)) che non è const, questo è chiamato costruttore di movimento, lo stesso vale per l'operatore di assegnazione. Quindi cosa fa un costruttore di mosse, beh invece di copiare la memoria dal suo argomento sorgente, "sposta" la memoria dalla sorgente alla destinazione.

Quando vorresti farlo? bene std :: vector è un esempio, supponiamo che tu abbia creato uno std :: vector temporaneo e lo ritorni da una funzione diciamo:

std::vector<foo> get_foos();

Avrai un overhead dal costruttore di copie quando la funzione ritorna, se (e lo farà in C ++ 0x) std :: vector ha un costruttore di mosse invece di copiarlo può semplicemente impostare i suoi puntatori e 'spostare' allocato dinamicamente memoria per la nuova istanza. È un po 'come la semantica del trasferimento di proprietà con std :: auto_ptr.


1
Non credo che questo sia un ottimo esempio, perché in questi esempi di valori restituiti l'ottimizzazione del valore restituito probabilmente sta già eliminando l'operazione di copia.
Zan Lynx,

7

Per illustrare la necessità di spostare la semantica , consideriamo questo esempio senza spostare la semantica:

Ecco una funzione che accetta un oggetto di tipo Te restituisce un oggetto dello stesso tipo T:

T f(T o) { return o; }
  //^^^ new object constructed

La funzione sopra utilizza call per valore, il che significa che quando questa funzione viene chiamata, un oggetto deve essere costruito per essere utilizzato dalla funzione.
Poiché la funzione restituisce anche in base al valore , viene costruito un altro nuovo oggetto per il valore restituito:

T b = f(a);
  //^ new object constructed

Sono stati costruiti due nuovi oggetti, uno dei quali è un oggetto temporaneo utilizzato solo per la durata della funzione.

Quando il nuovo oggetto viene creato dal valore restituito, viene chiamato il costruttore della copia per copiare il contenuto dell'oggetto temporaneo nel nuovo oggetto b. Al termine della funzione, l'oggetto temporaneo utilizzato nella funzione non rientra nell'ambito e viene distrutto.


Consideriamo ora cosa fa un costruttore di copie .

Deve prima inizializzare l'oggetto, quindi copiare tutti i dati rilevanti dal vecchio oggetto a quello nuovo.
A seconda della classe, forse è un contenitore con molti dati, quindi potrebbe rappresentare molto tempo e utilizzo della memoria

// Copy constructor
T::T(T &old) {
    copy_data(m_a, old.m_a);
    copy_data(m_b, old.m_b);
    copy_data(m_c, old.m_c);
}

Con la semantica di spostamento è ora possibile rendere la maggior parte di questo lavoro meno spiacevole semplicemente spostando i dati anziché copiandoli.

// Move constructor
T::T(T &&old) noexcept {
    m_a = std::move(old.m_a);
    m_b = std::move(old.m_b);
    m_c = std::move(old.m_c);
}

Lo spostamento dei dati comporta la riassociazione dei dati con il nuovo oggetto. E nessuna copia ha luogo affatto.

Questo si ottiene con un rvalueriferimento.
Un rvalueriferimento funziona più o meno come un lvalueriferimento con una differenza importante:
un riferimento al valore può essere spostato e un valore non può.

Da cppreference.com :

Per rendere possibile una forte eccezione, i costruttori di mosse definiti dall'utente non devono generare eccezioni. In effetti, i contenitori standard in genere si basano su std :: move_if_noexcept per scegliere tra move e copy quando è necessario spostare gli elementi del container. Se vengono forniti sia costruttori di copia che spostamento, la risoluzione di sovraccarico seleziona il costruttore di spostamento se l'argomento è un valore (o un valore come un temporaneo senza nome o un valore x come il risultato di std :: move) e seleziona il costruttore di copia se l'argomento è un lvalue (oggetto denominato o una funzione / operatore che restituisce il riferimento lvalue). Se viene fornito solo il costruttore della copia, tutte le categorie di argomenti lo selezionano (purché prenda un riferimento a const, poiché i valori possono legarsi a riferimenti const), il che rende la copia del fallback per lo spostamento, quando lo spostamento non è disponibile. In molte situazioni, i costruttori di movimento sono ottimizzati anche se producono effetti collaterali osservabili, vedi copia elisione. Un costruttore viene chiamato 'sposta costruttore' quando accetta un riferimento di valore come parametro. Non è obbligatorio spostare nulla, la classe non deve disporre di una risorsa da spostare e un "costruttore di spostamento" potrebbe non essere in grado di spostare una risorsa come nel caso ammissibile (ma forse non sensato) in cui il parametro è un valore di riferimento const (const T &&).


7

Sto scrivendo questo per essere sicuro di capirlo correttamente.

La semantica Move è stata creata per evitare la copia non necessaria di oggetti di grandi dimensioni. Bjarne Stroustrup nel suo libro "Il linguaggio di programmazione C ++" utilizza due esempi in cui la copia non necessaria si verifica per impostazione predefinita: uno, lo scambio di due oggetti di grandi dimensioni e due, il ritorno di un oggetto di grandi dimensioni da un metodo.

Lo scambio di due oggetti di grandi dimensioni in genere comporta la copia del primo oggetto in un oggetto temporaneo, la copia del secondo oggetto nel primo oggetto e la copia dell'oggetto temporaneo nel secondo oggetto. Per un tipo incorporato, questo è molto veloce, ma per oggetti di grandi dimensioni queste tre copie potrebbero richiedere molto tempo. Una "assegnazione di spostamento" consente al programmatore di sovrascrivere il comportamento di copia predefinito e di scambiare i riferimenti agli oggetti, il che significa che non è possibile eseguire alcuna copia e l'operazione di scambio è molto più veloce. L'assegnazione di spostamento può essere invocata chiamando il metodo std :: move ().

La restituzione di un oggetto da un metodo per impostazione predefinita comporta la creazione di una copia dell'oggetto locale e dei dati associati in una posizione accessibile al chiamante (poiché l'oggetto locale non è accessibile al chiamante e scompare al termine del metodo). Quando viene restituito un tipo incorporato, questa operazione è molto veloce, ma se viene restituito un oggetto di grandi dimensioni, ciò potrebbe richiedere molto tempo. La funzione di costruzione dello spostamento consente al programmatore di ignorare questo comportamento predefinito e invece "riutilizza" i dati dell'heap associati all'oggetto locale puntando l'oggetto che viene restituito al chiamante per heap i dati associati all'oggetto locale. Pertanto non è richiesta alcuna copia.

Nelle lingue che non consentono la creazione di oggetti locali (ovvero oggetti nello stack) questi tipi di problemi non si verificano poiché tutti gli oggetti sono allocati nell'heap e sono sempre accessibili per riferimento.


"Un" spostamento di assegnazione "consente al programmatore di sovrascrivere il comportamento di copia predefinito e di scambiare i riferimenti agli oggetti, il che significa che non c'è alcuna copia e l'operazione di scambio è molto più veloce." - queste affermazioni sono ambigue e fuorvianti. Per scambiare due oggetti xe ynon puoi semplicemente "scambiare riferimenti agli oggetti" ; è possibile che gli oggetti contengano puntatori che fanno riferimento ad altri dati e che tali puntatori possano essere scambiati, ma gli operatori di spostamento non sono tenuti a scambiare nulla. Potrebbero cancellare i dati dall'oggetto spostato, piuttosto che preservare i dati dest ivi contenuti.
Tony Delroy,

Puoi scrivere swap()senza spostare la semantica. "L'assegnazione di spostamento può essere invocata chiamando il metodo std :: move ()." - a volte è necessario usare std::move()- anche se in realtà non sposta nulla - fa solo sapere al compilatore che l'argomento è mobile, a volte std::forward<>()(con riferimenti di inoltro), e altre volte che il compilatore sa che un valore può essere spostato.
Tony Delroy,

-2

Ecco una risposta dal libro "Il linguaggio di programmazione C ++" di Bjarne Stroustrup. Se non vuoi vedere il video, puoi vedere il testo qui sotto:

Considera questo frammento. Il ritorno da un operatore + comporta la copia del risultato dalla variabile locale rese in un punto in cui il chiamante può accedervi.

Vector operator+(const Vector& a, const Vector& b)
{
    if (a.size()!=b.size())
        throw Vector_siz e_mismatch{};
    Vector res(a.size());
        for (int i=0; i!=a.size(); ++i)
            res[i]=a[i]+b[i];
    return res;
}

Non volevamo davvero una copia; volevamo solo ottenere il risultato da una funzione. Quindi dobbiamo spostare un vettore anziché copiarlo. Possiamo definire il costruttore di mosse come segue:

class Vector {
    // ...
    Vector(const Vector& a); // copy constructor
    Vector& operator=(const Vector& a); // copy assignment
    Vector(Vector&& a); // move constructor
    Vector& operator=(Vector&& a); // move assignment
};

Vector::Vector(Vector&& a)
    :elem{a.elem}, // "grab the elements" from a
    sz{a.sz}
{
    a.elem = nullptr; // now a has no elements
    a.sz = 0;
}

&& significa "riferimento al valore" ed è un riferimento al quale possiamo associare un valore. "rvalue" "è inteso come complemento di" lvalue "che significa approssimativamente" qualcosa che può apparire sul lato sinistro di un compito ". Quindi un valore significa approssimativamente "un valore che non è possibile assegnare a", come un numero intero restituito da una chiamata di funzione e la resvariabile locale in operatore + () per i vettori.

Ora, la dichiarazione return res;non verrà copiata!

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.