Programming Languages Hacks

Importanti regole per linguaggi di programmazione rilevanti come Java, C, C++, C#…

  • Subscribe

  • Lettori

    I miei lettori abituali

  • Twitter

Memory Leak con Garbage Collection: WeakReference

Posted by Ricibald on 27th November 2009

Come tutti sapete, la GC è una modalità automatica di gestione della memoria tramite cui vengono liberate porzioni non più referenziate. In questo modo il programmatore non si deve più preoccupare di deallocare esplicitamente gli oggetti e si evitano i tipici problemi legati alla gestione della memoria (dangling pointer e memory leak).

Se per memory leak intendiamo il mancato rilascio di memoria non più referenziata allora il GC effettivamente evita qualunque memory leak, ma se per memory leak intendiamo il mancato rilascio di memoria non più utilizzata, allora la GC non è sufficiente: non può conoscere le nostre intenzioni!!

Ora mi spiegherò meglio, ma è necessario prima notare una cosa importante: il problema che descriverò è generale per la GC, ma non significa che implementazioni più raffinate non abbiano risolto il problema. Infatti implementazioni C# o Java non prevedono questo problema, ma si noti anche che, per quanto raffinata sia l’implementazione della GC, questi errori non potranno mai essere completamente impediti, quindi è necessario un approccio consapevole alle tematiche.

Dunque mi spiego meglio: immaginiamo di avere un oggetto che rappresenta la struttura di una società. Avremo quindi una lista di dipendenti, di cui ognuno avrà un superiore (se esiste) e avrà dei dipendenti da coordinare (se non si trova all’ultimo gradino). Di seguito riporto un esempio di istanza dopo il suo utilizzo:

Come osserviamo l’applicazione dopo aver utilizzato la struttura, ora ha ormai perso il riferimento a qualunque dipendente. Sarebbe lecito aspettarsi che gli oggetti vengano deallocati in quanto non più utilizzati. Infatti “Riccardo” non viene più usato da nessuno e quindi deve essere deallocato sé stesso e tutti i dipendenti che referenzia in quanto a loro volta perdono il riferimento “Riccardo”.

Bene, questo non è così!!! I nostri dipendenti rimarranno allocati per sempre nella nostra applicazione, sebbene nessuno li utilizzi più! Questo può essere definito come vero e proprio memory leak, anche se devia leggermente dal significato originale.

La presenza di memory leak dipende da come realizziamo la classe “Dipendente”. Andiamo per gradi: nel seguente esempio NON avviene alcun memory leak:

public class Dipendente {
	public List<Dipendente> Subordinati = new List<Dipendente>();
}

Infatti se l’applicazione perde il riferimento ai dipendenti, il primo che verrà deallocato è il capo “Riccardo” poiché non ha più alcun riferimento. Successivamente verrà deallocata la List poiché non ha più riferimenti e in seguito verranno deallocati tutti e tre i dipendenti referenziati precedentemente dalla lista. Quindi in questo caso non avremo alcun memory leak. Osserviamo invece il seguente caso:

public class Dipendente {
    public Dipendente Superiore;
	public List<Dipendente> Subordinati = new List<Dipendente>();
	public Dipendente(Dipendente superiore) {
	   this.Superiore = superiore;
	}
}

In questo caso vogliamo mantenere per comodità anche un riferimento al superiore di ogni dipendente. Ma analizziamo stavolta cosa succede nel caso in cui l’applicazione non fa più riferimento ai dipendenti. Il capo “Riccardo” dovrebbe essere il primo candidato all’eliminazione, ma non può essere eliminato poiché i tre subordinati mantengono un riferimento. Viceversa, i tre subordinati non possono essere eliminati poiché il capo ne mantiene il riferimento tramite la lista. Questo significa che la struttura non verrà MAI deallocata!! Memory leak!! Una definizione più formale:

Groups of mutually referencing objects which are not directly referenced by other objects and are unreachable can thus become permanently resident; if an application continually generates such unreachable groups of unreachable objects this will have the effect of a memory leak. Weak references may be used to solve the problem of circular references if the reference cycles are avoided by using weak references for some of the references within the group.

In molti articoli come http://www.ibm.com/developerworks/library/j-leaks/index.html o http://blogs.msdn.com/davidklinems/archive/2005/11/16/493580.aspx spesso si risolvono questi problemi eliminando i riferimenti che causerebbero il memory leak adottando soluzioni del tipo:

public class Dipendente : IDisposable {
    public Dipendente Superiore;
	public List<Dipendente> Subordinati = new List<Dipendente>();
	public Dipendente(Dipendente superiore) {
	   this.Superiore = superiore;
	}
	
	public void Dispose() {
	   foreach(Dipendente subordinato in this.Subordinati) {
	       subordinato.Superiore = null;
	   }
	}
}

public class Application {
    static void main() {
	   using(Dipendente riccardo = new Dipendente(null)) {
	       riccardo.Superiori.Add(new Dipendente(riccardo));
		   riccardo.Superiori.Add(new Dipendente(riccardo));
		   riccardo.Superiori.Add(new Dipendente(riccardo));
	   } // esegue la dispose
	   // perde il riferimento
	}
}

Sinceramente non mi piace l’approccio di “ricordarsi” lo using: troverete ovunque forum del tipo “avete memory leak? La colpa è tua che non fai la dispose degli oggetti Drawing!”. Cosa?? La colpa è mia??? No, le cose non stanno così: chi scrive la libreria non deve caricare agli utilizzatori di dettagli che possono causare problemi!!

La soluzione esiste ed è molto semplice: bisogna pensare i riferimenti a oggetti in termini di POSSESSO (OWNERSHIP). Possiedo, e quindi ho diritto di vita e morte, dell’oggetto a cui mi sto riferendo? Se sì, utilizza un normale riferimento come hai sempre fatto (Strong Reference). Altrimenti usa un WeakReference. Cosa è un WeakReference? Semplicemente è un riferimento debole a un oggetto, che ha validità fin quando esistono Strong Reference che mantengono in vita l’oggetto. Risulta comodo vedere le cose in termini di composizione UML: tutte le composizioni sono Strong Reference, altrimenti sono WeakReference.

Si noti un aspetto importante: queste sono regole generali di buona programmazione che si applicano a prescindere dalla implementazione della GC. Infatti linguaggi come C# o Java la GC elimina riferimenti non più utilizzati dal contesto di esecuzione corrente: eventuali riferimenti circolari isolati saranno quindi eliminati. Questo significa quindi che riferimenti circolari causati nel contesto di esecuzione corrente provocano lo stesso identico problema descritto. Un esempio semplice? La nostra applicazione mantiene un riferimento all’ultimo studente selezionato, lo eliminiamo ma non verrà mai cancellato poiché mantenuto dalla app.

Per evitare quindi memory leak in modo corretto e senza impatti sugli utilizzatori della nostra classe dovremo scrivere:

[
public class Dipendente {
    private WeakReference<Dipendente> _superioreWeak;
    public Dipendente Superiore { get { return _superioreWeak.Target; } }
	public List<Dipendente> Subordinati = new List<Dipendente>();
	
	public Dipendente(Dipendente superiore) {
	   this._superioreWeak =  new WeakReference<Dipendente>(superiore);
	}
}

public class Application {
    static void main() {
	   Dipendente riccardo = new Dipendente(null);
	   riccardo.Superiori.Add(new Dipendente(riccardo));
	   riccardo.Superiori.Add(new Dipendente(riccardo));
	   riccardo.Superiori.Add(new Dipendente(riccardo));
	} // perde il riferimento
}

I WeakReference risultano quindi molto utili per impedire memory leak causati da riferimenti circolari. Utilizzi tipici sono situazioni composite con riferimento al padre o situazioni di caching, in cui tramite un weak hash map vengono memorizzati gli oggetti senza impedirne la deallocazione (altrimenti nella cache vivrebbero oggetti per sempre!!). Una buona regola è comunque utilizzare un tool per la rilevazione di memory leak, come CLR Profiler.

Posted in .net, design, java, performance | 6 Comments »

Polimorfismo dell’operatore == in C#

Posted by Ricibald on 3rd June 2008

Mi sono battuto in C# in un problema non di poco conto:

L’operatore == non è polimorfico

Questo significa che prende solo l’implementazione del tipo specifico (a differenza del C++, in cui l’operatore è polimorfico poiché non statico). Se quindi confrontiamo due istanze di tipo Object allora l’implementazione sarà quella di base fornita in Object. Se confrontiamo due istanze di tipo String allora l’implementazione sarà il confronto alfanumerico. Ma se eseguiamo il downcast verso Object di due String allora il confronto sarà solo tra puntatori, non tra contenuto.

Quindi l’invocazione dell’operatore non è polimorfica. Ma questo non significa che lo deve essere anche la relativa implementazione. Esiste infatti un workaround per simulare il polimorfismo negli operatori, implementando qualcosa come:

abstract class Base
{
    public static bool operator==(Base b1, Base b2)
    {
        if( object.ReferenceEquals( b1, b2 ) )
        {
            return true;
        }
        else if( object.ReferenceEquals( b1, null ) || 
                 object.ReferenceEquals( b2, null ) )
        {
            return false;
        }
        return b1.Equals(b2);
    }
}

Quindi eventuali classi che derivano da Base saranno sicure che la seguente invocazione darà comunque luogo a un’esecuzione corretta:

// Classe Derived estende Base
Derived d1 = new Derived();
Derived d2 = new Derived();
Base b1 = d1;
Base b2 = d2;
Object o1 = d1;
Object o2 = d2;

di == d2;       // polimorfico     (esegue Equals)
b1 == b2;       // polimorfico     (esegue Equals)
o1 == o2;       // non polimorfico (esegue ReferenceEquals)

Questo wokaround si basa sul fatto che, a differenza degli operatori, il metodo Equals è polimorfico.

Una trattazione ufficiale Microsoft sull’operatore == e sul metodo Equals è riportata qui.

Posted in .net | 3 Comments »

Il Problema della Non-Covarianza nei Generics in Java e C#

Posted by Ricibald on 11th April 2008

Java, C# e C++ non contemplano il concetto di covarianza:

Una classe C<T> si dice covariante rispetto a T se, date due classi A e B tali che B deriva da A, risulta anche che C<B> deriva da C<A>.

Significa quindi che questa istruzione solleva un’eccezione:

List<Persona> studenti;
List<Object> objects = (List<Object>) studenti; // Errore

Questo deriva da un motivo di logica Object Oriented, non strutturale del linguaggio. Infatti il cast verso “objects” altera il comportamento di “studenti”, che accetta come elementi della lista solo oggetti che derivano da “Studente”. Se fosse stato ammesso, ciclare la collezione “studenti” avrebbe potuto restituire oggetti anche non di tipo “Studente” che non è ammesso. In altre parole, il problema è che la covarianza consente di definire sottoclassi che restringono la classe base.

Il concetto di covarianza si ripercuote sull’utilizzo dei metodi. Ad esempio: immaginiamo il metodo di una nostra implementazione della classe List:

class List<T> {
    AddAll(List<T> l);
}

Immaginiamo di avere una classe Studente che deriva da Persona:

List<Studente> studenti = new List<Studente>();
List<Persona> persone = new List<Persona>();
studenti.AddAll(studenti); // Ok
studenti.AddAll(persone);  // Errore

Il codice funziona se introduciamo wildcard (Java) o naked constraint (C#):

// Java
class List<T> {
    addAll(List<? extends T> l);
}
// C#
class List<T> {
    AddAll<U>(List<U> l) where U : T;
}

Se invece vogliamo staccarci dalla genericità e trattare solo istanze di tipo “Object” osserviamo (come abbiamo già visto) che questo non compila:

List<Object> l = (List<Object>)persone; // Errore

Ma utilizzando wildcard (Java) o collezioni raw (C#) la classe compila:

List<?> l = persone; // Java
IList l = persone;   // C#

Da cui deriva che in C#, quando realizziamo una classe generica, è necessario fornire sempre un’implementazione non generica per supportare upcast non generici:

// C#
class List {
    Add(object item);
    AddAll(List l);
}
class List<T> : List {
    new Add(T item);
    new AddAll<U>(List<U> l) where U : T;
}

Si noti infine il differente approccio Java/C#:

  • Le librerie Java forniscono un supporto al problema dell’upcast:
    • I metodi sono già predisposti (si veda addAll, che accetta in input una Collection<? extends E>)
    • Il cast verso tipi raw è di default supportato (List<?>)
  • Le librerie C# non sono pensate per il supporto verso l’upcast:
    • I metodi non prevedono naked constraint (AddAll accetta in input solo una List<T>). I naked constraint sembrano più che altro un workaround non dichiarato
    • Il cast verso tipi raw è fornito solo se viene fornita un’implementazione non generica della classe (è quindi richiesto l’intervento implementativo dello sviluppatore)

Posted in .net, java | 3 Comments »

Eccezioni in C#: Critiche Progettuali

Posted by Ricibald on 23rd January 2008

In generale apprezzo come C# è stato concepito, ma una cosa che veramente non capisco è la gestione delle eccezioni.

In Java esiste la distinzione utilissima tra eccezioni checked e non. Le eccezioni checked consentono di forzare l’utilizzatore di un metodo a gestire o rilanciare solo le eccezioni dichiarate: niente di più, niente di meno. In C#, invece, tutte le eccezioni sono unchecked e questo pone il problema:

  • quali eccezioni devo gestire?
  • come forzare a sollevare solo determinati tipi di eccezione?

Si consideri infatti il codice Java:

public class Firefox {
      public void activatePlugins() {
            for(FirefoxPluginInterface plugin in this.plugins) {
                  try {
                        plugin.activate();
                  } catch (FirefoxPluginActivationException myEx) {
                        FirefoxHelper.manageActivationFromError(myEx);
                  } catch (Exception ex) {
                        FirefoxHelper.showError(ex);
                  }
           }
      }
}

public interface FirefoxPluginInterface {
     public void activate() throws FirefoxPluginActivationException;
}

Si consideri invece il codice C# analogo:

public class Firefox
{
      public void ActivatePlugins()
      {
            foreach(FirefoxPluginInterface plugin : _plugins)
            {
                  try
                  {
                        plugin.Activate();
                  }
                  catch (FirefoxPluginActivationException myEx)
                  {
                        FirefoxHelper.ManageActivationFromError(myEx);
                  }
                  catch (Exception ex)
                  {
                        FirefoxHelper.ShowError(ex);
                  }
           }
      }
}

public interface FirefoxPluginInterface {
     public void Activate();
}

Non siamo in grado di essere sicuri che tutte le persone che scriveranno plugin faranno buon uso della nostra eccezione. Speriamo in qualche modo nella bontà di chi scriverà… Una buona pezza era stata fornita nel framework 1.1 di .NET, inserendo la classe ApplicationException: in teoria l’utente che creava le proprie personali eccezioni doveva ereditare da questa classe, e non da Exception. In questo modo si era in grado di discriminare se l’eccezione apparteneva alle standard o se era una che avevamo lanciato noi. Successivamente, come riporta MSDN:

Per la maggior parte delle applicazioni, derivare le eccezioni personalizzate dalla classe Exception . L’idea iniziale di far derivare le eccezioni personalizzate dalla classe ApplicationException non ha prodotto alcun valore significativo in termini pratici.

Questa frase e questa organizzazione delle eccezioni mi lascia un po’ interdetto…

Esiste anche il problema della propagazione delle eccezioni. L’architettura di un sistema software è infatti normalmente definita a livelli o Layer: ogni livello rappresenta un diverso livello di astrazione. Il messaggio viene quindi passato attraverso vari livelli, in cui ad ogni livello eventuali errori devono essere riscritti a seconda del livello di astrazione. Ad esempio, un eccezione SQL al livello “Database” deve essere avvolta in una eccezione di livello più alto nel livello “Dominio” (ad es. una generica eccezione nella memorizzazione dei dati). In questa comunissima pratica ingegneristica abbiamo però alcuni problemi. Immaginiamo questa situazione:

try
{
}
catch (SQLException sqlEx)
{
    throw new PersistenceException("Errore nel salvataggio", sqlEx);
}
catch (Exception ex)
{
    throw new UnknownException("Errore sconosciuto durante il salvataggio nell'inserimento", ex);
}

L’eccezione PersistenceException viene ricatturata nel blocco “catch(Exception)”, comportamento assurdo in un codice Java, dove l’eccezione non sarebbe stata mai ricatturata. Per correggere questo comportamento si può eseguire un unico catch e il resto rilanciare:

try
{
}
catch (SQLException sqlEx)
{
    throw new PersistenceException("Errore nel salvataggio", sqlEx);
}
catch (Exception ex)
{
    // throw ex; NON UTILIZZARE: PERDO LO STACKTRACE!!
    throw; // OK!
}

Notare inoltre un altro problema: il “throw ex” causa la perdita di informazioni nello stacktrace: non parte più dall’origine dell’errore ma dall’origine del “throw ex”.

Realizzato il comportamento appena descritto nel codice, sarà il livello superiore che al momento della stampa del messaggio:

try
{
}
catch (Exception ex)
{
    string message = null;
    if(ex is PersistenceException) 
    {
        message = ex.Message;
    } 
    else if (ex is Exception)
    {
        message = "Errore sconosciuto durante il salvataggio nell'inserimento";
    }
    Trace.WriteLine(message);
}

Che cose articolate stranamente per realizzare un comportamento del tutto normale…

Posted in .net | 1 Comment »

Memorizzare Richieste e Risposte SOAP con le SoapExtension

Posted by Ricibald on 15th January 2008

In .NET le richieste e le risposte vengono sottoposte a wrapping e serializzate nella nostra comoda struttura ad oggetti, come visto in questo precedente post. Ma a volte è necessario proprio conoscere la richiesta SOAP “nuda e cruda”. Anche se ciò che stiamo richiedendo non è niente di apparentemente complicato, la cosa si rileva però molto più complicata del previsto.

Il ciclo di vita di un web service è mostrato nella figura seguente, preso dall’articolo di Microsoft:

Ciclo di vita di un web service

Le varie fasi possono essere intercettate utilizzando le Soap Extension. Normalmente vengono utilizzate per decorare il messaggio SOAP con cifratura o compressione, ma nel nostro caso saranno semplicemente utilizzate per memorizzare le richieste e risposte SOAP.

Innanzitutto è necessario creare una classe che estende dalla classe SoapExtension:

public class MessagesPrinterSoapExtension : SoapExtension
{
    public override object GetInitializer(Type obj)
    {
        return null;
    }

    public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attr)
    {
        return null;
    }

    public override void Initialize(object initializer)
    {
    }
}

Bisogna poi implementare i seguenti metodi:

private Stream _workingStream = null;
private Stream _originalStream = null;
private static string _soapRequestMessage = null;
private static string _soapResponseMessage = null;
private SoapMessageStage? _previousStage = null;

/// <summary>
/// Allows a SOAP extension access to the memory buffer containing the SOAP request or response
/// </summary>
/// <param name="stream">A memory buffer containing the SOAP request or response.</param>
/// <returns>New memory buffer that this SOAP extension can modify</returns>
public override Stream ChainStream(Stream originalStream)
{
    _originalStream = originalStream;
    _workingStream = new MemoryStream();
    bool isResponseMessage = _previousStage != null && _previousStage == SoapMessageStage.AfterSerialize;
    if (isResponseMessage) // response message
    {
        // Get the SOAP body response as a string and copy content in a seekable stream
        _soapResponseMessage = GetMessageAndCopyStream(_originalStream, _workingStream);
    }
    return _workingStream;
}

public override void ProcessMessage(SoapMessage message)
{
    // a SOAP message has 4 stages. We're interested in AfterSerialize
    if (message.Stage == SoapMessageStage.AfterSerialize)
    {
        // Get the SOAP body request as a string and copy content in a seekable stream
        _soapRequestMessage = GetMessageAndCopyStream(_workingStream, _originalStream);
    }
    _previousStage = message.Stage;
}

Che utilizzano il metodo di utilità seguente:

/// <summary>
/// Extracts the content from input and copies it in the specified stream
/// </summary>
private static string GetMessageAndCopyStream(Stream inStream, Stream outStream)
{
    string inStreamContent;

    // If allowed, rewind
    if (inStream.CanSeek)
    {
        inStream.Position = 0;
    }

    // Read content
    StreamReader inReader = new StreamReader(inStream);
    inStreamContent = inReader.ReadToEnd();

    // Copy to output
    StreamWriter writer = new StreamWriter(outStream);
    writer.Write(inStreamContent);
    writer.Flush();

    // If allowed, rewind
    if (outStream.CanSeek)
    {
        outStream.Position = 0;
    }
    return inStreamContent;
}

Ma ora viene naturale chiedersi: come fa il mio proxy verso il web service a sapere che esisto? Semplice: basta dichiarare la classe appena creata nel file di configurazione:

<configuration>
  <system.web>
    <webServices>
      <soapExtensionTypes>
        <add type="MyNamespace.MessagesPrinterSoapExtension,MyDll" priority="1" group="Low" />
      </soapExtensionTypes>
    </webServices>
  </system.web>
</configuration>

E’ tutto! Consigli, critiche?…

Posted in .net, web service | 10 Comments »

Memory Leak in C++ – Le Cinque (Semplici) Regole d’Oro Per Un Software “Leggero”

Posted by Ricibald on 30th December 2006

I Memory Leak avvengono quando aree di memoria non più utilizzabili rimangono in memoria. Tali errori derivano dal mancato rispetto della seguente regola:

ogni oggetto allocato nello heap – con new T/new T[]/malloc – deve essere esplicitamente deallocato – con delete/delete[]/free – secondo le regole di corrispondenza indicate con il delimitatore ‘/’

Infatti, lo scope di una area heap coincide con quella dell’ultimo puntatore che ne contiene l’indirizzo, ma il suo lifetime coincide con l’intera durata del programma a meno dell’invocazione di delete/delete[]/free.
Se non è stato invocato il corrispondente delete/delete[]/free e l’ultimo puntatore disponibile va out of scope, l’area non è più accessibile ma continua a occupare memoria inutilmente generando l’errore di memory leak.

Si nota subito quindi che il problema riguarda esclusivamente l’area heap. Nello stack questo non è possibile poiché il lifetime di una variabile allocata nello stack coincide con il suo scope: quando la variabile è out of scope viene automaticamente deallocata.

Innanzitutto la regola base è quindi quella di rispettare le corrispondenze di allocazione/deallocazione: il new T deve essere seguito dal delete, il new T[] deve essere seguito dal delete[], la mallocdeve essere seguito da una free.

Spesso la causa più frequente di memory leak è omettere di deallocare le risorse quando non sono più necessarie. Immaginiamo ad esempio di avere la classe factory WindowFactory che crea oggetti con tipo base Window (supponiamo quindi che esistano sottoclassi di Window):

class WindowFactory {
    public:
        static Window* createWindow();
}

la funzione createWindow() restituisce un puntatore a un oggetto allocato dinamicamente di tipo base Window. Ci ricordiamo che restituire un puntatore in questo contesto è necessario per evitare il problema dello slicing. Quindi chi utilizza una Window deve:

  1. creare la Window con createWindow()
  2. usare la Window
  3. cancellare la Window con delete

Questo comporta che l’utilizzatore di Window è responsabile di deallocare la Window. Forse è un rischio che non vogliamo correre… La soluzione consiste nel creare una classe che funge da resource manager:

class WindowManager {
    private:
        Window* w;
    public:
        WindowManager() {
            w = WindowFactory::createWindow();
        }
        Window* getWindow() {
            return w;
        }
        ~WindowManager() {
            delete w;
        }
}
class TestWindow {
    void disegna() {
        WindowManager m;
        Window* w = m.getWindow();
        /* utilizza w... */
    } /* ora m è out of scope: w è deallocata */
}

La classe WindowManager ha la sola funzione di avvolgere Window per garantirne la deallocazione. L’utilizzo di WindowManager viene spesso chiamato Resource Acquisition Is Initialization (RAII) poiché è un idioma consolidato in C++ il fatto che nella stessa espressione un oggetto venga contemporaneamente dichiarato e inizializzato. Le classi RAII potrebbero essere viste come:

“So che i memory leak derivano dalla differenza concettuale tra heap e stack. Sarebbe bello se tutti gli oggetti che l’utilizzatore deve creare rispettassero le semplici regole di visibilità dello stack! In questo modo sarebbe impossibile generare memory leak! Ma come ottenere questo? Semplice: creo classi RAII

Stavolta infatti l’utilizzatore finale non crea nulla nello heap. La deallocazione segue esclusivamente le semplici regole dello stack: la deallocazione della Window è basata solo sullo scope di WindowManager.

Se generalizziamo questo approccio attraverso i template otteniamo gli smart pointer. I più utilizzati sono i reference-counting smart pointer (RCSP), che tengono traccia del numero di puntatori alla risorsa gestita e permettono di garantire una garbage collection per una particolare risorsa. In C++ gli RCSP coincidono con std::tr1::shared_ptr<T>.

Tornando al nostro problema, ricordiamo che l’obbiettivo sarebbe quello di creare una nuova Window senza delegare la responsabilità di cancellare la Window all’utilizzatore. In parte l’abbiamo ottenuto tramite il WindowManager, ma l’utilizzatore potrebbe comunque invocare WindowFactory::createWindow() a proprio piacimento e il problema rimarrebbe.

Sarebbe comodo che la stessa funzione WindowFactory::createWindow() restituisse un “qualcosa” che rispetti l’idioma RAII. E’ possibile ottenere questo comportamento avvolgendo la Window in uno smart pointer RCSP:

typedef boost::shared_ptr<Window> WindowPtr;
class WindowFactory {
    public:
        static WindowPtr createWindow();
}
class TestWindow {
    void disegna() {
        WindowPtr wp = WindowFactory::createWindow();
        wp->onClose(Window::EXIT);
        wp->view();
    } /* ora wp è out of scope: wp è deallocato */
}

Utilizzare classi RAII si rileva l’approccio migliore anche per prevenire i memory leak causati da eccezioni. Ad esempio:


class TestWindow {
    private:
        Window* w;
        Image* i;
    void disegna() {
        w = WindowFactory::createWindow();
        w->view();
        /* La creazione di Image potrebbe sollevare una eccezione */
        i = new Image("/home/ricibald/image.png");
        w->setImage(i);
        w->refresh();
        w->close();
        delete w;
    }
}

Se il costruttore di Image solleva una eccezione allora la Window puntata da w non verrà mai deallocata! Se invece utilizziamo classi RAII, il comportamento sarà corretto:

typedef boost::shared_ptr<Window> WindowPtr;
class TestWindow {
    private:
        WindowPtr w;
        Image* i;
    void disegna() {
        wp = WindowFactory::createWindow();
        wp->view();
        /* La creazione di Image potrebbe sollevare una eccezione */
        i = new Image("/home/ricibald/image.png");
        wp->setImage(i);
        wp->refresh();
        wp->close();
    }
}

Se viene sollevata una (qualsiasi) eccezione, wp diventa out of scope e la deallocazione avviene correttamente. Viene infatti invocato il distruttore di shared_ptr che a sua volta invocherà il delete di w.

Rispettare la regola RAII consente di prevenire la maggior parte degli errori, ma esistono anche memory leak causati da comportamenti indefiniti del compilatore, pena possibili memory leak “molto sottili“.

Esistono tre possibili comportamenti indefiniti del compilatore.

Il primo comportamento indefinito si può verificare con gli smart pointer. Si consideri infatti il seguente codice:

processaFinestra(boost::shared_ptr<Window>(new Window),priority());

il compilatore deve eseguire le tre seguenti cose

  • Invocare priority
  • Eseguire “new Window
  • Invocare il costruttore di boost::shared_ptr

Ma l’ordine delle operazioni è indefinito! Chiaramente il costruttore di shared_ptr dovrà essere invocato dopo “new Window“, ma priority() può essere invocato per primo, secondo o terzo! Consideriamo questa possibile sequenza di operazioni:

  1. Eseguire “new Window
  2. Invocare priority
  3. Invocare il costruttore di boost::shared_ptr

Ma se la funzione priority() sollevasse un’eccezione? In questo caso il puntatore restituito da “new Window” sarebbe perduto e l’oggetto Window referenziato non verrebbe deallocato: memory leak!! Come evitare questo comportamenti anomalo? Semplice: basta memorizzare gli smart pointer in istruzioni isolate. L’esempio precedente diventa quindi:

boost::shared_ptr<Window> wp(new Window);
processaFinestra(wp,priority());

Il secondo comportamento indefinito si può verificare nella deallocazione di classi polimorfiche. In classi base polimorfiche è necessario dichiarare il distruttore virtual. Se non viene rispettata questa regola, il programma funzionerà lo stesso, ma il comportamento della deallocazione è indefinito. Tipicamente le sottoclassi non sono distrutte, scatenando seri memory leak.

Il terzo comportamento indefinito si può verificare se i distruttori sollevano eccezioni. Se ciò dovesse avvenire, potrebbero attivarsi più eccezioni contemporaneamente, e in tal caso il risultato è indefinito. Ad esempio:

  1. il mio distruttore di una classe “Test” cancella una lista “finestre” che contiene N oggetti “Finestra
  2. il distruttore della classe “Finestra potrebbe generare un’eccezione in caso di errori in chiusura
  3. la deallocazione della lista “finestre” potrebbe generare più eccezioni contemporaneamente
  4. il risultato è indefinito: i rimanenti oggetti Finestra vengono deallocati? Ho memory leak?

è logico che non ci possiamo basare su risultati indefiniti! Quindi quali soluzioni adottare? Tre possibilità

  1. termina il programma in modo anomalo con std::abort()
  2. gestisci l’eccezione nel distruttore con try-catch, ma questo non consente di intervenire sull’errore
  3. fornisci una normale funzione close() che (1) deallochi oggetti Finestra e (2) sollevi l’eventuale eccezione. Tale funzione deve essere invocata esplicitamente dall’utilizzatore di Finestra.
    Il distruttore di Finestra, invece, verifica se tale funzione è stata invocata manualmente dall’utente (tramite una variabile booleana closed). Se la funzione close() non è stata ancora invocata, il distruttore garantisce comunque l’esecuzione di close(), con la differenza che stavolta l’ eventuale eccezione di close() deve essere gestita all’interno del costruttore, in modo analogo al precedente punto.
    Tutto questo si traduce in:

    class WindowException: public exception {
        virtual const char* what() const {
            return "Errore nella chiusura della finestra";
        }
    }
    class Finestra {
        private:
            Window w;
            boolean closed;
        public:
            ...
            void close() throw(WindowException) {
                w.close();
                if(!w.closed()) {
                    throw WindowException();
                }
                closed = true;
            }
            ~Finestra() {
                if( !closed ) {
                    try {
                        w.close();
                    } catch(WindowException& e) {
                        cerr << e.what << endl;
                        /* Se necessario esegui std::abort() */
                    }
                }
            }
    }
    

Concludendo, per prevenire (o addirittura impedire) memory leak bisogna rispettare le seguenti regole:

  • rispettare le corrispondenze: new T/new T[]/malloc corrisponde a delete/delete[]/free
  • restituire all’utilizzatore solo classi RAII tramite smart pointer o resource manager creati appositamente
  • creare smart pointer in istruzioni isolate
  • dicharare virtual i distruttori di classi polimorfiche
  • costruire distruttori privi di eccezioni. In caso di errori da gestire occorre fornire una funzione regolare che dia la possibilità di intervenire sull’eventuale eccezione (il comportamento del distruttore deve essere comunque garantito in caso di mancata invocazione di tale funzione regolare)

Questo lavoro è basato soprattutto sul libro di Scott Meyers “Effective C++ – Third Edition”.

Un’ altro riferimento molto interessante è l’articolo di George Belotsky.

Posted in c++, performance | 6 Comments »