martedì 18 settembre 2012

Programmazione ad oggetti in C (ereditarieta')

Qualche giorno fa ho ricevuto un commento via e-mail ad uno dei primi post su questo blog, quello relativo all'utilizzo di casting fra i puntatori per simulare la programmazione OOP in linguaggio C.
Il commento in questione era relativo all'introduzione di uno o piu' metodi pubblici nella classe, che facendo riferimento a due strutture in overlapping, poteva risultare impossibile.
Per comprendere bene come procedere occorre anzitutto considerare la struttura stessa del sistema:
  • il fatto che esistano due strutture, una privata e una pubblica, non significa che vi siano due "istanze" separate: si deve pensare alla parte pubblica come al file header di una classe C++ e alla struttura privata come alla sua implementazione;
  • l'introduzione di un metodo pubblico, come pure di un qualsiasi altro attributo, e' una operazione che non va fatta nella classe base, ma in eventuali sottoclassi. Se venisse infatti fatta sulla classe base si farebbe una "banale" ristrutturazione della classe stessa, che a meno di ovvi problemi di compatibilita' binaria, continuerebbe a funzionare come prima.

Ho quindi deciso di estendere l'esempio del conto corrente creando una sottoclasse, chiamata ContoCorrenteConCarta (nei file CCX_pub.h e CCX_private.c) che estende le informazioni di base del conto corrente aggiungendo una carta di credito. Ho anche colto l'occasione per estendere il conto corrente stesso con alcune ovvie informazioni che avevo tralasciato, come ad esempio il titolare del conto.
Tutti i sorgenti nella loro nuova versione sono disponibili su uno dei miei repository su github.

Come per la classe conto corrente di base, la classe con carta di credito deve definire due strutture sovrapposte, una privata che contiene l'implementazione e una pubblica che contiene l'interfaccia.
La parte pubblica e' definita come:


typedef struct CCX_PUB {

  // riferimento ad un conto corrente normale
  ContoCorrentePub* super;

  // metodo che ritorna il numero della carta di credito
  char* (*m_numero_carta)( struct CCX_PUB * );

} ContoCorrenteConCartaPub;



Si noti che la struttura contiene un puntatore alla sua superclasse, o meglio ad una istanza della superclasse. Il puntatore e' stato volutamente denominato "super" per meglio indicare il legame con una istanza della superclasse.
La parte implementativa risulta come segue:


typedef struct CCX_private{
 
  // riferimento alla parte pubblica
  ContoCorrenteConCartaPub pub;

  /*------ dati nascosti ------------*/

 
  // numero della carta di credito
  char* numero_carta;
 
} ContoCorrenteConCartaPrivate;



dove, come al solito, il primo campo e' una variabile di tipo "pubblico" per fare in modo che il casting di puntatori fra ContoCorrenteConCartaPub e ContoCorrenteConCartaPrivate possa funzionare come nell'esempio del conto corrente semplice.
Al solito si definiscono dei metodi di conversione da parte pubblica a privata per il nuovo tipo di conto corrente. Occorre anche definire un metodo di inizializzazione, che ad esempio puo' essere come il seguente:


ContoCorrenteConCartaPub* abilitaCarta( ContoCorrentePub* contoNormale, char* numero_carta ){

  // controlli di sicurezza
  if( contoNormale == NULL || numero_carta == NULL )
    return NULL;


  printf("\n\t[abilitaCarta] Abilitazione carta %s su conto %s",
         numero_carta,
         contoNormale->m_titolare( contoNormale ) );

  // costruisco un nuovo conto corrente esteso
  ContoCorrenteConCartaPrivate *ccpriv = malloc( sizeof( ContoCorrenteConCartaPrivate ) );


 
 
  // inserisco il conto normale nella struttura pubblica
  ContoCorrentePub* superClass = aperturaContoCorrente( 

                          contoNormale->m_numero_conto( contoNormale ),                           contoNormale->m_titolare( contoNormale )
                              );
  ccpriv->pub.super = superClass;

  // Metodo getter per la carta di credito
  ccpriv->pub.m_numero_carta = getNumeroCartaDiCredito;

  // inserisco il numero di carta nella struttura privata
  ccpriv->numero_carta = numero_carta;

  // oggetto costruito!
  return Private2Public( ccpriv );
 

}




Si noti che il metodo abilitaCarta funge da costruttore di copia: dato un conto corrente normale (senza carta), si costruisce un oggetto con carta di credito "attorno" a un clone del conto corrente.
Questa operazione di copia e' una pura scelta implementativa, sarebbe stato possibile usare il conto passato come argomento invece che crearne un clone. Si noti comunque che il clone ha dei puntatori alla stessa area dati (es. il titolare), e quindi questo costruttore effettua una shallow copy. Ovviamente questa implementazione non raffinata e' per mantenere l'esempio semplice.
L'uso della parola "superClass" all'interno del metodo non e' a caso: il conto normale di partenza contiene infatti i dati (e i metodi) che faranno si che il conto carta di credito possa essere trattato anche come conto normale. Inoltre il conto con carta prevede una variabile di tipo conto corrente normale al suo interno, creando quindi un link fra le due implementazioni che e' appunto il legame di ereditarieta'.
Volendo essere piu' precisi si dovrebbe sostituire il nome "superClass" con "super", per seguire una terminologia Java, ad ogni modo e' il concetto che conta.

Alla fine della costruzione si ha quindi un conto corrente con carta di credito che contiene un link ad un conto normale, inglobato in esso, e gli attributi aggiuntivi definiti nell'estensione della classe stessa.
L'utilizzo del nuovo tipo di conto e' semplice, ad esempio:


        printf( "\nCostruzione di un conto con carta di credito\n");
        ContoCorrenteConCartaPub *ccCarta = abilitaCarta( C1, "123456789" );

        ContoCorrentePub* ccpub = ccCarta->contoCorrente;
        printf( "\nCarta di credito per il conto %s = %s\n",
                ccCarta->super->m_titolare( ccCarta->super ),
                ccCarta->m_numero_carta( ccCarta ) );



Si noti che e' possibile "castare" il conto corrente con carta ad uno senza (ossia alla superclasse), accedendo al campo "super" della sua interfaccia.
Ne consegue che le due righe seguenti sono equivalenti:


ccCarta->super->m_titolare( ccCarta->super );
ccpub->m_titolare( ccpub );



E per il polimorfismo?
Avere un metodo polimorfico significa, lato pratico, che il puntato alla funzione contenuta nella implementazione (della superclasse) sia modificato.
In realtà la cosa e' leggermente piu' complessa: non deve essere infatti modificato il puntatore nella sueprclasse, ma un nuovo puntatore nella sottoclasse deve essere introdotto.
Infatti rimuovere/modificare il puntatore nella superclasse significa perdere ogni possibile legame con il metodo ridefinito, cosa che invece puo' tornare utile ad esempio se si deve invocare tale metodo.
Per semplicita' si supponga di voler sovrascrivere il metodo che fornisce il titolare di un conto corrente, in modo che nel caso ci sia una carta di credito venga stmapato il nome del titolare e il numero della carta.
La struttura pubblica del nuovo conto corrente con carta di credito viene quindi modificata come segue:


typedef struct CCX_PUB {

  // riferimento ad un conto corrente normale
  ContoCorrentePub* super;

  // metodo che ritorna il numero della carta di credito
  char* (*m_numero_carta)( struct CCX_PUB * );

  // overriding del metodo titolare di ContoCorrentePub
  char* (*m_titolare)( struct CCX_PUB * );

} ContoCorrenteConCartaPub;




La definizione del nuovo metodo e' la seguente, da inserire nella implementazione di ContoCorrenteConCartaPrivare:


static
char*
getTitolare( ContoCorrenteConCartaPub *ccpub ){
  if( ccpub == NULL )
    return NULL;

  ContoCorrenteConCartaPrivate *ccpriv = Public2Private( ccpub );
  ContoCorrentePub *super              = ccpub->super;
  char* titolare_super = super->m_titolare( super );
  char* titolare_desc  = malloc( sizeof( char ) * strlen( titolare_super ) * 2 );
  sprintf( titolare_desc, "%s (%s)", titolare_super, ccpriv->numero_carta );
  return titolare_desc;
}




Come si puo' notare il metodo combina della logica legata solo al conto con carta di credito a della logica della superclasse, andando infatti ad invocare il metodo della superclasse.
Il resto del codice viene modificato di conseguenza per inserire il nuovo puntatore a funzione e utilizzare il nuovo metodo.

Occorre fare una riflessione sul meccanismo del polimorfismo sopra descritto.
Anzitutto si noti come cambia l'argomento del metodo, che a seconda della posizione in cui si trova nella catena di ereditarieta' accetta un puntatore alla istanza di classe stessa. Questo e' anche il metodo con il quale molti linguaggi OOP implementano le chiamate di metodo.
Tuttavia questo rudimentale meccanismo non permette di effettuare una vera chiamata polimorfica: se si usa un puntatore alla superclasse (ContoCorrentePub) e si invoca il metodo si otterra' l'esecuzione del metodo della superclasse.
Affinche' anche la superclasse invochi il metodo definito per ultimo nella catena di ereditarieta' occorre modificare il puntatore alla funzione m_titolare di ContoCorrentePub affinche' punti alla nuova implementazione. All'interno del metodo si dovra' poi fare il cast esplicito da ContoCorrentePub a ContoCorrenteConCartaPub.
In questo modo pero' si rischia di perdere il legame al metodo precedente, e quindi di non poterlo piu' invocare. E' quindi compito della implementazione della sottoclasse mantenere i puntatori necessari e fare gli aggiustamenti ai pnntatori della superclasse stessa.

domenica 2 settembre 2012

PGDay 2012: e sono 6!

C'è fermento per l'organizzazione del PGDay 2012, il sesto da quando abbiamo dato vita a questo evento.


PGDay 2012 - Scopri il più avanzato database open-source

offsetof

Il linguaggio C, attraverso l'aritmetica dei puntatori, permette agli sviluppatori di passare da un tipo di dato ad un altro in maniera incontrollata. Solitamente solo gli sviluppatori virtuosi si applicano nella "artimetica dei puntatori". L'aritmetica dei puntatori puo' poi essere usata per fare una sorta di "introspezione", consentendo ad uno sviluppatore di estrarre da un puntatore ad un dato complesso (es. struct) un riferimento ad un singolo dato e viceversa. Per venire incontro agli sviluppatori, molti compilatori supportano delle  istruzioni speciali, come ad esempio __builtin_offsetof di GCC, e la libreria C fornisce dei wrapper per queste funzioni. Uno di questi wrapper, molto interessante, e' appunto offsetof, che consente dato un tipo di struttura e il nome di un campo, di trovare l'offset del campo all'interno della struttura. Ad esempio, considerando la struttura seguente:


struct three_chars {
       char a;
       char b;
       char c;
}


e volendo recuperare l'offset del campo "b" si puo' scrivere una cosa come segue:

offset = offsetof( struct three_chars, b );





che ritornera' il numero di bytes da sommare ad un puntatore alla base della struttura per ottenere l'indirizzo di memoria ove risiede "b". La macro offsetof e' indubbiamente comoda, ma risultati analoghi possono essere ricavati anche con l'analisi delle strutture e l'aritmetica dei puntatori; tuttavia lo sforzo richiesto e' maggiore.
Per meglio comprendere come si possa usare l'approccio con e senza offsetof ho creato alcuni semplici programmi che illustrano il funzionamento dell'aritmetica dei puntatori. L'idea alla base di questi programmi e' quella di avere l'annidamento di alcune strutture, un header e un pacchetto. Esistono due versioni di pacchetto: una con header all'inizio e una con header alla fine. Lo scopo delle due implementazione e' di mostrare come cambi l'aritmetica dei puntatori nei due casi. Le strutture dati sono molto semplici e definite come segue:


struct header {
 
  /**
   * An header name, just to print something out.
   */
  char* h_name;
 
  /**
   * A version, just to simulate some real header data.
   */
  int h_version;

};


struct packet_with_header_at_top {
 
  /**
   * The packet header.
   */
  struct header p_header;
 
 
  /**
   * A name for this packet, just to print out something.
   */
  char* p_name;
 
  /**
   * An integer to simulate some real packet data.
   */
  int p_data;
 

};



struct packet_with_header_at_bottom {
 
 
  /**
   * A name for this packet, 

   * just to print out something.
   */
  char* p_name;
 
  /**
   * An integer to simulate some real packet data.
   */
  int p_data;

  /**
   * The packet header.
   */
  struct header p_header;
 
};


Si supponga di inizializzare strutture e relativi puntatori come segue:


  // create an header
  struct header my_header = {
    .h_name    = "HEADER",
    .h_version = 1
  };
  

  struct packet_with_header_at_top my_packet_top = {
    .p_header = my_header,
    .p_name   = "PACKET WITH HEADER AT TOP",
    .p_data   = 99
  };


  struct packet_with_header_at_bottom my_packet_bottom = {
    .p_header = my_header,
    .p_name   = "PACKET WITH HEADER AT BOTTOM",
    .p_data   = 999
  };

  struct packet_with_header_at_top*    top_pointer    

                     = &my_packet_top;
  struct packet_with_header_at_bottom* bottom_pointer 

                     = &my_packet_bottom;
  struct header*                       header_pointer 

                     = &my_header;




Si consideri prima il caso privo di supporto offsetof: avendo inizializzato le due strutture di pacchetto e avendo i relativi puntatori, per estrarre l'header dai pacchetti occorre considerare il layout delle rispettive strutture pacchetto.
Nel caso di packet_with_header_at_top, essendo l'header il primo campo utile della struttura pacchetto, un puntatore al pacchetto e' automaticamente anche un puntatore all'header, e quindi non occorre applicare nessuna matematica dei puntatori:


  // cast the pointer to the packet to a pointer to the header
  header_pointer = top_pointer;
  printf( "\nHeader name and data extracted from the packet: %s - %d", 

            header_pointer->h_name, 
            header_pointer->h_version );
  // cast back from header to packet
  top_pointer = ( struct packet_with_header_at_top* ) header_pointer;
  printf( "\n Name of the packet with header at top:    %s", 

            top_pointer->p_name );


Nel caso di packet_with_header_at_bottom il puntatore alla struttura punta alla "base" della struttura, mentre l'header si trova ad un certo spiazzamento da questo. Occorre quindi calcolare manualmente l'offset considerando le dimensioni dei campi che precedono p_header nella struttura (e allineare l'offset stesso al byte):


  // compute the supposed offset depending on the fields that are before
  // the p_header
  offset = sizeof( int ) + sizeof( char* );

  // align the offset to 1 byte
  offset = ( offset % sizeof( char* ) == 0 ? offset 

:      ceil( ( (double) offset / (double) sizeof( char* ) ) ) 
            * sizeof( char* ) );

  // move the pointer to the offset
  header_pointer = ( (char*) bottom_pointer ) + offset;

  printf( "\nHeader name and data extracted from the packet: %s - %d",

           header_pointer->h_name, 
           header_pointer->h_version );

  // cast back from header to packet using again the offset
  bottom_pointer = ( struct packet_with_header_at_bottom* ) 

                   ( (char*) header_pointer - offset );
  printf( "\n Name of the packet with header at top:    %s",

           bottom_pointer->p_name );


Avendo quindi conoscenza del layout della struttura e' quindi possibile ottenere puntatori ai membri interni e, da questi, alla struttura stessa.
Tuttavia questo approccio non e' pienamente portabile, poiche' appunto prevede il calcolo manuale basato sul layout della struttura container.
E' qui che entra in gioco offsetof, che non richiede una conoscenza puntuale della struttura container; di seguito il caso relativo a packet_with_header_at_bottom (l'altro caso e' analogo):


  offset = offsetof( struct packet_with_header_at_bottom, p_header );
  printf( "\nOffset computed is %d", offset );

  // move the pointer to the offset
  header_pointer = ( (char*) bottom_pointer ) 

                   + offset;

  printf( "\nHeader name and data extracted from the packet: %s - %d",

           header_pointer->h_name, 
           header_pointer->h_version );

  // cast back from header to packet using again the offset
  bottom_pointer = ( struct packet_with_header_at_bottom* ) 

                  ( (char*) header_pointer - offset );
  printf( "\n Name of the packet with header at top:    %s",

            bottom_pointer->p_name );


Il funzionamento e' identico al caso manuale, ma qui non compare da nessuna parte l'elenco dei campi che precedono p_header, e quindi lo sviluppatore non e' costretto a ragionare sul layout della struttura container.
E' possibile implementare una sorta di "offsetof del poveraccio" usando ancora l'aritmetica dei puntatori e definendo una macro come la seguente:


#define custom_offset_of( struct_pointer, struct_member ) \
  ( (size_t) 

      ( (char*) &( (struct_pointer*) NULL )->struct_member 
       - (char*) NULL ) )


L'idea della macro e' abbastanza semplice e puo' essere riassunta nei seguenti passi:

1) si converte il valore NULL ad un puntatore alla struttura del tipo specificato, ossia si considera di avere una struttura che ha indirizzo di base 0: (struct_pointer*) NULL )

2) si chiede di accedere al campo specificato mediante operatore freccia, e si prende l'indirizzo del campo mediante operatore &. In altre parole si calcola dove risiede l'indirizzo di memoria del campo se la struct avesse indirizzo di base 0: &( (struct_pointer*) NULL )->struct_member

3) si sottrae dall'indirizzo di memoria del membro il valore di un puntatore a NULL, e quindi si ricava di quanti byte ci si deve spostare per arrivare al campo: 
&( (struct_pointer*) NULL )->struct_member - (char*) NULL )
4) si restituisce il valore trovato come size_t.

Quanto appena visto viene usato in modo pesante all'interno delle strutture dati del kernel, come ad esempio le liste (semplici e doppie).