In questo articolo scopriamo come implementare un tipo di dato opaco in C.
Prima di mettere mano al codice, però, dovremo capire di cosa si tratta e a cosa può servire!

ginger cat 650545 640

CHE COS'E' UN TIPO DI DATO OPACO?

Innanzitutto vediamo che cos'è un tipo di dato opaco.

Un tipo di dato opaco è un tipo di dato la cui reale struttura non è esposta nell'interfaccia.

Si tratta, in parole povere, di una struttura di cui non vengono forniti dettagli sui campi e, di conseguenza, l'accesso alle informazioni può avvenire solo tramite l'utilizzo di una serie di funzioni, ovvero di un'interfaccia ben delimitata.

Un esempio può essere il tipo FILE presente nella libreria standard del linguaggio C: si tratta di un puntatore ad una struttura di cui non conosciamo il contenuto (e non abbiamo bisogno di conoscerlo!). Quando apriamo un file con fopen() questa funzione ci ritorna un puntatore che deve essere passato alle successive chiamate (fwrite(), fread(), fclose() ...): l'utilizzatore non deve preoccuparsi in nessun modo di conoscere il contenuto puntato da FILE.

A COSA PUO' SERVIRE?

Teoricamente parlando, questa tecnica implementa il concetto di "information hiding" perchè permette di nascondere all'utilizzatore di una libreria i dettagli del funzionamento della stessa. Nella pratica permette di evitare che l'utilizzatore della libreria acceda impropriamente ai campi della struttura, che rimangono pertanto strettamente ad uso e consumo della libreria, permettendo così allo sviluppatore di modificare la struttura stessa a seconda delle esigenze.
Personalmente ho utilizzato questa tecnica in una situazione in cui una mia libreria doveva eseguire una serie di operazioni parziali, per le quali avevo la necessità di salvaguardare i risultati intermedi; la soluzione fu proprio quella di implementare un tipo di dato opaco. L'alternativa sarebbe stata quella di utilizzare delle variabili globali rendendo però il codice non rientrante (vedi https://it.wikipedia.org/wiki/Codice_rientrante).

IMPLEMENTAZIONE

Passiamo ora a creare il nuovo tipo MioTipo (scusate il nome davvero poco originale!): qui sotto ci sono il file header con l'interfaccia (miotipo.h), il file con l'implementazione del tipo (miotipo.c) ed un esempio d'uso.

Il file header miotipo.h deve solo dichiarare la struttura, senza specificare i campi contenuti.
Qui sotto ho dichiarato MioTipo con una typedef della struttura: alcuni preferiscono fare il typedef del puntatore alla struttura, in modo che nei prototipi non ci sia bisogno di utilizzare l'asterisco... questione di punti di vista: io preferisco non nascondere all'utilizzatore il fatto che si tratti di un puntatore.
Inoltre, nella mia libreria non avevo nemmeno dichiarato il typedef perchè nei prototipi avevo semplicemente utilizzato un puntatore a void (void *); non cambia niente, sempre di puntatore si tratta, è un'alternativa sicuramente meno elegante ma che permette di non dover dare un nome alla struttura!

struct MioTipoStr;

typedef struct MioTipoStr MioTipo;

extern MioTipo *createMioTipo(void);
extern void freeMioTipo(MioTipo *mt);
extern void processA(MioTipo* mt);
extern void processB(MioTipo* mt);
extern void processC(MioTipo* mt);

Nel file miotipo.c troviamo invece l'implementazione dei vari metodi: da notare che qui c'è bisogno di conoscere il dettaglio dei campi per cui:
- se si vuole utilizzare lo stesso header miotipo.h basta dichiarare la struttura prima della include (come nell'esempio qui sotto)
- in alternativa si può comporre uno header diverso per il proprio sorgente, contenente le dichiarazioni complete.
Infine, se si usa un void * bisogna ricordarsi di eseguire i cast opportuni.

#include <stdio.h>
#include <stdlib.h>

struct MioTipoStr
{
int x;
int y;
int z;
};

#include "miotipo.h"

MioTipo *createMioTipo(void)
{
MioTipo* ptr;

ptr = (MioTipo *)malloc(sizeof(MioTipo));
if (ptr)
{
ptr->x = 1;
ptr->y = 0;
ptr->z = 0;
}

return ptr;
}

void freeMioTipo(MioTipo *mt)
{
if (mt)
free(mt);
}

void processA(MioTipo* mt)
{
if (mt)
mt->x = mt->x + 1;
}

void processB(MioTipo* mt)
{
if (mt)
mt->x = mt->x * 2;
}

void processC(MioTipo* mt)
{
if (mt)
mt->x = mt->x - 1;
}

Il codice che segue è un esempio d'uso. Da notare che qui è possibile definire solo un puntatore a MioTipo; definire una variable MioTipo porta ad un errore del tipo "incomplete type is not allowed" (il messaggio che otterrete se provate con Visual Studio) in quanto il compilatore non può sapere quanta memoria "allocare" per questa variabile.

#include <stdio.h>
#include "miotipo.h"

int main(void)
{
//MioTipo mt; // errore "incomplete type is not allowed"
MioTipo *pmt;

pmt = createMioTipo();
if (pmt)
{
processA(pmt);
processB(pmt);
processC(pmt);

freeMioTipo(pmt);
}

return 0;
}

CONCLUSIONI

Naturalmente l'esempio proposto in questo articolo è privo di utilità ma ci ha permesso di dimostrare le potenzialità di questa tecnica.
L'utilizzatore della libreria non può interpretare i dati puntati da pmt e quindi è costretto a passare tramite le funzioni di interfaccia: dall'altra parte, la libreria può salvaguardare i risultati intermedi di processA(), processB() e processC() senza dover far ricorso a variabili globali o ad altre soluzioni più complicate.

FONTI

- https://en.wikipedia.org/wiki/Opaque_data_type
- http://www.programmerfish.com/how-to-create-an-opaque-data-type-in-c-cpp/