Supponiamo di implementare un semplice tipo di dato stringa che provvederemo in seguito ad includere in un set. Per una nuova stringa allocheremo un buffer dinamico che ne contenga il testo. Quando la stringa verrà cancellata, dovremo liberare il buffer.
new() si occupa di creare un oggetto e delete() libera le risorse occupate dall'oggetto. new() pertanto conosce quale tipo di oggetto deve essere creato, perchè conosce la descrizione dell'oggetto come primo parametro. Basandoci sul parametro, potremmo usare una catena di istruzioni if per ogni singola creazione. Il problema principale con new() è che dovrebbe contenere il codice esplicito per ogni tipo di dato che decidiamo di voler supportare.
delete() presenta un problema ancora più complesso. Anch'essa deve contenere del codice esplicito per ogni tipo di dato dell'oggetto da cancellare: per una stringa il buffer di testo deve essere liberato; per un oggetto come descritto nel capitolo 1 solo l'oggetto stesso deve essere eliminato; e un set può aver acquisito vari spezzoni di memoria per memorizzare i riferimenti agli elementi che contiene.
Dovremmo dare a delete() un altro parametro: potrebbe essere il nostro descrittore di tipo oppure la funzione stessa che realizza l'eliminazione, ma questo approccio non è esente da errori. C'è, a dire il vero, una soluzione più generale e allo stesso tempo più elegante: ogni oggetto potrebbe conoscere come "distruggere" le proprie risorse.
Una parte di ogni oggetto (e tutti gli oggetti dovranno avere questa parte) sarà un puntatore con il quale potremmo identificare la funzione di "pulizia" ed eliminazione. Questa funzione verrà chiamata in seguito distruttore dell'oggetto.
Ora con new() avremmo un problema. new() è responsabile della creazione di oggetti e della restituzione dei puntatori che possono essere quindi passati a delete(), in altre parole new() deve installare l'informazione sul distruttore in ogni oggetto. La soluzione ovvia al problema è fare sì che un puntatore alla parte relativa al distruttore della descrizione di tipo sia passata a new(). Avremo bisogno sostanzialmente di qualcosa come le seguenti dichiarazioni:
struct type { size_t size; /* size of an object */ void (* dtor) (void *); /* destructor */ }; struct String { char * text; /* dynamic string */ const void * destroy; /* locate destructor */ }; struct Set { ... information ... const void * destroy; /* locate destructor */ };
Ecco quindi che avremo un altro problema: qualcuno potrebbe avere la necessità di copiare il puntatore al distruttore dtor dalla descrizione di tipo a destroy nel nuovo oggetto e la copia potrebbe essere inserita in una posizione differente in ogni classe di oggetti.
L'inizializzazione è una parte di lavoro svolta da new() e tipi diversi necessitano di inizializzazioni diverse - new() potrebbe pertanto richiedere argomenti diversi per tipi diversi:
new(Set); /* make a set */ new(String, "text"); /* make a string */
Per l'inizializzazione useremo quindi una funzione specifica a seconda del tipo che chiameremo in seguito costruttore. Poichè i costruttori e i distruttori sono specifici per tipo e non variano, li passeremo entrambi a new() come parte della descrizione del tipo.
Da notare che costruttori e distruttori non sono responsabili della allocazione e della deallocazione della memoria per l'oggetto stesso - questo lavoro è svolto infatti da new() e delete(). Il costruttore chiamato da new() è il solo responsabile dell'inizializzazione dell'area di memoria allocata da new(). Per una stringa, ad esempio, significa acquisire un ulteriore porzione di memoria per memorizzare il testo, ma lo spazio per struct String è allocato di per sè da new(). Questo spazio verrà liberato da delete(). Prima di ciò, delete() chiamerà il distruttore che provvederà a "invertire" l'inizializzazione fatta dal costruttore. Poi delete() ricicla l'area di memoria allocata da new().