Trace – Tracciare il codice

Quando si scrive del codice, che sia un semplice programma o parte di un sistema più complesso, è utile inserire delle istruzioni che consentano di seguirne l’esecuzione in fase di debugging. La cosa più semplice che si possa fare è inserire delle stampe su standard error. Tuttavia, è utile consentire all’utente/sviluppatore di settare che tipo di informazioni stampare e in che dettaglio. Pertanto, tornerà utile una struttura contenente un insieme di interi, ognuno dei quali rappresentante il livello di informazioni da stampare per uno specifico aspetto del codice.

1
2
3
4
5
6
struct TraceLevels
{
    unsigned aspectOne;
    unsigned aspectTwo;
    ...
};

Naturalmente, in un contesto reale dei nomi più significativi saranno da preferire ai poco informativi aspectOne, aspectTwo, ....

Supponendo di avere una istanza traceLevels di TraceLevels a visibilità globale, le informazioni di trace possono essere stampate all’interno di opportuni blocchi if.

1
2
3
4
5
6
7
8
9
10
11
void
myFunction()
{
    if(traceLevels.aspectOne >= 1)
        cerr << "Entering in myFunction()" << endl;
 
    ...
 
    if(traceLevels.aspectOne >= 2)
        cerr << "End of function myFunction()" << endl;
}

Questo approccio però ha un grosso inconveniente: il codice di trace, utile esclusivamente per il debugging, sarà presente anche nella versione release (ovvero destinata all’utente finale) del programma. Per programmi in cui l’efficienza è cruciale questo inconveniente è inaccettabile.

Una soluzione elegante al problema può essere la definizione di una macro nello stile della macro assert. Questa è una macro che riceve una condizione booleana. Se il codice è compilato per debugging, la condizione viene verificata; se fallisce il programma termina. Tuttavia, se il codice è compilato per release, la macro viene sostituita con “niente”; questo significa che non ci sarà traccia della macro nel codice eseguibile, a tutto vantaggio delle performance.

Adottando una soluzione di questo tipo andremo a definire una macro trace che riceve oltre alle informazioni da stampare anche la categoria cui le informazioni appartengono e il livello minimo richiesto per stamparle. Se la compilazione è per debugging (quindi non è definita la macro NDEBUG), allora la macro viene sostituita con una istruzione if, altrimenti con “niente”. Visto che le informazioni possono essere rappresentate anche da dati, come il contenuto di specifiche variabili, la funzione fprintf è quella che più si adatta ai nostri scopi; in particolare, tornerà molto utile il passaggio di un numero arbitrario di argomenti. La macro può essere definita nel modo seguente.

1
2
3
4
5
6
7
#ifndef NDEBUG
#    define trace(type, level, msg, ...) 
        if(traceLevels.type >= level) 
            fprintf(stderr, msg, ##__VA_ARGS__)
#else
#    define trace(type, level, msg, ...)
#endif

L’utilizzo è molto semplice, come mostrato dal codice seguente.

1
2
3
4
5
6
7
8
9
10
void
myFunction(
    int arg1)
{
    trace(aspectOne, 1, "Entering myFunction()n  arg1: %dn", arg1);
 
    ...
 
    trace(aspectOne, 2, "End of function myFunction()n");
}

Leave a Reply

Your email address will not be published. Required fields are marked *