Che cos'è std :: move () e quando dovrebbe essere usato?


656
  1. Che cos'è?
  2. Che cosa fa?
  3. Quando dovrebbe essere usato?

I buoni collegamenti sono apprezzati.


43
Bjarne Stroustrup spiega la mossa in una breve introduzione ai riferimenti ai valori
DumbCoder


12
Questa domanda si riferisce a std::move(T && t); esiste anche std::move(InputIt first, InputIt last, OutputIt d_first)un algoritmo a cui è correlato std::copy. Lo sottolineo in modo che gli altri non siano così confusi quanto lo sono stato quando ho dovuto affrontare std::movetre discussioni. en.cppreference.com/w/cpp/algorithm/move
josaphatv

Risposte:


287

Pagina di Wikipedia su riferimenti di valore R in C ++ 11 e costruttori di spostamento

  1. In C ++ 11, oltre ai costruttori di copie, gli oggetti possono avere costruttori di spostamento.
    (E oltre a copiare gli operatori di assegnazione, hanno operatori di assegnazione di spostamenti.)
  2. Il costruttore di spostamento viene utilizzato al posto del costruttore di copia, se l'oggetto ha il tipo "rvalue-reference" ( Type &&).
  3. std::move() è un cast che produce un riferimento al valore a un oggetto, per consentire lo spostamento da esso.

È un nuovo modo C ++ per evitare copie. Ad esempio, usando un costruttore di spostamento, a std::vectorpotrebbe semplicemente copiare il suo puntatore interno ai dati nel nuovo oggetto, lasciando l'oggetto spostato in uno stato spostato dallo stato, quindi non copiare tutti i dati. Questo sarebbe C ++ - valido.

Prova a cercare su Google la semantica dei movimenti, il valore, l'inoltro perfetto.


40
La semantica di spostamento richiede che l'oggetto spostato rimanga valido , che non è uno stato errato. (Razionale: deve ancora distruggere, farlo funzionare.)
GManNickG

13
@GMan: beh, deve essere in uno stato sicuro da distruggere, ma, AFAIK, non deve essere utilizzabile per nient'altro.
Zan Lynx,

8
@ZanLynx: giusto. Si noti che la libreria standard richiede inoltre che gli oggetti spostati siano assegnabili, ma questo è solo per gli oggetti utilizzati nello stdlib, non un requisito generale.
GManNickG

25
-1 "std :: move () è il modo C ++ 11 per usare la semantica di movimento" Correggi. std::move()non è il modo di utilizzare la semantica di spostamento, la semantica di spostamento viene eseguita in modo trasparente per il programmatore. moveè solo un cast per passare un valore da un punto all'altro in cui il valore originale non verrà più utilizzato.
Manu343726,

15
Andrei oltre. std::movedi per sé non fa "niente" - non ha effetti collaterali. Segnala solo al compilatore che al programmatore non importa più cosa succede a quell'oggetto. cioè dà il permesso ad altre parti del software di spostarsi dall'oggetto, ma non richiede che venga spostato. In effetti, il destinatario di un riferimento a valori non deve fare promesse su ciò che farà o non farà con i dati.
Aaron McDaid,

241

1. "Che cos'è?"

Anche std::move() se tecnicamente è una funzione, direi che non è davvero una funzione . È una specie di convertitore tra i modi in cui il compilatore considera il valore di un'espressione.

2. "Che cosa fa?"

La prima cosa da notare è che in std::move() realtà non sposta nulla . Converte un'espressione da essere un valore (come una variabile denominata) in un valore x . Un xvalue dice al compilatore:

Puoi saccheggiarmi, spostare tutto ciò che tengo in mano e usarlo altrove (dato che comunque sarò distrutto presto) ".

in altre parole, quando usi std::move(x), stai permettendo al compilatore di cannibalizzare x. Quindi, se xha, diciamo, il suo buffer in memoria - dopo std::move()ing il compilatore può avere un altro oggetto al suo posto.

Puoi anche spostarti da un valore (come un temporaneo che stai passando), ma questo è raramente utile.

3. "Quando dovrebbe essere usato?"

Un altro modo di porre questa domanda è "Per cosa dovrei cannibalizzare le risorse di un oggetto esistente?" bene, se stai scrivendo il codice dell'applicazione, probabilmente non ti sbaglierebbe molto con gli oggetti temporanei creati dal compilatore. Quindi principalmente lo faresti in luoghi come costruttori, metodi di operatore, funzioni simili a algoritmi di libreria standard ecc. In cui gli oggetti vengono creati e distrutti molto automagicamente. Certo, questa è solo una regola empirica.

Un uso tipico è lo "spostamento" delle risorse da un oggetto a un altro anziché la copia. @Guillaume collega a questa pagina che ha un breve esempio semplice: scambiare due oggetti con meno copie.

template <class T>
swap(T& a, T& b) {
    T tmp(a);   // we now have two copies of a
    a = b;      // we now have two copies of b (+ discarded a copy of a)
    b = tmp;    // we now have two copies of tmp (+ discarded a copy of b)
}

l'utilizzo di move ti consente di scambiare le risorse invece di copiarle:

template <class T>
swap(T& a, T& b) {
    T tmp(std::move(a));
    a = std::move(b);   
    b = std::move(tmp);
}

Pensa a cosa succede quando T, diciamo, vector<int>della taglia n. Nella prima versione leggi e scrivi elementi 3 * n, nella seconda versione fondamentalmente leggi e scrivi solo i 3 puntatori ai buffer dei vettori, più le dimensioni dei 3 buffer. Naturalmente, la classe Tdeve sapere come muoversi; la tua classe dovrebbe avere un operatore di assegnazione di mosse e un costruttore di mosse affinché la classe Tfunzioni.


3
Per molto tempo ho sentito parlare di queste semantiche di mosse, non le ho mai esaminate. Da questa descrizione che hai dato sembra proprio che sia una copia superficiale invece di una copia profonda.
Zebrafish,

7
@TitoneMaurice: tranne che non è una copia, poiché il valore originale non è più utilizzabile.
einpoklum,

3
@Zebrafish non potresti sbagliare di più. Una copia superficiale lascia l'originale nello stesso identico stato, una mossa di solito comporta che l'originale sia vuoto o in uno stato altrimenti valido.
rubenvb,

17
@rubenvb Zebra non è del tutto errata. Mentre è vero che l'oggetto cannabilizzato originale viene di solito deliberatamente sabotato per evitare errori confusi (ad esempio, impostare i suoi puntatori su nullptr per segnalare che non possiede più le punte), il fatto che l'intera mossa viene implementata semplicemente copiando un puntatore dalla sorgente alla destinazione (ed evitare deliberatamente di fare qualsiasi cosa con la punta) ricorda davvero una copia superficiale. In effetti, arriverei al punto di dire che una mossa è una copia superficiale, seguita facoltativamente da un autodistruzione parziale della fonte. (cont.)
Razze di leggerezza in orbita

3
(cont.) Se permettiamo questa definizione (e mi piace piuttosto), l'osservazione di @ Zebrafish non è sbagliata, solo leggermente incompleta.
Corse di leggerezza in orbita dal

146

Puoi usare move quando devi "trasferire" il contenuto di un oggetto da qualche altra parte, senza fare una copia (cioè il contenuto non è duplicato, ecco perché potrebbe essere usato su alcuni oggetti non copiabili, come un unique_ptr). È anche possibile che un oggetto prenda il contenuto di un oggetto temporaneo senza fare una copia (e risparmiare molto tempo), con std :: move.

Questo link mi ha davvero aiutato:

http://thbecker.net/articles/rvalue_references/section_01.html

Mi dispiace se la mia risposta arriva troppo tardi, ma stavo anche cercando un buon link per lo std :: move, e ho trovato i link sopra un po '"austeri".

Questo pone l'accento sul riferimento al valore r, in quale contesto dovresti usarli, e penso che sia più dettagliato, ecco perché volevo condividere questo link qui.


26
Bel link. Ho sempre trovato l'articolo di Wikipedia e altri collegamenti che mi sono imbattuto in un po 'di confusione dal momento che ti lanciano semplicemente dei fatti, lasciandoti a capire quale sia il vero significato / motivazione. Mentre "spostare la semantica" in un costruttore è piuttosto ovvio, tutti quei dettagli sul passaggio di && - i valori in giro non lo sono ... quindi la descrizione in stile tutorial è stata molto bella.
Christian Stieber,

66

D: Cos'è std::move?

A: std::move()è una funzione della libreria standard C ++ per il cast in un riferimento di valore.

Semplicemente std::move(t)equivale a:

static_cast<T&&>(t);

Un valore è un valore temporaneo che non persiste oltre l'espressione che lo definisce, come un risultato di funzione intermedia che non viene mai memorizzato in una variabile.

int a = 3; // 3 is a rvalue, does not exist after expression is evaluated
int b = a; // a is a lvalue, keeps existing after expression is evaluated

Un'implementazione per std :: move () è data in N2027: "Una breve introduzione ai riferimenti di valore" come segue:

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

Come puoi vedere, std::moverestituisce T&&non importa se chiamato con un valore ( T), un tipo di riferimento ( T&) o un riferimento rvalue ( T&&).

Q: che cosa fa?

A: Come cast, non fa nulla durante il runtime. È rilevante solo al momento della compilazione per dire al compilatore che si desidera continuare a considerare il riferimento come un valore.

foo(3 * 5); // obviously, you are calling foo with a temporary (rvalue)

int a = 3 * 5;
foo(a);     // how to tell the compiler to treat `a` as an rvalue?
foo(std::move(a)); // will call `foo(int&& a)` rather than `foo(int a)` or `foo(int& a)`

Cosa non fa:

  • Fai una copia dell'argomento
  • Chiama il costruttore di copie
  • Cambia l'oggetto argomento

Q: Quando dovrebbe essere usato?

A: Dovresti usare std::movese vuoi chiamare funzioni che supportano la semantica di movimento con un argomento che non è un valore (espressione temporanea).

Questo mi pone le seguenti domande di follow-up:

  • Cosa sono le semantiche di movimento? Spostare la semantica in contrasto con la copia della semantica è una tecnica di programmazione in cui i membri di un oggetto vengono inizializzati "prendendo il posto" invece di copiare i membri di un altro oggetto. Tale "presa in consegna" ha senso solo con i puntatori e gli handle delle risorse, che possono essere trasferiti a buon mercato copiando il puntatore o l'handle intero anziché i dati sottostanti.

  • Che tipo di classi e oggetti supportano la semantica di movimento? Spetta a te come sviluppatore implementare la semantica di movimento nelle tue classi se questi trarrebbero beneficio dal trasferimento dei loro membri invece di copiarli. Una volta implementata la semantica di spostamento, beneficerai direttamente del lavoro di molti programmatori di biblioteche che hanno aggiunto il supporto per la gestione efficiente delle classi con la semantica di spostamento.

  • Perché il compilatore non riesce a capirlo da solo? Il compilatore non può semplicemente chiamare un altro sovraccarico di una funzione se non lo dici tu. È necessario aiutare il compilatore a scegliere se chiamare la versione normale o sposta della funzione.

  • In quali situazioni vorrei dire al compilatore che dovrebbe trattare una variabile come un valore? Ciò accadrà molto probabilmente nelle funzioni di template o libreria, dove sai che un risultato intermedio potrebbe essere salvato.


2
Grande +1 per esempi di codice con semantica nei commenti. Le altre risposte migliori definiscono std :: move usando "move" stesso - non chiarisce davvero nulla! --- Credo che valga la pena ricordare che non fare una copia dell'argomento significa che il valore originale non può essere utilizzato in modo affidabile.
Ty

34

std :: move in sé non fa molto. Ho pensato che chiamasse il costruttore spostato per un oggetto, ma in realtà esegue semplicemente un cast di tipo (lanciando una variabile lvalue su un valore in modo che la suddetta variabile possa essere passata come argomento a un costruttore di spostamento o operatore di assegnazione).

Quindi std :: move è appena usato come precursore nell'uso della semantica di movimento. Spostare la semantica è essenzialmente un modo efficiente per gestire oggetti temporanei.

Prendi in considerazione l'oggetto A = B + C + D + E + F;

Questo è un bel codice, ma E + F produce un oggetto temporaneo. Quindi D + temp produce un altro oggetto temporaneo e così via. In ogni normale operatore "+" di una classe, si verificano copie profonde.

Per esempio

Object Object::operator+ (const Object& rhs) {
    Object temp (*this);
    // logic for adding
    return temp;
}

La creazione dell'oggetto temporaneo in questa funzione è inutile: questi oggetti temporanei verranno comunque eliminati alla fine della riga quando escono dall'ambito.

Possiamo piuttosto usare la semantica di movimento per "saccheggiare" gli oggetti temporanei e fare qualcosa di simile

 Object& Object::operator+ (Object&& rhs) {
     // logic to modify rhs directly
     return rhs;
 }

Questo evita che vengano fatte copie profonde inutili. Con riferimento all'esempio, l'unica parte in cui si verifica la copia in profondità è ora E + F. Il resto utilizza la semantica di spostamento. Anche il costruttore di movimenti o l'operatore di assegnazione deve essere implementato per assegnare il risultato ad A.


3
hai parlato di mossa semantica. dovresti aggiungere alla tua risposta come può essere usato std :: move perché la domanda fa questo.
Koushik Shetty,

2
@Koushik std :: move non fa molto, ma viene utilizzato per implementare la semantica di movimento. Se non sai di std :: move, probabilmente non conosci neanche la semantica di spostamento
user929404

1
"non fa molto" (sì, solo un static_cast a un riferimento di valore). ciò che effettivamente fa e ciò che fa è ciò che l'OP ha chiesto. non devi sapere come funziona std :: move ma devi sapere cosa fa la semantica di move. inoltre, "ma viene utilizzato per implementare la semantica di movimento" è il contrario. sai spostare la semantica e capirai std :: move altrimenti no. move aiuta solo nel movimento e usa la semantica di movimento. std :: move non fa altro che convertire il suo argomento in riferimento al valore, che è ciò che richiede la semantica di movimento.
Koushik Shetty,

10
"ma E + F produce un oggetto temporaneo" - L'operatore +va da sinistra a destra, non da destra a sinistra. Quindi B+Csarebbe il primo!
Ajay,

8

"Che cos'è?" e "che cosa fa?" è stato spiegato sopra.

Darò un esempio di "quando dovrebbe essere usato".

Ad esempio, abbiamo una classe con molte risorse come un grande array.

class ResHeavy{ //  ResHeavy means heavy resource
    public:
        ResHeavy(int len=10):_upInt(new int[len]),_len(len){
            cout<<"default ctor"<<endl;
        }

        ResHeavy(const ResHeavy& rhs):_upInt(new int[rhs._len]),_len(rhs._len){
            cout<<"copy ctor"<<endl;
        }

        ResHeavy& operator=(const ResHeavy& rhs){
            _upInt.reset(new int[rhs._len]);
            _len = rhs._len;
            cout<<"operator= ctor"<<endl;
        }

        ResHeavy(ResHeavy&& rhs){
            _upInt = std::move(rhs._upInt);
            _len = rhs._len;
            rhs._len = 0;
            cout<<"move ctor"<<endl;
        }

    // check array valid
    bool is_up_valid(){
        return _upInt != nullptr;
    }

    private:
        std::unique_ptr<int[]> _upInt; // heavy array resource
        int _len; // length of int array
};

Codice di prova:

void test_std_move2(){
    ResHeavy rh; // only one int[]
    // operator rh

    // after some operator of rh, it becomes no-use
    // transform it to other object
    ResHeavy rh2 = std::move(rh); // rh becomes invalid

    // show rh, rh2 it valid
    if(rh.is_up_valid())
        cout<<"rh valid"<<endl;
    else
        cout<<"rh invalid"<<endl;

    if(rh2.is_up_valid())
        cout<<"rh2 valid"<<endl;
    else
        cout<<"rh2 invalid"<<endl;

    // new ResHeavy object, created by copy ctor
    ResHeavy rh3(rh2);  // two copy of int[]

    if(rh3.is_up_valid())
        cout<<"rh3 valid"<<endl;
    else
        cout<<"rh3 invalid"<<endl;
}

uscita come di seguito:

default ctor
move ctor
rh invalid
rh2 valid
copy ctor
rh3 valid

Possiamo vedere che std::movecon move constructorrende facilmente trasformare le risorse.

Dove altro è std::moveutile?

std::movepuò anche essere utile durante l'ordinamento di una matrice di elementi. Molti algoritmi di ordinamento (come ordinamento per selezione e ordinamento a bolle) funzionano scambiando coppie di elementi. In precedenza, abbiamo dovuto ricorrere alla copia-semantica per eseguire lo scambio. Ora possiamo usare la semantica di movimento, che è più efficiente.

Può anche essere utile se vogliamo spostare i contenuti gestiti da un puntatore intelligente a un altro.

Citato:


0

Ecco un esempio completo, usando std :: move per un vettore (semplice) personalizzato

Uscita prevista:

 c: [10][11]
 copy ctor called
 copy of c: [10][11]
 move ctor called
 moved c: [10][11]

Compila come:

  g++ -std=c++2a -O2 -Wall -pedantic foo.cpp

Codice:

#include <iostream>
#include <algorithm>

template<class T> class MyVector {
private:
    T *data;
    size_t maxlen;
    size_t currlen;
public:
    MyVector<T> () : data (nullptr), maxlen(0), currlen(0) { }
    MyVector<T> (int maxlen) : data (new T [maxlen]), maxlen(maxlen), currlen(0) { }

    MyVector<T> (const MyVector& o) {
        std::cout << "copy ctor called" << std::endl;
        data = new T [o.maxlen];
        maxlen = o.maxlen;
        currlen = o.currlen;
        std::copy(o.data, o.data + o.maxlen, data);
    }

    MyVector<T> (const MyVector<T>&& o) {
        std::cout << "move ctor called" << std::endl;
        data = o.data;
        maxlen = o.maxlen;
        currlen = o.currlen;
    }

    void push_back (const T& i) {
        if (currlen >= maxlen) {
            maxlen *= 2;
            auto newdata = new T [maxlen];
            std::copy(data, data + currlen, newdata);
            if (data) {
                delete[] data;
            }
            data = newdata;
        }
        data[currlen++] = i;
    }

    friend std::ostream& operator<<(std::ostream &os, const MyVector<T>& o) {
        auto s = o.data;
        auto e = o.data + o.currlen;;
        while (s < e) {
            os << "[" << *s << "]";
            s++;
        }
        return os;
    }
};

int main() {
    auto c = new MyVector<int>(1);
    c->push_back(10);
    c->push_back(11);
    std::cout << "c: " << *c << std::endl;
    auto d = *c;
    std::cout << "copy of c: " << d << std::endl;
    auto e = std::move(*c);
    delete c;
    std::cout << "moved c: " << e << std::endl;
}
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.