\\ Home : Articoli : Stampa
Smart Pointers
Di Kelesis (del 17/06/2008 @ 21:25:06, in C++, linkato 1912 volte)

Con l'avvento del C# e del suo garbage collector gli smart pointers hanno perso parte della loro utilità. Ma in C++ rimangono degli alleati validissimi, e possono risolvere una moltitudine di problemi. Spesso spinosi. Quello che vi propongo non è che uno dei tanti smart pointers in circolazione. Ci tengo a precisare che non ho inventato niente di nuovo. Questo smart pointer infatti esiste già nella libreria boost (concettualmente), ed è il boost::weak_ptr.
Se non avete boost ma avete la tr1 allora l'equivalente è lo std::tr1::weak_ptr (che poi è una copia di quello implementato nella celeberrima boost). Il tipo di smart pointer che vi presento è solitamente implementato in due 'pezzi'.
Gli smart pointer puntano ad un proxy, il proxy punta alla Risorsa che volete gestire tramite gli smart pointer.
La Risorsa è al corrente dell'esistenza del proprio proxy, e al momento della propria distruzione non fa che informare il proxy che è giunto il momento di annullare tutti i puntatori.
Io ho fatto le cose un pochino diversamente. La parte proxy dello smart pointer è implementata come una abstract base class dalla quale la classe di un'ipotetica Risorsa deve ereditare.
In questo modo è possibile integrare il mio smart pointer nel vostro progetto apportando una quantità minima di modifiche, come vi spiegherò in seguito.
Immaginate che nel vostro programma una stessa Risorsa (una classe) sia referenziata da più classi sparse in più punti del programma. Immaginate poi (per un qualunque motivo) che la vostra Risorsa venga distrutta da un momento all'altro, lasciando dietro di se una scia di puntatori non più validi, ma che tuttavia possiedono indirizzi nonZero. Ecco che siete nei guai. Al primo utilizzo di uno di quei puntatori malandrini, il vostro programma andrà in crash.
Se proprio volete un caso pratico potreste immaginare un plotone di Terran Marine che prende di mira un povero Zerg Zergling. Tutti i Marine possiedono un puntatore allo Zergling nella loro lista dei bersagli. E quando lo Zergling muore?
L'istanza di quello Zergling non è più valida, e tutti i puntatori -che ancora lo stanno puntando- dovrebbero essere annullati.
Ma i dumb pointers non offrono questa funzionalità. Vi serve uno smart pointer.
Se preferite, vi serve il mio Chain Pointer.
Pubblico il sorgente completo. Gratis et amore dei.
Deve stare tutto in un unico header. Non vi sono dipendenze aggiuntive.
Non vi sono beghe legali. Copiatelo, modificatelo come credete, usatelo se vi torna utile...

 
// header: chainptr.h

#if !defined __CHAINPTR_H___INCLUDED__
#define __CHAINPTR_H___INCLUDED__

    // macro
    #if !defined NULL
        #define NULL 0
    #endif

    // class forwarding
    template <typename T> class link_ptr;

// +-------+
// | PROXY |
// +-------+
template <typename T>
class chain_ptr abstract
{
    friend link_ptr;

    private:
        link_ptr* _chain;

    protected:
        chain_ptr () : _chain (NULL) {}
        virtual ~chain_ptr () = 0
        {
            link_ptr* Iter = this->_chain;
            link_ptr* Curr;

            while (Iter)
            {
                Curr       = Iter;
                Iter       = Iter->prev;
                Curr->p    = NULL;
                Curr->prev = NULL;
                Curr->next = NULL;
            }

            //this->_chain = NULL; <-- Superfluo. 
            //La Risorsa è ormai distrutta.
        }
};



// +---------------+
// | SMART POINTER |
// +---------------+
template <typename T>
class link_ptr
{
    friend chain_ptr;

    private:
        T*           p;
        link_ptr* prev;
        link_ptr* next;

    public:
        operator bool () const
        {
            return (this->p != NULL);
        }
        const T* operator -> () const
        {
            return this->p;
        }
        T* operator -> ()
        {
            return this->p;
        }
        link_ptr& operator = (const T* ptr)
        {
            if (this->p)
            {
                if (!ptr)
                {
                    //////////////////////////////////////////////////////////////////////
                    // Il puntatore è stato annullato direttamente. L'attuale catena viene
                    // smembrata e la Risorsa associata viene distrutta. Questo metodo è
                    // equivalente al metodo NullChain (NULL).
                    //////////////////////////////////////////////////////////////////////

                    delete this->p;
                }
                else if (this->p != ptr)
                {
                    //////////////////////////////////////////////////////////////////////
                    // E' stata specificata una nuova Risorsa per questa catena. L'attuale
                    // catena di puntatori viene aggiunta in blocco alla catena della
                    // nuova Risorsa (se alcuna). La vecchia Risorsa viene distrutta.
                    //////////////////////////////////////////////////////////////////////

                    T* pOldObj = this->p;

                    link_ptr* Iter = ((chain_ptr*) pOldObj)->_chain;
                    link_ptr* Curr;

                    // Collega la vecchia catena alla nuova Risorsa.
                    while (Iter)
                    {
                        Curr    = Iter;
                        Iter    = Iter->prev;
                        Curr->p = (T*) ptr;
                    }

                    if (((chain_ptr*) ptr)->_chain)
                    {
                        // Curr è ora il 1° anello della vecchia catena.
                        Curr->prev       = ((chain_ptr*) ptr)->_chain;
                        Curr->prev->next = Curr;
                    }

                    ((chain_ptr*) ptr)->_chain = ((chain_ptr*) pOldObj)->_chain;

                    // Distrugge la vecchia Risorsa.
                    ((chain_ptr*) pOldObj)->_chain = NULL;
                    delete pOldObj;
                }
            }
            else
            {
                ///////////////////////////////////////////////////////////////////
                // Questo puntatore non è parte di alcuna catena.
                // Se la Risorsa in argomento non possiede già una catena, questo
                // puntatore ne diventa il 1° anello. Altrimenti il puntatore viene
                // aggiunto alla catena esistente.
                ///////////////////////////////////////////////////////////////////

                this->p = (T*) ptr;

                if (((chain_ptr*) ptr)->_chain)
                {
                    this->prev       = ((chain_ptr*) this->p)->_chain;
                    this->prev->next = this;
                }

                ((chain_ptr*) this->p)->_chain = this;
            }

            return *this;
        }
        link_ptr& operator = (const link_ptr& ptr)
        {
            ////////////////////////////////////////////////////////////////////
            // Rimuove questo puntatore dalla catena attuale, e lo aggiunge alla
            // catena del puntatore in argomento.
            ////////////////////////////////////////////////////////////////////

            if ((this != &ptr) && (this->p != ptr.p))
            {
                this->Null ();

                if (ptr.p)
                {
                    this->p                           = ptr.p;
                    this->prev                        = ((chain_ptr*) this->p)->_chain;
                    this->prev->next                  = this;
                    ((chain_ptr*) this->p)->_chain = this;
                }
            }

            return *this;
        }
        bool operator == (const link_ptr& ptr) const
        {
            return (this->p == ptr.p);
        }
        bool operator != (const link_ptr& ptr) const
        {
            return (this->p != ptr.p);
        }
        const T* Ptr (void) const
        {
            return this->p;
        }
        bool IsLast (void) const
        {
            /////////////////////////////////////////////////////////////
            // Determina se il puntatore è l'ultimo rimasto della catena.
            /////////////////////////////////////////////////////////////

            return (!this->prev && !this->next);
        }
        void MoveTo (link_ptr& ptr)
        {
            /////////////////////////////////////////////////////////////////////
            // Invalida questo puntatore e trasferisce contenuto e riferimenti ad
            // un altro puntatore.
            /////////////////////////////////////////////////////////////////////

            if (this != &ptr)
            {
                if (this->p == ptr.p)
                {
                    this->Null ();
                }
                else
                {
                    ptr.Null ();

                    if (this->p)
                    {
                        ptr.p    = this->p;
                        ptr.prev = this->prev;
                        ptr.next = this->next;

                        if (ptr.prev)ptr.prev->next = &ptr;
                        if (ptr.next)ptr.next->prev = &ptr;
                        if (((chain_ptr*) ptr.p)->_chain == this) 
                               ((chain_ptr*) ptr.p)->_chain = &ptr;

                        // Cancella il puntatore.
                        this->p    = NULL;
                        this->prev = NULL;
                        this->next = NULL;
                    }
                }
            }
        }
        link_ptr& Null (void)
        {
            //////////////////////////////////////////////////////////////////////
            // Rimuove questo puntatore dalla catena. Inoltre distrugge la Risorsa
            // se questo è l'unico puntatore esistente.
            //////////////////////////////////////////////////////////////////////

            if (this->p)
            {
                // Rimuove il puntatore dalla catena.
                if (this->prev) this->prev->next                  = this->next;
                if (this->next) this->next->prev                  = this->prev;
                else            ((chain_ptr*) this->p)->_chain = this->prev;

                // Distrugge la Risorsa (se necessario).
                if (!((chain_ptr*) this->p)->_chain) delete this->p;

                // Cancella il puntatore.
                this->p    = NULL;
                this->prev = NULL;
                this->next = NULL;
            }

            return *this;
        }
        link_ptr& NullChain (T** ppObj = NULL)
        {
            ////////////////////////////////////////////////////////////////////
            // Distrugge la catena di puntatori. Se richiesto, restituisce un
            // puntatore alla Risorsa, che altrimenti viene anch'essa distrutta.
            ////////////////////////////////////////////////////////////////////

            if (!ppObj)
            {
                if (this->p) delete this->p;
            }
            else if (*ppObj = this->p) // L'assegnazione è voluta.
            {
                link_ptr* Iter = ((chain_ptr*) this->p)->_chain;
                link_ptr* Curr;

                while (Iter)
                {
                    Curr       = Iter;
                    Iter       = Iter->prev;
                    Curr->p    = NULL;
                    Curr->prev = NULL;
                    Curr->next = NULL;
                }

                ((chain_ptr*) *ppObj)->_chain = NULL;
            }

            return *this;
        }
        link_ptr () : p    (NULL),
                      prev (NULL),
                      next (NULL) {}
        link_ptr (const T* ptr) : p    (NULL),
                                  prev (NULL),
                                  next (NULL)
        {
            if (ptr)
            {
                this->p = (T*) ptr;

                if (((chain_ptr*) this->p)->_chain)
                {
                    this->prev       = ((chain_ptr*) this->p)->_chain;
                    this->prev->next = this;
                }

                ((chain_ptr*) this->p)->_chain = this;
            }
        }
        link_ptr (const link_ptr& ptr) : p    (NULL),
                                            prev (NULL),
                                            next (NULL)
        {
            if (ptr.p)
            {
                this->p                           = ptr.p;
                this->prev                        = ((chain_ptr*) this->p)->_chain;
                this->prev->next                  = this;
                ((chain_ptr*) this->p)->_chain = this;
            }
        }
        ~link_ptr ()
        {
            this->Null ();
        }
};

#endif // __CHAINPTR_H___INCLUDED__

Il comportamento del chain pointer è intuitivo. Tende a prendere il controllo esclusivo del ciclo di vita della vostra Risorsa. Come è logico pensare, la sua priorità è quella di preservare la vostra Risorsa il più a lungo possibile, ma non un istante di più. Quindi: Fintanto che avete almeno 1 smart pointer che punti alla vostra Risorsa, questa rimane in vita. Nel momento in cui anche l'ultimo smart pointer (per una data Risorsa) esce di scena, la Risorsa viene distrutta. Quando una Risorsa viene distrutta (in qualunque modo) tutti gli smart pointer che ancora puntano a quella Risorsa si auto-annullano. Il chain pointer espone le stesse funzionalità di un dumb pointer, quindi potete usarlo come fareste per un qualunque puntatore. A queste funzionalità sono poi affiancate quelle per la gestione del singolo smart pointer o anche di tutta la catena di puntatori alla quale uno smart pointer appartiene. L'insieme degli smart pointer che puntano alla stessa Risorsa forma infatti una catena di puntatori. Ogni Risorsa possiede una propria catena di puntatori. Ogni puntatore può essere considerato un anello di questa catena, ed ecco il perchè dei nomi chain_ptr e link_ptr. Per poter gestire una Risorsa tramite il chain pointer, occorre modificare leggermente la classe della Risorsa. Niente di preoccupante, giudicate voi stessi. Questa potrebbe essere la classe di una delle vostre Risorse:

class Resource
{
    private:
        long Stuff;

    public:
        void DoStuff (void) { /*...*/ }

        Resource  () { /*...*/ }
        ~Resource () { /*...*/ }
};

E questa sarebbe la stessa classe, ma gestibile tramite il chain pointer:

 

#include "chainptr.h"

class Resource : chain_ptr
{
    private:
        long Stuff;

    public:
        void DoStuff (void) { /*...*/ }

        Resource  () { /*...*/ }
        ~Resource () { /*...*/ }
};

 

Fatto.
Ho incluso l'header del chain pointer (tanto per mettere i puntini sulle 'i') e ho ereditato la classe della Risorsa dalla classe chain_ptr, dove il parametro T è la Risorsa stessa. Avrete notato che non ho specificato private o protected o public per ereditare da chain_ptr. Non è un errore. Qualunque cosa scriviate è ininfluenete. L'unico membro di chain_ptr è infatti private. E rimane private in ogni caso, perchè la vostra Risorsa non ci deve mettere le mani. Il Costruttore e il Distruttore di chain_ptr invece sono protected. E quindi diventano public, per la vostra Risorsa, indipendentemente da cosa specifichiate per ereditare.
Non potrebbe essere più semplice : ) Se poi date un'occhiata al link_ptr vedrete che faccio una cosa strana. Mi riferisco ai metodi:
- link_ptr::operator = (const T* ptr)
- link_ptr::link_ptr (const T* ptr)
In questi metodi l'argomento è dichiarato const, che come saprete significa "sola lettura". Ma poi nei metodi infrango la regola e mi metto a modificare il contenuto del parametro in argomento, cioè vado a scrivere in una Risorsa nella quale non sarebbe permesso scrivere. Tecnicamente sarebbe un errore. Ma in pratica non lo è. Il link_ptr infatti non tocca mai la vostra Risorsa. Semmai tocca il proxy dentro alla Risorsa. Ma il proxy è in fin dei conti un corpo estraneo che non ha nulla a che vedere con il buon funzionamento della vostra Risorsa. Quindi è lecito considerare la clausola di "sola lettura" come pienamente rispettata.
Non voglio offendere la vostra intelligenza dandovi codice di esempio per illustrare l'utilizzo del chain pointer. Mi limiterò a descrivere i metodi principali del link_ptr.


operator bool () const
Questo è il typecast per il tipo bool. Vi permette di testare se uno smart pointer è NULL proprio come fareste per un dumb pointer.


const T* operator -> () const
T* operator -> ()

Questi sono i metodi per esporre le funzionalità della Risorsa puntata dal link_ptr. Va da se che se la Risorsa è NULL, usare questi metodi vi manda in crash senza appello. Proprio come un normale puntatore.


link_ptr& operator = (const link_ptr& ptr)
Questo metodo modifica un singolo smart pointer. Il puntatore di destinazione viene aggiunto alla catena del puntatore in argomento. Se il puntatore di destinazione faceva parte di una catena, esso viene rimosso dalla catena attuale e poi viene aggiunto alla catena del puntatore in argomento. Se il puntatore in argomento è NULL, il puntatore di destinazione diventa NULL. Se i due puntatori fanno già parte della stessa catena, non succede niente. Se il puntatore di destinazione era l'ultimo a tenere in vita una Risorsa, la Risorsa viene distrutta. In altre parole lo usate per effettuare una copia di puntatori. Solo che il metodo si preoccupa di preservare l'integrità delle catene (e delle Risorse puntate). Tutto qui.


link_ptr& operator = (const T* ptr)
Questo metodo è simile SOLO in apparenza all'altro operatore =. A differenza dell'altro metodo, questo modifica un'intera catena di puntatori.
Quindi fate attenzione a come lo usate. La Risorsa attualmente puntata dalla catena del puntatore di destinazione viene distrutta, e poi la catena della vecchia Risorsa viene aggiunta alla catena della Risorsa in argomento (se alcuna). In pratica trasferite una catena da una Risorsa all'altra.


bool operator == (const link_ptr& ptr) const
bool operator != (const link_ptr& ptr) const

I metodi per comparare due link_ptr. Puntano alla stessa Risorsa? Puntano a Risorse diverse? Potreste voler aggiungere i metodi per comparare un link_ptr e una Risorsa. Decidete voi.

const T* Ptr (void) const
Questo è un metodo per recuperare un dumb pointer alla vostra Risorsa. Tecnicamente non dovreste mai aver bisogno di usarlo. Ma non si può mai sapere...


bool IsLast (void) const
Quando volete controllare se state per annullare l'ultimo smart pointer che tiene in vita una Risorsa, questo è il metodo che fa per voi.

void MoveTo (link_ptr& ptr)
Gli smart pointer sanno l'uno dell'altro grazie a dei puntatori interni. Se per disgrazia compromettere l'integrità di questi puntatori, vi mettete in guai seri. La necessità di spostare dei puntatori da una memoria all'altra può presentarsi quando avete una lista di puntatori. Supponete di rimuovere un elemento al centro della lista. Nella lista rimane un 'buco'. Se poi volete compattare la lista (per eliminare il buco), allora siete tentati di spostare all'indietro quegli elementi successivi al buco. Usare ::memcpy() o ::memmove() sarebbe una scorciatoia veloce, ma comprometterebbe irrimediabilmente la catena di puntatori che state toccando. Questo perchè nonostante i puntatori cambino di posto in memoria, i loro puntatori interni verrebbero copiati bitwise, e manterrebbero i vecchi indirizzi. Pertanto dovete usare la MoveTo() per trasferire un puntatore da un indirizzo all'altro. Alternativamente potete effettuare una copia di puntatori (tramite l'operatore "=") e poi annullare il vecchio puntatore (tramite Null() ). Ma questo approccio si rivela più lento (e meno elegante) della MoveTo().


link_ptr& Null (void)
Questo metodo annulla un singolo smart pointer, togliendolo dalla catena. Se annullate l'ultimo puntatore esistente per una data Risorsa, la Risorsa viene distrutta.

link_ptr& NullChain (T** ppObj = NULL)
Questo metodo è invocabile da qualunque smart pointer in una catena, e serve a smembrare l'intera catena. Potete usare il parametro opzionale per richiedere un dumb pointer alla Risorsa. Così facendo distruggereste la catena, ma salvereste la Risorsa che la catena gestiva. Se invece non fate richiesta per il dumb pointer, la vostra Risorsa muore insieme alla catena che la gestiva.

link_ptr ()
Costruttore standard. Imposta preventivamente i membri dello smart pointer a NULL.

link_ptr (const T* ptr)
link_ptr (const link_ptr& ptr)

Costruttori di copia.
Il primo accetta un dumb pointer ad una Risorsa. Se la Risorsa ancora non possiede una catena, lo smart pointer che state costruendo diventa il 1° anello della catena della Risorsa. Se invece la Risorsa possiede già una catena, lo smart pointer che state costruendo viene aggiunto a questa catena. Logico, no?
Il secondo Costruttore accetta uno smart pointer. Lo smart pointer che state costruendo viene aggiunto alla catena alla quale appartiene lo smart pointer in argomento. Se lo smart pointer in argomento non punta ad alcuna Risorsa, allora finite per costruire uno smart pointer che non punta ad alcuna Risorsa. Anche qui è logico.

~link_ptr ()
Distruttore. Quando uno smart pointer cessa di esistere, il Distruttore invoca il metodo Null() per togliere lo smart pointer dalla catena alla quale appartiene (se alcuna). E' ovvio che se lo smart pointer che si sta distruggendo era l'ultimo a tenere in vita una Risorsa, il metodo Null() distruggerà la Risorsa. Ricorderete che la vostra Risorsa deve ereditare dalla classe chain_ptr. Questo implica che per distruggere una Risorsa e la relativa catena di puntatori, avete la facoltà di invocare una delete direttamente su un dumb pointer alla Risorsa. Il Distruttore scalare del chain_ptr distruggerà la catena di puntatori per voi.