domenica 26 aprile 2015

Modern C++ - Classi e operatori di copia e spostamento

Bentornati al corso di Modern C++, dopo una lunga sosta.

Nella precedente lezione abbiamo visto come e' fatta una classe, con un breve esempio riguardante il costruttore e il distruttore. Da questo punto in avanti, ci sposteremo sempre piu' verso le librerie Qt, dato che fanno uso massiccio di classi ed oggetti (praticamente l'intera libreria e' basata sulla classe-base QObject).

Oggi vediamo nel dettaglio la realizzazione di una classe Automobile che offre la possibilita' di impostare varie informazioni e copiare/spostare l'istanza della classe (l'oggetto) in altri punti di un programma.

I costruttori di copia e di spostamento sono particolari costruttori che sono rispettivamente la copia esatta e la destinazione di un altro oggetto. In particolare, il costruttore di movimento (move-constructor) e' stato introdotto di recente in C++ (C++11) e ha a che fare con la move-semantics e i riferimenti R-value (ovvero l'operatore & si riferisce all'operando destro in una operazione).

Una classe con operatori di copia e di spostamento si presenta in tal modo:

//Auto.h
#include 

class Auto
{
  public:
    Auto();  //costruttore di default
    Auto(int cc);  //Overload del costruttore
    
    ~Auto(); //distruttore

    Auto(const Auto &altra_auto); //copy-constructor
    Auto(Auto &&altra_auto);      //move-constructor C++11

    Auto& operator=(const Auto &altra_auto); //copy-assignment-operator
    Auto& operator=(Auto &&altra_auto); //move-assignment-operator C++11

  private:
    int cilindrata;
    int num_porte;
    int vel;
    std::string *colore;
    std::unique_ptr nome;
};

Nella classe sopra definita abbiamo il costruttore di default, in genere usato per inizializzare i propri membri (ad esempio, cilindrata = 0), un costuttore che specifica la cilindrata dell'automobile durante la fase di costruzione e poi un costruttore di copie e uno di movimento.
Un costruttore di copie dovrebbe sempre procedere all'inizializzazione dei membri dell'oggetto corrente con quelli dell'oggetto da copiare.

//in Auto.cpp

Auto::Auto(const Auto &altra_auto): 
   cilindrata(altra_auto.cilindrata), 
   num_porte(altra_auto.porte),
   vel(altra_auto.vel)
{
  colore = new std::string(altra_auto.colore);
  nome.reset(new std::string(altra_auto.nome));
}

// ...

Auto nuova_auto(altra_auto);  //costruttore di copie invocato - allocazione statica
std::unique_ptr nuova_auto(new Auto(altra_auto)); // allocazione dinamica con smart pointer


Nel caso del move-constructor abbiamo la seguente definizione:

//in Auto.cpp

Auto::Auto(Auto &&altra_auto): 
   cilindrata(altra_auto.cilindrata), 
   num_porte(altra_auto.porte),
   vel(altra_auto.vel)
{
  colore = altra_auto.colore;
  nome = altra_auto.nome;

  //cancello i dati di altra_auto

  altra_auto.cilindrata = altra_auto.num_porte = altra_auto.vel = 0;
  altra_auto.colore = nullptr;
  altra_auto.nome.reset(); 
}

// ...

Auto nuova_auto(std::move(altra_auto));  //altra_auto avrà i suoi membri reimpostati
std::unique_ptr nuova_auto(new Auto(std::move(altra_auto));


Il move contructor quindi ruba le informazioni di un oggetto, trasferendole nell'oggetto corrente, lasciando pero' l'oggetto derubato in uno stato valido (puntatori impostati a nullptr ad esempio).
Gli operatori di assegnamento sono quasi simili a quanto visto finora, con l'aggiunta di dover deallocare/allocare nuove risorse per i suoi membri, prima di effettuare l'operazione di assegnamento:

//in Auto.cpp
Auto& Auto::Auto(const Auto &altra_auto): 
   cilindrata(altra_auto.cilindrata), 
   num_porte(altra_auto.porte),
   vel(altra_auto.vel)
{
  //elimino i vecchi dati
  delete colore;
  colore = nullptr;
  nome.reset();  //esplicito, ma non necessario
  //alloco i nuovi dati
  colore = new std::string(altra_auto.colore);

  //con gli smart risulta piú semplice:
  nome.reset(new std::string(altra_auto.nome));

  return *this; //riferimento all'oggetto corrente
}

Auto::Auto(Auto &&altra_auto): 
   cilindrata(altra_auto.cilindrata), 
   num_porte(altra_auto.porte),
   vel(altra_auto.vel)
{
  //elimino i vecchi dati
  delete colore;
  colore = nullptr;
  nome.reset();  //esplicito, ma non necessario
  //sposto i nuovi dati
  colore = altra_auto.colore;
  nome = altra_auto.nome;

  //cancello i dati di altra_auto
  altra_auto.cilindrata = altra_auto.num_porte = altra_auto.vel = 0;
  altra_auto.colore = nullptr;
  altra_auto.nome.reset(); 
  
  return *this; //riferimento all'oggetto corrente
}

// ...
Auto nuova_auto; //Default constructor
nuova_auto = altra_auto;   //copy-assignment 
nuova_auto = std::move(altra_auto); //move-assignment, altra_auto reimpostata


Il move constructor e il move operator introducono la possibilitá di poter spostare quindi i dati senza ricorrere ad allocazioni/copie dei dati, onerose per dati di grandi dimensioni (come ad esempio un vettore std::vector con migliaia di elementi): l'operazione (ove possibile, altrimenti il compilatore richiama gli operatori di copia, nel caso peggiore) non infligge penalizzazione all'esecuzione del nostro programma. 

Gli operatori su definiti, se non esplicitamente specificati, vengono auto-generati dal compilatore in modo automatico (implicito), cercando di ottimizzare il piú possibile il codice, ma non sempre tale auto-generazione risulta ottimale (specie per classi complesse). Le soluzioni sono 2: specificare gli operatori come visto in questa lezione, oppure disabilitarli, se non ne abbiamo bisogno:

//Auto.h
class Auto
{
  public:
    Auto();  //costruttore di default
    
    ~Auto(); //distruttore

    Auto(const Auto &altra_auto) = delete; //copy-constructor
    Auto(Auto &&altra_auto) = delete;      //move-constructor C++11

    Auto& operator=(const Auto &altra_auto) = delete; //copy-assignment-operator
    Auto& operator=(Auto &&altra_auto) = delete; //move-assignment-operator C++11
};

La parola chiave delete permette di eliminare la generazione di tali operazioni, disabilitando la possibilitá di spostare o copiare il nostro oggetto.
Nel caso in cui sostituissimo delete con default, indicheremmo al compilatore che l'auto generazione di tali costruttori/operatori di assegnamento vanno piú che bene.

Alla prossima!

Nessun commento:

Posta un commento