L’assert, questa sconosciuta
L’assert è un sistema di controllo delle asserzioni. Di default il C definisce la macro nell’header assert.h, in C++ in cassert.h. La particolarità delle asserzioni è che vengono testate solo in fase di debug (cioè solo quando l’applicazione viene compilata in debug e poi eseguita) e spariscono senza aggiungere overhead quando compiliamo in release. L’assert di default controlla un’esplessione booleana passata come argomento e se questa è false scrive sullo stderr l’espressione che ha fallito, il file e la riga dove è avvenuto l’errore e successivamente viene chiamato abort() per terminare il programma.
Bisogna cerca di capire quando va usata un’assert e quando invece bisogna usare un’altro sistema di gestione degli errori (ad esempio ritornare un’eccezione, un valore noto, etc etc….). Sostanzialmente va usata un’assert tutte quelle volte che bisogna controllare una condizione che sicuramente NON deve assolutamente verificarsi. Se questo non è vero, ad esempio se prevediamo un pointer che può essere nullo, dobbiamo usare altre strategie come le eccezioni o semplicemente ritornare un particolare valore… E’ preferibile l’uso di assert, quando possibile, poiché, come già detto, non aggiunge overhead nella release finale e questo è una grandissimo vantaggio.
Un caso emblematico si verifica, ad esempio, in una funzione che restituisce l’elemento i-esimo di un vettore. Se l’elemento i-esimo non esiste? Questa è una condizione che non si deve mai verificare e quindi il metodo è un buon candidato per l’aggiunta di un’asserzione che controlli la consistenza del parametro di input (appunto l’indice del vettore). In codice:
{
// controllo che index sia maggiore di 0
// e che non superi da dimensione del vettore
assert(index >= 0 && index < v.size() - 1 );
return v[i];
}
Da notare che molte volte possiamo fare a meno dell’assert…ad esempio immaginiamoci un metodo che prende un pointer come parametro e NON vogliamo assolutamente che sia NULL:
Ora mettere un assert per controllare T è semplicemente brutto perché basta usare un reference al posto del pointer per impedire il passaggio di un riferimento nullo (come sappiamo i reference non possono essere “nulli”).
Sfortunatamente le assert di default non sempre soddisfano tutte le necessità. Ad esempio si potrebbe voler specificare, oltre all’espressione, anche un testo che indichi a parole cosa “è andato storto” o mettere a disposizione delle assert senza condizione cioè “assert(void);” uno statement che porta alla terminazione del programma cioè in sostanza un ramo “morto” dove non saremmo mai dovuti entrare (anche se è uno stile alquanto discutibile
).
Ad ogni modo basta definirsi in modo adeguato il nostro sistema di macro. Un esempio può essere il seguente (poco leggibile sul blog, me ne scuso):
#define std_assert(condition)
#define full_assert(condition, description)
#define fail_assert(description)
#else
#define std_assert(condition) \
{ \
do \
{ \
if(!(condition)) \
{ \
std::cerr << "ASSERT FAILED: " << #condition << " @ " << __FILE__ << " (" << __LINE__ << ")" << std::endl; \
__debugbreak(); \
} \
} while(0); \
}
#define full_assert(condition, description) \
{ \
do \
{ \
if(!(condition)) \
{ \
std::cerr << "ASSERT FAILED: " << #condition << " @ " << __FILE__ << " (" << __LINE__ << ")" << std::endl; \
std::cout << "DESCRIPTION: " << #description << std::endl; \
__debugbreak(); \
} \
} while(0); \
}
#define fail_assert(description) \
std::cerr << "ASSERT FAILED: " << "no condition" << " @ " << __FILE__ << " (" << __LINE__ << ")" << std::endl; \
std::cout << "DESCRIPTION: " << #description << std::endl; \
__debugbreak();
#endif
Per prima cosa viene controllato se siamo in debug o no (con la define _DEBUG). Se non siamo in debug le nostre definizione sono nulle, cioè non fanno niente [1] mentre in debug vengono definite.
Come potete vedere ho definito 3 tipi di assert [2]: std_assert, full_assert e fail_assert. La prima ha lo stesso comportamento dell’assert del C con la sola differenza che l’avvisso dell’errore viene scritto su stdout e non su stderr. La full_assert, oltre alla condizione prende anche un testo che serve per spiegare a parole umanamente comprensibili l’errore stesso dell’assert (per quale motivo è scattata). La fair_assert non ha condizione, scatta quando viene incontrata e serve per definire un percorso del codice “non consentito”. Tutte e tre al posto di abortire il programma terminano la fase di debug chiamando __debugbreak(); (questa è una call platform/compiler dipendent!)
Note: possiamo vedere un uso di ‘\’ a fine riga: servono per dichiarare una macro su più righe.
La macro __FILE__ viene rimpiazzata dal compilatore con il nome del file che sta processando, __LINE__ stessa cosa ma per il numero di riga.
Vediamo come usarle:
std_assert(condition);
full_assert(condition, "Qua va specificato il nostro messaggio personalizzato");
fail_assert("Qua va specificato il nostro messaggio personalizzato");
[1] Come in tutte le cose del C++, anche per questa ci sono mille teorie. L’assert di default in release è definita come ((void)0). In altre implementazioni ho visto sizeof(x) etc…io ho semplicemente definito la/le macro vuote e visualizzando l’assembly generato dal compilatore ho notato che non viene aggiunto nessun comando. Chi avesse maggiori informazioni su questo è il benvenuto.
[2] Avrei preferito avere un sola macro con 3 overload in base al numero di parametri passato ma questo nella definizione delle macro non è consentito.
Vi sono altre implementazioni di assert, come l’uso di funzioni inline al posto di macro, l’utilizzo, previa definizione, di opportune callbacks richiamate dall’assert, l’integrazione con il nostro sistema di log e così via….
Saluti!
ps non potevo lasciarvi senza qualche sano link:
Ottimo articolo di JP sulle nuove assert statiche del C++0x.
riferimenti:
http://cnicholson.net/2009/02/stupid-c-tricks-adventures-in-assert/
http://msinilo.pl/blog/?p=212
Ciao!
Rispondo alla [1]. Diciamo che c’è una differenza abissale fra una #define vuota ed una che contiene un’espressione inutile.
La prima può creare problemi quando viene usata all’interno o da altre macro.
Un esempio chiarificatore (che si basa sul tuo):
[sourcecode lang='cpp']
// not-empty defines
#define std_assert(condition) (void)0
#define full_assert(condition, description) (void)0
#define fail_assert(description) (void)0
// empty defines
#define std_assert_err(condition)
#define full_assert_err(condition, description)
#define fail_assert_err(description)
#define TEST_OK(cond) \
(cond) ? std_assert(cond) : fail_assert(cond)
#define TEST_ERR(cond) \
(cond) ? std_assert_err(cond) : fail_assert_err(cond)
#include
#include
using namespace std;
int main()
{
TEST_OK(1 > 2); // OK!
TEST_ERR(1 > 2); // FAIL! => it won’t compile
return EXIT_SUCCESS;
}
[/sourcecode]
La differenza fra le due è semplice: quella vuota non compila perchè un conto è un parametro superfluo ma presente, un conto è un parametro che manca proprio.
Per questo si sconsiglia di definire “semplici simboli” (cioè #define vuote) e si consiglia piuttosto di sfruttare trucchi come (void)0 o simili. Tanto le cose inutili sono “strippate” dal compilatore, che agisce in un secondo momento, dopo che il preprocessore ha finito!
Se ti interessa, posso scrivere un posticino per spiegare bene questa cosa e dove/perchè viene usata… ^^’
Ciau!
ahhhhh ora mi è chiaro ed era anche semplice, proprio non ci ho pensato
scrivi pure, leggerò con attenzione
grazie per la risposta: precisa e puntuale come sempre. Ciau!
[...] marzo 2010 jp Lascia un commento Passa ai commenti Qualche giorno fa il buon Martino ha scritto un bel post in cui presentava dell’uso delle asserzioni (assert) in C++, incluso il come ometterle in [...]
Puoi emulare le assert statiche anche con il vecchio standard in una qualche maniera i.e. #define static_assert(x) {fake_array[x?1:-1];}
Cosa più importante è decidere se gestire o meno il fallimento di un assert. Normalmente sono d’accordo che un assert esprima un fallimento catastrofico, ma in generale “in produzione” vedrai che non è così. L’assert è usata per segnalare a tutti che qualcosa deve essere fatta immediatamente, perchè i continous build e gli automated tests si rompono, ed il gioco (o progetto) crasha quando lo esegui. Ma visto che comunque in un team grosso, se si verifica un errore non è bello tener bloccati tutti per il tempo necessario a risolvere l’assert, è meglio comunque quando possibile renderle “skippabili”, magari disabilitando il sottosistema che non può andare avanti ma cercando comunque di tenere su quanto più possibile del gioco.