2010-04-03 6 views
12

Ho implementato un ostream per l'output di debug che invia termina inviando le informazioni di debug a OutputDebugString. Un utilizzo tipico di esso assomiglia a questo (dove debug è un oggetto ostream):Ostreams di solo debug in C++?

debug << "some error\n"; 

Per build di rilascio, che cosa è il modo meno doloroso e più performante di non uscita queste istruzioni di debug?

risposta

7

Che ne dici di questo? Dovreste verificare che in realtà ottimizza nulla nel rilascio:

#ifdef NDEBUG 
    class DebugStream {}; 
    template <typename T> 
    DebugStream &operator<<(DebugStream &s, T) { return s; } 
#else 
    typedef ostream DebugStream; 
#endif 

si dovrà passare l'oggetto flusso di debug come DebugStream &, non come un ostream &, dal momento che in build di rilascio non è uno. Questo è un vantaggio, dal momento che se il tuo debug stream non è un ostream, significa che non si incorre nella normale penalità di runtime di un flusso nullo che supporta l'interfaccia ostream (funzioni virtuali che vengono effettivamente chiamate ma non fanno nulla).

Attenzione: l'ho appena inventato, normalmente farei qualcosa di simile alla risposta di Neil - avere una macro che significa "esegui solo questo in build di debug", in modo che sia esplicito nella sorgente che cos'è il codice di debug e che cosa non lo è. Alcune cose che in realtà non voglio astrarre.

La macro di Neil ha anche la proprietà che assolutamente, sicuramente, non valuta i suoi argomenti in rilascio. Al contrario, anche con il mio modello inline, vi accorgerete che a volte:

debug << someFunction() << "\n"; 

non può essere ottimizzato per niente, perché il compilatore non necessariamente sapere che someFunction() non ci sono effetti collaterali. Naturalmente se someFunction()ha effetti collaterali, è possibile che venga chiamato in build di rilascio, ma si tratta di un mix particolare di registrazione e codice funzionale.

+0

Grazie! Stavo iniziando a pensare qualcosa in questo senso e sono felice di vedere che non sono l'unico a pensarlo. Lo proverò lunedì al lavoro e vedrò quanto bene il compilatore è in grado di ottimizzare il flusso. – Emanuel

+0

Chiunque utilizzi questo metodo presta attenzione al fatto che alcuni elementi non verranno ottimizzati in modalità di rilascio. Se hai: '' 'debug << someCPUIntensiveFunctionOrFunctionWhichMayAffectState() <<" \ n "' '' verrà ancora eseguito in modalità di rilascio. Ho perso questo cavetto la prima volta. –

9

Il modo più comune (e certamente più performante) è di rimuoverli utilizzando il preprocessore, utilizzando qualcosa di simile (più semplice implementazione possibile):

#ifdef RELEASE 
    #define DBOUT(x) 
#else 
    #define DBOUT(x) x 
#endif 

È quindi possibile dire

DBOUT(debug << "some error\n"); 

Modifica: Ovviamente è possibile rendere DBOUT un po 'più complesso:

#define DBOUT(x) \ 
    debug << x << "\n" 

che permette una sintassi alquanto migliore:

DBOUT("Value is " << 42); 

Una seconda alternativa è quella di definire dbout essere il flusso. Ciò significa che è necessario implementare una sorta di classe di flusso nullo - vedere Implementing a no-op std::ostream. Tuttavia, un tale stream ha un sovraccarico di runtime nella build di rilascio.

+0

Speravo che ci fosse un modo che mantenesse la bella sintassi iostream e ottimizzasse anche le dichiarazioni nelle build di rilascio come fanno le macro. – Emanuel

3

Come altri hanno affermato, il modo più efficace è quello di utilizzare il preprocessore. Normalmente evito il preprocessore, ma si tratta dell'unico utilizzo valido che ho trovato per la barra che protegge le intestazioni.

Normalmente desidero la possibilità di attivare qualsiasi livello di traccia negli eseguibili di rilascio e negli eseguibili di debug.Gli eseguibili di debug ottengono un livello di tracciamento predefinito più alto, ma il livello di traccia può essere impostato dal file di configurazione o dinamicamente in fase di runtime.

A tal fine le mie macro sembrano

#define TRACE_ERROR if (Debug::testLevel(Debug::Error)) DebugStream(Debug::Error) 
#define TRACE_INFO if (Debug::testLevel(Debug::Info)) DebugStream(Debug::Info) 
#define TRACE_LOOP if (Debug::testLevel(Debug::Loop)) DebugStream(Debug::Loop) 
#define TRACE_FUNC if (Debug::testLevel(Debug::Func)) DebugStream(Debug::Func) 
#define TRACE_DEBUG if (Debug::testLevel(Debug::Debug)) DebugStream(Debug::Debug) 

La cosa bella di utilizzare un'istruzione if è che non v'è alcun costo per per il tracciamento che non viene emesso, il codice di tracciatura unica viene chiamato se sarà stampato.

Se non si desidera che un determinato livello non venga visualizzato nelle build di rilascio, utilizzare una costante disponibile in fase di compilazione nell'istruzione if.

#ifdef NDEBUG 
    const bool Debug::DebugBuild = false; 
#else 
    const bool Debug::DebugBuild = true; 
#endif 

    #define TRACE_DEBUG if (Debug::DebugBuild && Debug::testLevel(Debug::Debug)) DebugStream(Debug::Debug) 

questo mantiene la sintassi iostream, ma ora il compilatore di ottimizzare l'istruzione if fuori dal codice, in build di rilascio.

+0

L'uso delle istruzioni if ​​non è una cattiva idea! So che se le istruzioni nei macro possono avere delle insidie, quindi dovrei stare particolarmente attenta a costruirle e usarle. Ad esempio: if (errore) TRACE_DEBUG << "errore"; else do_something_for_success(); Avrebbe finito per eseguire do_something_for_success() se si verifica un errore e le istruzioni di tracciamento a livello di debug sono disabilitate perché l'istruzione else si lega con l'istruzione if interna. Tuttavia, la maggior parte degli stili di codifica impone l'uso di parentesi graffe che risolvono il problema. if (errore) { TRACE_DEBUG << "errore"; } else do_something_for_success(); – Emanuel

+0

Il modo più sicuro per eseguire una macro è racchiuderlo all'interno di {macro_qui} while (0); –

1

@iain: Esaurito dalla stanza nella casella di commento, quindi pubblicarlo qui per chiarezza.

L'uso di istruzioni if ​​non è una cattiva idea! So che se le istruzioni nei macro possono avere delle insidie, quindi dovrei stare particolarmente attenta a costruirle e usarle. Per esempio:

if (error) TRACE_DEBUG << "error"; 
else do_something_for_success(); 

... finirebbe esecuzione do_something_for_success() in caso di errore e istruzioni trace-livello di debug sono disattivate perché l'istruzione else si lega con l'interno, se-dichiarazione. Tuttavia, la maggior parte degli stili di codifica impone l'uso di parentesi graffe che risolvono il problema.

if (error) 
{ 
    TRACE_DEBUG << "error"; 
} 
else 
{ 
    do_something_for_success(); 
} 

In questo frammento di codice, do_something_for_success() non viene erroneamente eseguito se a livello di debug analisi è disattivata.

+0

@Emanuel, sì, devi stare attento a questo, uso sempre parentesi per le mie istruzioni if ​​e per i loop. Ho visto bug in cui le persone aggiungevano una nuova riga alla parte allora di una dichiarazione if, ma si dimenticavano di aggiungere i curlies, il codice era ben rientrato, quindi il bug era quasi impossibile da individuare. – iain

2
#ifdef RELEASE 
    #define DBOUT(x) 
#else 
    #define DBOUT(x) x 
#endif 

Basta usare questo negli operatori ostream stessi. Potresti anche scrivere un singolo operatore per questo.

template<typename T> Debugstream::operator<<(T&& t) { 
    DBOUT(ostream << std::forward<T>(t);) // where ostream is the internal stream object or type 
} 

Se il compilatore non può ottimizzare le funzioni vuote in modalità di rilascio, allora è il momento per ottenere un nuovo compilatore.

Naturalmente ho utilizzato i riferimenti di rvalue e l'inoltro perfetto, e non c'è alcuna garanzia che tu abbia un compilatore di questo tipo. Ma puoi sicuramente usare un const ref se il tuo compilatore è compatibile solo con C++ 03.

5

Metodo più bella:

#ifdef _DEBUG 
#define DBOUT cout // or any other ostream 
#else 
#define DBOUT 0 && cout 
#endif 

DBOUT << "This is a debug build." << endl; 
DBOUT << "Some result: " << doSomething() << endl; 

Finché non fai niente di strano, funzioni chiamate e passati a DBOUT non saranno chiamati in build di rilascio. Questa macro funziona a causa della precedenza degli operatori e della logica AND; poiché && ha precedenza inferiore rispetto a <<, le build di rilascio compilano DBOUT << "a" come 0 && (cout << "a"). L'AND logico non valuta l'espressione a destra se l'espressione a sinistra viene valutata a zero o false; poiché l'espressione mano sinistra viene sempre valutata a zero, l'espressione della mano destra viene sempre rimossa da qualsiasi compilatore che valga la pena di utilizzare, tranne quando l'ottimizzazione è disabilitata (e anche in questo caso, il codice ovviamente non raggiungibile può ancora essere ignorato.)


Ecco un esempio di cose strane che si romperà questa macro:

DBOUT << "This is a debug build." << endl, doSomething(); 

Guarda le virgole. doSomething() verrà sempre chiamato, indipendentemente dal fatto che sia definito o meno _DEBUG. Questo è perché l'istruzione è valutata in build di rilascio come:

(0 && (cout << "This is a debug build." << endl)), doSomething(); 
// evaluates further to: 
false, doSomething(); 

Per utilizzare virgole con questa macro, la virgola devono essere confezionate in parentesi, così:

DBOUT << "Value of b: " << (a, b) << endl; 

altro esempio:

(DBOUT << "Hello, ") << "World" << endl; // Compiler error on release build 

In build di rilascio, questo è valutato come:

(0 && (cout << "Hello, ")) << "World" << endl; 
// evaluates further to: 
false << "World" << endl; 

che causa un errore del compilatore perché bool non può essere spostato a sinistra da un puntatore char a meno che non sia definito un operatore personalizzato. Questa sintassi provoca anche problemi aggiuntivi:

(DBOUT << "Result: ") << doSomething() << endl; 
// evaluates to: 
false << doSomething() << endl; 

Proprio come quando la virgola è stata usata male, doSomething() ancora viene chiamato, perché il suo risultato deve essere passato al gestore sinistra-shift. (Ciò può verificarsi solo quando un operatore personalizzato è definito che sinistra-sposta un bool da un puntatore char;. In caso contrario, si verifica un errore di compilazione)

Non parenthesize DBOUT << .... Se si desidera confrontare un intero intero letterale in parentesi, quindi parentesi, ma non sono a conoscenza di un singolo motivo valido per la parentesi di un operatore di flusso.

+1

Questa è una buona idea e ha funzionato perfettamente. – martin

+1

Esattamente quello che immaginavo potesse sembrare, e quello che stavo cercando. Molte grazie ! – user1913596