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: Smart Pointers
Pubblicato da Dario Oliveri il 2013-06-07 21:58:27

In uno degli articoli precedenti avevo illustrato il funzionamento degli unique_ptr.

Oggi andiamo a completare l'argomento "Smart Pointers" parlando anche di:

 

-shared_ptr

-weak_ptr

 

Importante: "auto_ptr" è da considerarsi deprecato, il suo naturale sostituto è l'unique_ptr (che sostituisce anche lo scoped_ptr).

 

Alcuni degli smart pointers che sono finiti nel C++11 sono stati originariamente introdotti da boost (tranne l'unique_ptr). Quindi chi già utilizzava boost non avrà grosse difficoltà a capire il funzionamento di questi puntatori, anche se qualche differenza comunque c'è.

 

Gli Smart Pointers non sono nient'altro che classi che agiscono da "intermediari" per i puntatori. Lo scopo degli Smart Pointers è di garantire la cancellazione automatica di oggetti che non servono più (è un vantaggio enorme, ma a volte può essere uno svantaggio. Di sicuro se usati correttamente sono un aiuto extra per combattere I memory leaks).

 

Shared Pointer

 

Semantica: Uno shared pointer viene inizializzato con un puntatore ad un oggetto singolo (è importante: viene chiamato "delete" non "delete []"). Nel momento in cui lo shared Pointer viene creato si assume tutte le responsabilità della cancellazione di quell'oggetto (non dovrete più chiamare "delete"), questo viene fatto permettendo al tempo stesso di condividere l'oggetto in più classi contemporaneamente.

 

shared_ptr < Room > myPtr( new Room() ); // creo l'oggetto e lo metto dentro allo shared pointer
shared_ptr < Room > myPtr2 = myPtr;
shared_ptr < Room > myPtr3 = myPtr; // posso crearmi altri puntatori
shared_ptr < Room > myPtr4 = myPtr2; // e usarli a loro volta per crearne degli altri

 

L'oggetto viene automaticamente cancellato solo quando TUTTI gli shared pointer che lo contenevano vengono distrutti. Questo viene fatto attraverso un meccanismo di "reference counting".

 

Vediamo come poteva essere scritto in C++03 un oggetto simile.

class SharedFloat{

    float * sharedValue;
    int   * referenceCounter;

public:

    SharedFloat(float * value){
        sharedValue = value;
        referenceCounter = new int(1); //creo il reference counter
    }

    SharedFloat( const SharedFloat & other){
        (*other.referenceCounter)++;  //aumento il reference counter
        sharedValue = other.sharedValue;
        referenceCounter = other.referenceCounter;
    }

    SharedFloat & operator=(const SharedFloat & other){
        (*other.referenceCounter)++;
        sharedValue = other.sharedValue;
        referenceCounter = other.referenceCounter;
	return (*this);
    }

    ~SharedFloat(){
        (*referenceCounter)--; //quando l'oggetto viene distrutto diminuisco il reference counter
        if((*referenceCounter)==0){ //se il conteggio arriva a zero allora tutti gli oggetti sono distrutti
            delete sharedValue;     //e posso quindi cancellare la risorsa condivisa
            delete referenceCounter; //... ovviamente non mi serve più nemmeno il referenceCounter :)
            cout < < "Oggetto condiviso distrutto" < < endl;
        }
    }
};

 

 

Come vedete il meccanismo di reference counting non è nulla di particolarmente complesso, ma è bene capirlo per conoscerne i "costi":

 

- il reference counter deve essere allocato

- il reference counter deve essere aumentato e diminuito

 

Ovviamente non sono costi così "grandi", ma questo dipende anche dall'uso che ne facciamo.

 

Ora passiamo però alle note dolenti:

 

#include < iostream >
#include < memory >

struct CircularListNode{
    shared_ptr < CircularListNode > next;
    shared_ptr < CircularListNode > prev;
    ~CircularListNode(){
		cout < < "nodo distrutto" < < endl;
	}
};

int main(){
	//creiamo una lista circolare bidirezionale di 2 nodi
	shared_ptr < CircularListNode > first(new CircularListNode); //creo il primo nodo
	first-> next = shared_ptr < CircularListNode > (new CircularListNode); //lo collego al 2° nodo
	first-> prev = first -> next; //avendo solo 2 nodi anche da dietro vediamo comunque il 2° nodo
	first-> prev-> next = first; //collego il secondo nodo al 1°
	first-> prev-> prev = first; //colego il secondo nodo al 1° anche da dietro.
	return 0;
}

 

Se proviamo a far partire il programma vediamo che "nodo distrutto" non verrà mai stampato a console. Ecco che gli shared_ptr hanno già mostrato il loro "dark side" ovvero i riferimenti circolari. In pratica un puntatore ha creato un "anello" che riporta su se stesso, questo impedisce la cancellazione di quel puntatore e di tutti i puntatori ad esso collegati (memory leak).

 

Allora entrano in gioco i weak pointers

 

Weak pointer

 

Semantica: Un weak_ptr permette di tenere un riferimento ad un oggetto condiviso (shared pointer) con la differenza che non ne cambia il reference counter, Il weak_ptr per questo motivo non permette di accedere all'oggetto direttamente: prima deve essere chiamato "lock", e tale operazione può fallire (ritornando un nullptr) se l'oggetto condiviso è stato nel frattempo distrutto.

 

Esempio di funzionamento: nel primo caso il "lock" ha successo, nel secondo caso ritorna "nullptr", il codice deve essere pronto a comportarsi correttamente in entrambi i casi. Se il weak_ptr non funzionasse correttamente il programma si pianterebbe su uno dei 2 "assert". Ma invece vediamo che non si pianta.

#include < iostream >
#include < cassert >
using namespace std;

int main(){

	weak_ptr < int > weakInt;

	{ 
		shared_ptr < int > sharedInt( new int(5) );
		weakInt = sharedInt;

		assert ( weakInt.lock() != nullptr ); //QUI CI SAREBBE DA DIRE MOLTO.. PROSSIMO ARTICOLO FORSE ^^
		//Per farla breve "lock" crea un 2° shared_pointer che "vive" solamente per tempo il necessario a 
		//valutare il parametro. Nella realtà il compilatore si accorge grazie alle "move semantics"
		//che questo oggetto verrebbe distrutto subito e non lo crea affatto. Bada bene che non si tratta
		//di un ottimizzazione "arbitraria" del compilatore. E' proprio una feature del linguaggio.
	}
	//sharedInt è andato "out-of-scope".

	assert (weakInt.lock() == nullptr); //lo shared pointer è stato rilasciato e "5" è stato "deleted"

	return 0;
}

 

 

Usando "lock" creiamo di fatto uno "shared_ptr", questo ci garantisce che una volta chiamato "lock" l'oggetto rimanga utilizzabile per il tempo necessario ( in altre parole, "lock" aumenta il reference counting di 1). ATTENZIONE: c'è di nuovo il rischio di un riferimento circolare, quindi non salvate da nessuna parte questo shared_ptr. La regola a prova di errore sarebbe:

 

-Non passarlo come parametro a funzioni

-Non ritornarlo

 

L'utilità di un "weak_ptr" è che permette di creare uno "shared_ptr" temporaneo. Se invece noi lo andiamo a salvare non è più temporaneo (a dire il vero qualche volta mi sono chiesto: "Ma non potevano fare anche un scoped_shared_ptr?").

 

Vediamo come diventerebbe la lista circolare usando i weak pointers

 

#include < iostream >
#include < memory >

struct CircularListNode2{
    shared_ptr < CircularListNode2 > next;
    weak_ptr < CircularListNode2 >   prev;
    ~CircularListNode2(){
		cout < < "nodo 2 distrutto" < < endl;
		next.reset();
	}
};

int main(){
	//creiamo una lista circolare bidirezionale di 2 nodi
	shared_ptr < CircularListNode2 > first(new CircularListNode2); //creo il primo nodo
	first-> next = shared_ptr < CircularListNode2 > (new CircularListNode2); //lo collego al 2° nodo
	first-> prev = first-> next; //avendo solo 2 nodi anche da dietro vediamo comunque il 2° nodo
	first-> next-> next = first; //collego il secondo nodo al 1°
	first-> next-> prev = first; //colego il secondo nodo al 1° anche da dietro.
	first-> next.reset(); // sto resettando un puntatore DENTRO alla lista (se resettassi "first" non risolverei nulla)

	//"first" va out-of-scope qui.. (si resetta da solo in poche parole)
	return 0;
}

 

E' vero ho aggiunto dei "reset", ma se non avessi utilizzato un "weak_ptr" non avrei risolto nulla.

Infatti anche aggiungendo "reset" nei primo esempio dei riferimenti circolari non viene cancellato nessun nodo. Questo schema dovrebbe chiarire le cose:

 

 

Potete verificare con il codice allegato, noterete che compilando e lanciando il codice sorgente, compariranno soltanto i seguenti messaggi:

"Oggetto condiviso distrutto"

"nodo 2 distrutto"

 

Invece il messaggio:

"nodo distrutto"

 

non comparirà proprio perchè l'oggetto non è stato distrutto.

 

Codice Allegato [LINK: clicca qui]

 

Altri esempi applicativi:-Il weak_ptr è particolarmente utile per strutture gerarchiche (alberi) che si "auto-cancellano". Infatti se ogni padre punta ai figli usando "shared_ptr", mentre i figli puntano ai padri usano "weak_ptr" possiamo tranquillamente evitare riferimenti circolari e ottenere "alberi managed". (ci basterà chiamare "reset" sulla root dell'albero per cancellarlo del tutto).

 

 

Campagne crowfunding

Just One Line
Siamo presenti su

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