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: Ne resterà solo uno
Pubblicato da Dario Oliveri il 2013-02-27 22:11:21

Oggi parleremo dell' "unique_ptr". Tralasciamolo per ora, e vediamo come creare un oggetto in C++ che abbia la stessa semantica (ovvero, per capire l'unique_ptr, creiamo qualcosa che si comporti nello stesso modo, o quasi per lo meno). Si da per scontato che abbiate molta dimestichezza con Shallow Copies e Deep Copies.

 

Semantica: Avere un oggetto "unico". Ovvero i tentativi di copia/moltiplicazione devono fallire in qualche modo(ad esempio o fallisce la copia, o la copia prende possesso del contenuto di un altro oggetto). L'oggetto deve essere accessibile solo da 1 "postazione" in poche parole.

 

Disambiguazione: (leggi questa sezione solo se ti è venuto in mente il "singleton", altrimenti passa pure a Implementazione) Questo articolo non ha nulla a che vedere con i Singletons. I Singletons si prendono l' "esclusiva" sulla creazione di qualcosa, gli unique pointers invece si prendono l' "esclusiva" sull'accessibilità di qualcosa.

 

Implementazione:

 

In C++ possiamo ottenere qualcosa di molto simile senza molto sforzo (ovviamente in C++11 il codice risulta molto più elegante, ma non è obbligatorio usare il C++11).

 

Classe di supporto (Item.hpp)

#include < iostream >
#include < string >

// Classe dummy, permette di vedere quando sono chiamati costruttori e distruttori.
class Item{
	std::string name;
public:
	Item(const std::string & newname) : name(newname) {
		std::cout << "Item()" << name << std::endl;
	}

	~Item(){
		std::cout << "~Item()" << name << std::endl;
	}

	void doSomething() const{
        	std::cout << "Hi by: " << name << std::endl,
	}
};

 

 

 

 

 Codice C++:

 

#include < cassert >
#include "Item.hpp"

class UniqueItem {

    //membri mutabili (cambiano anche se l'oggetto è const)
    mutable bool shallow_copy;
    mutable bool taken;

    mutable Item * data; // I dati su cui agiremo attraverso vari metodi.

    //altro...

public:

    UniqueItem(Item * newdata=0){
        shallow_copy = false;
        taken = false;
        data = newdata;
    }

    ~UniqueItem(){
        clean();
    }

    /** Copia l'altro oggetto, letteralmente prendendone possesso. */
    UniqueItem( const UniqueItem & other){
        taken = false; data = 0;
        if(!other.shallow_copy){
            assert(false); //pianta il programma
            data = 0;
            shallow_copy=false;
            return; //esce dal ctor!
        }
        other.taken = true;
        data = other.data;
        other.data = 0;
        shallow_copy = true;
    }

    void clean(){
        if(!shallow_copy || (shallow_copy && !taken))
            if(data != 0){
                delete data; //cancella dati solo se li possiede
            }
        data = 0;
    }

    /** Crea un nuovo oggetto a partire da questo. */
    UniqueItem createData( const std::string & name) const{
        UniqueItem dummy;
        dummy.data = new Item( name );
        dummy.shallow_copy = true; //rende l'oggetto trasferibile ma anche "weak"
        return dummy;
    }

    UniqueItem& operator = (const UniqueItem & other){
        taken = false; clean(); //devo cancellare i vecchi dati
        if(!other.shallow_copy){
            assert(false); //pianta il programma
            data = 0;
            shallow_copy=false;
            return (*this);
        }
        other.taken= true;
        data = other.data;
        other.data = 0;
        shallow_copy = true;
        return (*this);
    }

    void doSomething(){
        assert(data != 0);
        data->doSomething();
    }
};

 

 

Nota bene: questa classe ancora non è protetta contro l'auto assegnamento (C=C). Vi lascio come esercizio aggiungere qualche check per evitare l'auto assegnamento (per mantenere il codice più leggibile ho tolto questi check)

 

 

 

Con un attimo di attenzione si capisce quasi immediatamente cosa la classe UniqueItem fa:

#include < UniqueItem.hpp >

void function( UniqueItem & item){
	//fa qualcosa con item
}

void function2( UniqueItem item){
	//fa qualcosa con item
	
	//item viene distrutto (destructor chiamato su item)
	//oppure se fosse stato il metodo di una classe, potevamo memorizzare item
	//in una delle variabili membro
}

int main(){

	UniqueItem A(new Item("oggetto A"));
	UniqueItem B(A); //CRASH (giusto!)
	UniqueItem C(A.createData("oggetto C"));//  C != A (C è copiabile, A non lo è, ma copiando C lo priviamo del suo "data")
	
	function(A); //passaggio per riferimento consentito
	function2(A); // CRASH (giusto!)

	A.doSomething(); // ok 
	C.doSomething(); // ok
	
	function(C); // ok (passaggio per riferimento)
	function2(C); // oggetto C viene distrutto (passaggio per valore)
	
	C.doSomething(); //CRASH (giusto)
	
	//A viene distrutto qui
	return 0;
}

// Lo stesso valeva se al posto del copy constructor usavamo l'operatore =.
int main(){
	UniqueItem A;
	UniqueItem B=A; //CRASH (giusto!)
	UniqueItem C=A.createData()); //  C != A (C è copiabile, A non lo è, ma copiando C lo priviamo del suo "data")
	//...
}

 

 

Svantaggi? Bisogna sapere quali oggetti è possibile copiare e quali no, gli errori vengono fuori subito, ma soltanto a run-time. Sarebbe molto meglio trovare gli errori "prima".

 

 

C++11:

unique_ptr:

#include < memory >
#include < Item.hpp >

typedef std::unique_ptr < Item > UniqueItem;

void function( UniqueItem & item){
	//fa qualcosa con item
}

void function2( UniqueItem item){
	//fa qualcosa con item
	
	//item viene distrutto (destructor chiamato su item)
	//se questo era il metodo di una classe, item veniva memorizzato.
}

int main()
{
    UniqueItem A( new Item("oggetto A"));
    UniqueItem B(A); //errore di compilazione
    UniqueItem C( new Item("oggetto C"));
    
    function(A);
    function2(A); //errore di compilazione
    
    A->doSomething();
    C->doSomething();
    
    function(C); // ok
    function2(std::move(C)); // ok ma C viene distrutto 
                            //quando il pointer va out of scope
                            //alla fine della funzione.
	
    C->doSomething(); //CRASH (giusto)
}

 

 

 

Grazie al C++11 tutti gli errori stavolta sono trovati a compile-time, non male se si tiene conto che in alcuni casi la compilazione di un progetto può durare alcune ore. Un piccolo svantaggio invece è che usando "std::move" si possono fare parecchi casini, per cui se possibile è meglio evitare di usare "std::move".

 

disambiguazione: come vedete non è stato chiamato "delete" negli esempi, perchè non c'è bisogno di chiamarlo. Gli unique pointers si "auto-cancellano" automaticamente quando vanno out-of-scope. Per capire le regole di ciò basta seguire gli esempi (fate particolarmente attenzione alla differenza tra passaggio per riferimento e passaggio per valore). Questo accade per la maggior parte degli SMART POINTERS, lo scopo ti tutti gli smart pointers è fare in modo che l'utente possa dimenticarsi di chiamare "delete" (altri smart pointers sono : shared_ptr, weak_ptr ), si tratta per lo più di oggetti che funzionano come se fossero dei puntatori, ma forniscono features aggiuntive.

 

Ecco il codice:

Download link

 

 

Ma a cosa serve un unique_ptr?

 

Ecco subito un esempio pratico:

 

MessageQueue Producer/Consumer.

 

Supponiamo di avere 2 Thread (o processi paralli, chiamiamoli come volete), un thread produce messaggi, mentre l'altro thread li legge e li "consuma". Il fatto è che ogni message queue, può avere soltanto 1 Producer e 1 Consumer.

Qui usare il comportamento degli unique_ptr è fondamentale.

 

Sarà sufficiente:

 

std::unique_ptr < Producer > MessageQueue::getProducer(){
	Producer * ptr=nullptr;
	if(!ProducerFlag)
		ptr = new Producer(this);
	ProducerFlag = true;
	return std::unique_ptr < Producer > (ptr);
}

std::unique_ptr < Consumer > MessageQueue::getConsumer(){
	Consumer* ptr=nullptr;
	if(!ConsumerFlag)
		ptr = new Consumer(this);
	ConsumerFlag = true;
	return std::unique_ptr < Consumer > (ptr);
}

 

E avremo soltanto 1 Producer e 1 Consumer in circolazione. Ovviamente non si tratta di due metodi "thread-safe" ma in fase di inizializzazion può essere utile, e impedirebbe che qualcuno si possa impadronire per sbaglio dei 2 oggetti (anzi ora è possibile trasferirli a qualcun'altro quando non servono più, letteralmente riciclandoli, come se fossero una pool di oggetti con dentro 1 solo oggetto).

 

Nel prossimo articolo, faremo una message queue (usando i lock, e se riesco a farla funzionare, pure una message queue lock-less che funziona senza variabili atomiche, seee vabbè sognare è bello).

 

Come sempre suggerimenti per rendere più chiaro l'articolo e/o segnalazione di errori sono ben accetti :)

Campagne crowfunding

Just One Line
Siamo presenti su

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