Motivazione e uso dei costruttori di mosse in C ++

17

Recentemente ho letto dei costruttori di movimenti in C ++ (vedi ad esempio qui ) e io sono cercando di capire come funzionano e quando Dovrei usarli.

Per quanto ho capito, un costruttore di mosse viene utilizzato per alleviare i problemi di prestazioni causati dalla copia di oggetti di grandi dimensioni. La pagina di wikipedia dice: "Un problema di prestazioni croniche con C ++ 03 sono le copie profonde costose e non necessarie che possono verificarsi implicitamente quando gli oggetti vengono passati per valore."

Solitamente mi occupo di tali situazioni

  • passando gli oggetti per riferimento o
  • utilizzando puntatori intelligenti (ad es. boost :: shared_ptr) per passare attorno all'oggetto (i puntatori intelligenti vengono copiati anziché l'oggetto).

Quali sono le situazioni in cui le due tecniche di cui sopra non sono sufficienti e usare un costruttore di mosse è più conveniente?

    
posta Giorgio 31.05.2012 - 20:29
fonte

4 risposte

16

Spostare la semantica introduce un'intera dimensione in C ++ - non è solo lì per farti restituire i valori a basso costo.

Ad esempio, senza move-semantics std::unique_ptr non funziona - guarda std::auto_ptr , che è stato deprecato con l'introduzione di move-semantics e rimosso in C ++ 17. Lo spostamento di una risorsa è molto diverso dal copiarlo. Permette trasferimento di proprietà di un oggetto unico.

Ad esempio, non guardiamo std::unique_ptr , poiché è abbastanza ben discusso. Diamo un'occhiata, ad esempio, a un oggetto Vertex Buffer in OpenGL. Un buffer di vertici rappresenta la memoria sulla GPU - deve essere allocato e deallocato utilizzando funzioni speciali, possibilmente avendo vincoli stretti su quanto a lungo può vivere. È anche importante che sia utilizzato da un solo proprietario.

class vertex_buffer_object
{
    vertex_buffer_object(size_t size)
    {
        this->vbo_handle = create_buffer(..., size);
    }

    ~vertex_buffer_object()
    {
        release_buffer(vbo_handle);
    }
};

void create_and_use()
{
    vertex_buffer_object vbo = vertex_buffer_object(SIZE);

    do_init(vbo); //send reference, do not transfer ownership

    renderer.add(std::move(vbo)); //transfer ownership to renderer
}

Ora, questo potrebbe essere fatto con un std::shared_ptr - ma questa risorsa non deve essere condivisa. Questo rende confuso l'uso di un puntatore condiviso. Potresti usare std::unique_ptr , ma ciò richiede ancora la semantica del movimento.

Ovviamente, non ho implementato un costruttore di mosse, ma mi viene l'idea.

La cosa rilevante qui è che alcune risorse non sono copiabili . Puoi passare i puntatori anziché spostarti, ma a meno che tu non usi unique_ptr, c'è il problema della proprietà. Vale la pena di essere il più chiaro possibile su quale sia l'intento del codice, quindi un costruttore di mosse è probabilmente l'approccio migliore.

    
risposta data 31.05.2012 - 20:54
fonte
5

Spostare la semantica non è necessariamente un grande miglioramento quando si restituisce un valore - e quando / se si usa un shared_ptr (o qualcosa di simile) probabilmente si sta pessimizzando prematuramente. In realtà, quasi tutti i compilatori ragionevolmente moderni fanno ciò che si chiama Return Value Optimization (RVO) e Named Return Value Optimization (NRVO). Ciò significa che quando si restituisce un valore, invece di copiare effettivamente il valore a tutti , si passa semplicemente un puntatore / riferimento nascosto a cui verrà assegnato il valore dopo il ritorno e la funzione lo usa per creare il valore in cui finirà. Lo standard C ++ include disposizioni speciali per consentire ciò, quindi anche se (ad esempio) il costruttore di copia ha effetti collaterali visibili, non è necessario utilizzare il costruttore di copie per restituire il valore. Ad esempio:

#include <vector>
#include <numeric>
#include <iostream>
#include <stdlib.h>
#include <algorithm>
#include <iterator>

class X {
    std::vector<int> a;
public:
    X() {
        std::generate_n(std::back_inserter(a), 32767, ::rand);
    }

    X(X const &x) {
        a = x.a;
        std::cout << "Copy ctor invoked\n";
    }

    int sum() { return std::accumulate(a.begin(), a.end(), 0); }
};

X func() {
    return X();
}

int main() {
    X x = func();

    std::cout << "sum = " << x.sum();
    return 0;
};

L'idea di base qui è abbastanza semplice: creare una classe con un contenuto sufficiente preferiremmo evitare di copiarla, se possibile (il std::vector viene riempito con 32767 valori casuali). Abbiamo un copyctor esplicito che ci mostrerà quando / se verrà copiato. Abbiamo anche un po 'più di codice per fare qualcosa con i valori casuali nell'oggetto, quindi l'ottimizzatore non eliminerà (almeno facilmente) tutto ciò che riguarda la classe solo perché non fa nulla.

Abbiamo quindi del codice per restituire uno di questi oggetti da una funzione e quindi utilizzare la somma per garantire che l'oggetto sia realmente creato, non solo ignorato completamente. Quando lo eseguiamo, almeno con i compilatori più recenti / moderni, scopriamo che il programma di copia che abbiamo scritto non viene mai eseguito affatto, e sì, sono abbastanza sicuro che anche una copia veloce con shared_ptr sia ancora più lenta di non fare nessuna copia.

Il movimento ti consente di fare un buon numero di cose che semplicemente non potresti fare (direttamente) senza di loro. Considera la parte "unione" di un ordinamento di unione esterno: hai 8 file che unirai insieme. Idealmente ti piacerebbe inserire tutti gli 8 di questi file in vector - ma poiché vector (come in C ++ 03) deve essere in grado di copiare gli elementi, e ifstream s non può essere copiato , sei bloccato con un po 'di unique_ptr / shared_ptr , o qualcosa su quell'ordine per essere in grado di metterli in un vettore. Nota che anche se (per esempio) abbiamo reserve spazio in vector , siamo sicuri che il nostro ifstream s non verrà mai copiato, il compilatore non lo saprà, quindi il codice non verrà compilato nemmeno anche se noi sappiamo che il costruttore di copie non sarà mai usato comunque.

Anche se non può ancora essere copiato, in C ++ 11 un ifstream può essere spostato. In questo caso, gli oggetti probabilmente non saranno mai spostati, ma il fatto che potrebbero essere se necessario mantiene il compilatore felice, quindi possiamo mettere i nostri ifstream oggetti in vector direttamente , senza alcun hacker puntatore intelligente.

Un vettore che espande è un esempio abbastanza decente di un tempo in cui la semantica può essere davvero utile / utile. In questo caso, RVO / NRVO non aiuterà, perché non abbiamo a che fare con il valore restituito da una funzione (o qualcosa di molto simile). Abbiamo un vettore che contiene alcuni oggetti e vogliamo spostare quegli oggetti in un nuovo blocco più grande di memoria.

In C ++ 03, ciò è stato fatto creando copie degli oggetti nella nuova memoria, quindi distruggendo i vecchi oggetti nella vecchia memoria. Fare tutte quelle copie solo per buttare via quelle vecchie, tuttavia, è stata una perdita di tempo. In C ++ 11, puoi aspettarti che vengano spostati. Questo di solito ci consente, in sostanza, di fare una copia superficiale invece di una copia profonda (generalmente molto più lenta). In altre parole, con una stringa o un vettore (solo per un paio di esempi) basta copiare il puntatore (o gli oggetti) negli oggetti, invece di fare copie di tutti i dati a cui si riferiscono i puntatori.

    
risposta data 31.05.2012 - 23:38
fonte
4

Si consideri:

vector<string> v;

Quando si aggiungono stringhe a v, si espanderà secondo necessità e in ogni riallocazione le stringhe dovranno essere copiate. Con i costruttori di movimento, questo è fondamentalmente un non-problema.

Naturalmente, potresti anche fare qualcosa di simile:

vector<unique_ptr<string>> v;

Ma funzionerà bene solo perché std::unique_ptr implementa move constructor.

L'utilizzo di std::shared_ptr ha senso solo in situazioni (rare) in cui hai effettivamente una proprietà condivisa.

    
risposta data 31.05.2012 - 20:50
fonte
2

I valori di ritorno sono quelli in cui mi piacerebbe più spesso passare per valore anziché un qualche tipo di riferimento. Essere in grado di restituire rapidamente un oggetto "in pila" senza una massiccia penalizzazione delle prestazioni sarebbe bello. D'altra parte, non è particolarmente difficile aggirare questo problema (i puntatori condivisi sono così facili da usare ...), quindi non sono sicuro che valga davvero la pena di fare un lavoro extra sui miei oggetti solo per essere in grado di farlo.

    
risposta data 31.05.2012 - 20:36
fonte

Leggi altre domande sui tag