Pagine    Articoli    Prodotti    Forum    Cerca  
Nickname

Password


Non sei registrato?
Registrati a GPI qui!

Puoi anche attivare un vecchio utente GPI e chiedere una nuova password.
I Team

Mappa Team
I nostri utenti

Mappa Utenti
  C++11: Move Semantics
Pubblicato da Dario Oliveri il 2013-08-20 15:12:44

Per rinfrescare un po la memoria, tra un overloading e l'altro...

 

Una feature senza dubbio molto utilizzata ma anche criticata del C++ è la possibilità di fare "operator overloading". Ovvero sostituire la funzionalità dei classici simboli (+, -, *, /, +=, ++, ->, etc...) con qualcos'altro. Un tipico utilizzo è quello nelle librerie matematiche.

 

L'esempio più usato è quello della somma di due vettori:

 

struct Vector3{
    float X;
    float Y;
    float Z;
    
    //operatori overloaded
    friend Vector3 operator + (const Vector3 &a, const Vector3 &b);
    Vector3& operator = (const Vector3 &other);

    // ... altri dettagli omessi
};

int main(){
    Vector3 a(1,2,3);
    Vector3 b(2,1,0);

    Vector3 c = a+b;
    c.print(); // stamperà 3,3,3
    return 0;
}

 

Anche la stampa di stringhe con "<<" funziona grazie all'overloading degli operatori. Di fatto se noi vogliamo stampare un oggetto a console usando "cout" ci basta fare un overload in più per quell'oggetto dell'operatore "<<":

 

// ad esempio per stampare il vettore di prima ci basta
std::ostream& operator < < (std::ostream &out, const Vector3& vec){
    std::cout < < "X= " < < vec.X < < ", Y= " < < vec.Y < < ", Z= " < < vec.Z;
}

int main(){
    Vector3 a(1,2,3);
    std::cout < < a < < std::endl; //stampa "X= 1, Y= 2, Z= 3"
    return 0;
}

 

 

 

Questo risulta molto utile per il debug o anche per stampare su file o su console eventi importanti, warnings o errori.

 

Se io volessi una classe in grado di "sommare" due immagini, dovrei crearla così (ho cercato di rendere il codice il più semplice possibile):

 

Immagine:

class Image{

    unsigned char * buffer = nullptr; 
    std::size_t     size = 0; 

    //utility, copia i dati membro a membro
    void copyFrom( const Image & o){
        for( std::size_t i = 0; i < size; i++)
            buffer[i] = o.buffer[i];
        std::cout < < "Copia eseguita" < < std::endl;
    }

    //rialloca il buffer dell'immagine
    void realloc( std::size_t s){
        if(buffer)
            delete buffer;

        if(s > 0){
            buffer = new unsigned char[s];
            std::cout < < "Allocazione eseguita" < < std::endl;
        }
    }

public:

    Image(){  } //default ctor

    //crea un'immagine riservando un buffer
    Image( std::size_t s):size(s) {
        realloc(s); // allocazione
    }

    //copia un'immagine da un altra immagine
    Image( const Image &o): Image(o.size){ // allocazione
        copyFrom(o);
    }

    //copia un'immagine da un altra immagine
    Image& operator = ( const Image &o){
        size = o.size;
        realloc(size); //allocazione
        copyFrom(o);
        return (*this);
    }

    // colora di un colore unico l'immagine
    bool drawAColor( unsigned char color ){
        for( std::size_t i = 0; i < size; i++)
            buffer[i] = color;

        return true;
    }

    // somma due immagine
    friend Image operator+( const Image &A, const Image &B){
        if(A.size!=B.size)
            throw MismatchingSize();

        Image image(A.size);
        for( std::size_t i = 0; i < A.size; i++)
            image.buffer[i] = A.buffer[i]+B.buffer[i];

        cout < < "Somma eseguita" < < std::endl;

        return image; //attenzione qui non viene effettuata ancora nessuna copia
    }

    // stampa immagine
    void print() const{
        for( std::size_t i = 0; i < size; i++)
            std::cout < < (int)buffer[i] < < " ";
        std::cout < < std::endl;
    }
};

 

 

Ora possiamo eseguire un "test di performance" eseguendo questo semplice programmino:

int main(){
    Image a(5),b(5);
    a.drawAColor(10); // mettiamo qualche valore a caso dentro le immagini.
    b.drawAColor(17);

    Image c;
    c = a+b; //scrittura apparentemente innocua e "leggera"

    a.print();
    b.print();
    c.print();
}

 

 

Su console il risultato sarà:

Allocazione eseguita
Allocazione eseguita
Allocazione eseguita
Somma eseguita
Allocazione eseguita
Copia eseguita

 

Il costo di "c=a+b" è:

-4 allocazioni

-1 somma (somma membro a membro dell'array)

-1 copia (copia membro a membro dell'array)

 

Move semantics:

 

Le Move Semantics, trattano "lo spostamento di oggetti". Lo scopo principale e' permettere di trasferire la proprietà di qualcosa. Per trasferire esplicitamente un oggetto si può usare l'utility "std::move" (si trova nell'header algorithm che viene praticamente sempre incluso da ogni altro header STL). Quello che viene fatto effettivamente è una via di mezzo tra una "deep copy" e una "shallow copy". Nella maggior parte dei casi il "move" viene eseguito implicitamente e non sarà quindi necessario chiamare "std::move".

 

C++:

1) Deep Copy: si fa usando il CopyConstructor, l'operator =, o un metodo "clone"

2) Shallow Copy: si fa usando puntatori e SMART POINTERS)

 

Nel C++11 è possibile eseguire una 3° operazione:

3) Move: si fa usando il MoveConstructor o il MoveAssignment (operator=(&&))

 

Il risultato finale sarà quello di ottenere una copia "shallow" che però possiamo utilizzare come se fosse una "deep copy", si tratta quindi di un operazione molto veloce: l'unico costo è che l'oggetto originale "da cui copiamo" perde il suo puntatore

 

Implementare un MoveConstructor e un MoveAssignment è piuttosto facile, si tratta di copiare membro a membro tutti i dati di una classe, ponendo particolare attenzione ai puntatori (bisogna copiarli nella nostra classe e impostarli a nullptr nell'altra classe).

 

Per aggiungere le move semantics alla classe Immagine vista prima ci basta aggiungere 2 nuovi metodi:

    Image( Image &&o){
        std::cout < < "Move Constructor chiamato!" < < std::endl;
        buffer = o.buffer; //copio i dati da "o"
        size = o.size;

        o.buffer = nullptr; //resetto "o"
        o.size = 0;
    }

    Image& operator = ( Image &&o){
        std::cout < < "Move Assignment chiamato!" < < std::endl;
        buffer = o.buffer; //copio i dati da "o"
        size = o.size;

        o.buffer = nullptr;  //resetto "o"
        o.size = 0;
        return (*this);
    }

 

Rieseguendo il "test di performance" otterremo stavolta il seguente risultato

 

Allocazione eseguita
Allocazione eseguita
Allocazione eseguita
Somma eseguita
Move Assignment chiamato!

 

E' stato chiamato il move assignment e questo ci ha permesso di effettuare 1 allocazione in meno e di non effettuare alcuna copia!

 

Infatti ora abbiamo:

3 Allocazioni

1 Somma (membro a membro)

1 Move Assignment (trascurabile)

 

Trovate il codice di test al seguente link:

http://ideone.com/iJJowB

 

L'Importanza di usare std::move

 

Avere oggetti "movable" è importantissimo perchè permette di trasferire anche gli oggetti "non copiabili" (ovvero oggetti su cui è stato "vietato" chiamare CopyConstructor o Operator=). Un famoso esempio di oggetto non copiabile sono gli std::istream e std::ostream. Questi oggetti rappresentato 2 risorse "stream" che non possono essere copiate. Al di la delle "ottimizzazioni" rese possibili da ciò, esiste quindi un vantaggio a livello di potenza espressiva del linguaggio, che è poi la vera essenza della move semantics.

 

Tipicamente in C++11 anzichè creare uno stream (scoped) alla vecchia maniera, si creerà uno stream (usando "new") e lo si metterà dentro un UNIQUE_PTR . Facendo questo è possibile "muovere" lo stream. Si può vedere facilmente che ora il lifetime dello stream non è più legato allo "scope", possiamo infatti muovere lo stream da una classe all'altra fintanto che ci serve. Dopodichè l'unique_ptr lo cancellerà automaticamente.

 

// alla vecchia maniera
int main(){
    using namespace std;
    istream inputFile(...);


    return 0; //inputFile viene distrutto qui (scoped)
}

// alla nuova maniera
int main(){
    using namespace std;
    unique_ptr < istream > inputFile( new istream(...)); // (managed!)
    
    function ( move(inputFile) ); //fintanto che serve, "inputFile" verrà tenuto in vita

    // a questo punto InputFile è stato già distrutto
    // quando? non importa. ci fidiamo di unique_ptr
    // quando non ci serve più viene distrutto in automatico.
    return 0;
}

 

IMPORTANTE!

 

Quando intendete usare il move constructor, fatelo esplicitamente usando sempre "std::move".

A rigor di logica il programma di esempio avrei dovuto scriverlo come:

 

Image c;
c = std::move(a+b);

 

La ragione per fare questo (oltre a rendere più esplicito il codice) è che in alcuni casi il MoveConstructor deve essere chiamato esplicitamente altrimenti il compilatore da solo non capisce cosa volete fare e chiamerà il copy constructor (se volete che qualcosa sia effettivamente mosso senza affidarvi alle regole implicite del C++, muovetelo con std::move):

 

#include < iostream >
#include < memory >
 
void funzione2( std::unique_ptr < int > ptr){
    std::cout < < "Valore: " < < (*ptr) < < std::endl;
}
 
void funzione1( std::unique_ptr < int > ptr){
    //funzione2( ptr); //errore di compilazione!
    funzione2(std::move(ptr));  //esplicitiamo il fatto che vogliamo muovere!
}
 
int main(){
    funzione1(std::unique_ptr < int > (new int(4)) );
    return 0;
}

 


In questo esempio otterreste un errore di compilazione perchè "unique_ptr" è "non-copyable", in altri casi il codice potrebbe compilare senza warnings o senza errori!

 

std::swap

Tra le ottimizzazioni rese possibili ce n'è una che tutti dovrebbero utilizzare, ovvero "std::swap". Un'implementazione possibile è:

template < typename T >
void swap ( T &A, T &B){
    T C = std::move(A);
    A = std::move(B);
    B = std::move(C);
}

 

Creare lo swap in questo modo genera quasi sempre codice ideale e ottimizzato. Sfruttare il pre-esistente std::swap a questo punto è consigliato rispetto all'utilizzo di qualunque "swap" homemade.

 

La maggior parte dei containers STL supporta le move semantics (std::string inclusa!).

Un qualunque algoritmo di ordinamento eseguito con "std::swap" su un container diventa quindi molto efficiente senza la necessità di dover scrivere specializzazioni dell'algoritmo per ogni container.

 

Fare lo swap di 2 stringhe non è più un'operazione con costo O(n) + allocazione, ma è un'operazione O(1) senza allocazione!

Dal punto di vista "exception-safety" significa avere meno codice inutile e meno eccezioni da gestire.

Campagne crowfunding

Just One Line
Siamo presenti su

     
Copyright ©2016 - Manifesto - Privacy - Termini di Servizio - Community - Collaboratori - Contattaci