13

I've implemented an ostream for debug output which sends ends up sending the debug info to OutputDebugString. A typical use of it looks like this (where debug is an ostream object):

debug << "some error\n";

For release builds, what's the least painful and most performant way to not output these debug statements?

pyrachi
  • 800
  • 6
  • 15

6 Answers6

9

The most common (and certainly most performant) way is to remove them using the preprocessor, using something like this (simplest possible implementation):

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

You can then say

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

Edit: You can of course make DBOUT a bit more complex:

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

which allows a somewhat nicer syntax:

DBOUT( "Value is " << 42 );

A second alternative is to define DBOUT to be the stream. This means that you must implement some sort of null stream class - see Implementing a no-op std::ostream. However, such a stream does have an runtime overhead in the release build.

Community
  • 1
  • 1
  • I was hoping there was some way that retained the nice iostream syntax and also optimized the statements away in release builds like the macros do. – pyrachi Apr 03 '10 at 12:02
  • Warning: Do not do `#define DBOUT( x )`, it's prone to errors. For example, `if (foo) DBOUT(x); if (bar) do_something_else();` would be translated into `if (foo && bar) do_something_else()`, so the original program is changed. – ChronoTrigger Aug 18 '19 at 04:24
8

A prettier method:

#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;

As long as you don't do anything weird, functions called and passed to DBOUT won't be called in release builds. This macro works because of operator precedence and the logical AND; because && has lower precedence than <<, release builds compile DBOUT << "a" as 0 && (cout << "a"). The logical AND doesn't evaluate the expression on the right if the expression on the left evaluates to zero or false; because the left-hand expression always evaluates to zero, the right-hand expression is always removed by any compiler worth using except when all optimization is disabled (and even then, obviously unreachable code may still be ignored.)


Here is an example of weird things that will break this macro:

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

Watch the commas. doSomething() will always be called, regardless of whether or not _DEBUG is defined. This is because the statement is evaluated in release builds as:

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

To use commas with this macro, the comma must be wrapped in parentheses, like so:

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

Another example:

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

In release builds, this is evaluated as:

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

which causes a compiler error because bool cannot be shifted left by a char pointer unless a custom operator is defined. This syntax also causes additional problems:

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

Just like when the comma was used poorly, doSomething() still gets called, because its result has to be passed to the left-shift operator. (This can only occur when a custom operator is defined that left-shifts a bool by a char pointer; otherwise, a compiler error occurs.)

Do not parenthesize DBOUT << .... If you want to parenthesize a literal integer shift, then parenthesize it, but I'm not aware of a single good reason to parenthesize a stream operator.

ShadowFan-X
  • 131
  • 1
  • 2
7

How about this? You'd have to check that it actually optimises to nothing in release:

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

You will have to pass the debug stream object as a DebugStream&, not as an ostream&, since in release builds it isn't one. This is an advantage, since if your debug stream isn't an ostream, that means you don't incur the usual runtime penalty of a null stream that supports the ostream interface (virtual functions that actually get called but do nothing).

Warning: I just made this up, normally I would do something similar to Neil's answer - have a macro meaning "only do this in debug builds", so that it is explicit in the source what is debugging code, and what isn't. Some things I don't actually want to abstract.

Neil's macro also has the property that it absolutely, definitely, doesn't evaluate its arguments in release. In contrast, even with my template inlined, you will find that sometimes:

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

cannot be optimised to nothing, because the compiler doesn't necessarily know that someFunction() has no side-effects. Of course if someFunction() does have side effects then you might want it to be called in release builds, but that's a peculiar mixing of logging and functional code.

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
  • Thanks! I was starting to think something along these lines myself and I'm glad to see I'm not the only one that was thinking it. I'll try it out on Monday back at work and see how well the compiler is able to optimize the stream away. – pyrachi Apr 03 '10 at 23:34
  • Anyone using this method please heed the point that some items won't be optimised out in release mode. If you have: ```debug << someCPUIntensiveFunctionOrFunctionWhichMayAffectState() << "\n" ``` it will still execute in release mode. I missed this cavet the first time round – Benjamin Close Nov 08 '17 at 22:00
3

Like others have said the most performant way is to use the preprocessor. Normally I avoid the preprocessor, but this is about the only valid use I have found for it bar protecting headers.

Normally I want the ability to turn on any level of tracing in release executables as well as debug executables. Debug executables get a higher default trace level, but the trace level can be set by configuration file or dynamically at runtime.

To this end my macros look like

#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)

The nice thing about using an if statement is that there is no cost to for tracing that is not output, the tracing code only gets called if it will be printed.

If you don't want a certain level to not appear in release builds use a constant that is available at compile time in the if statement.

#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)

This keeps the iostream syntax, but now the compiler will optimise the if statement out of the code, in release builds.

iain
  • 10,798
  • 3
  • 37
  • 41
  • The use of if statements isn't a bad idea! I know that if statements in macros may have some pitfalls so I'd have to be especially careful constructing them and using them. For example: if (error) TRACE_DEBUG << "error"; else do_something_for_success(); Would end up executing do_something_for_success() if an error occurs and debug-level trace statements are disabled because the else statement binds with the inner if-statement. However, most coding styles mandate use of curly braces which would solve the problem. if (error) { TRACE_DEBUG << "error"; } else do_something_for_success(); – pyrachi Apr 03 '10 at 23:47
  • Safest way to do a macro is to wrap it inside do{ macro_here } while(0); – Zan Lynx Apr 04 '10 at 02:23
2
#ifdef RELEASE
  #define DBOUT( x )
#else
  #define DBOUT( x )  x
#endif

Just use this in the actual ostream operators themselves. You could even write a single operator for it.

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

If your compiler can't optimize out empty functions in release mode, then it's time to get a new compiler.

I did of course use rvalue references and perfect forwarding, and there's no guarantee that you have such a compiler. But, you can surely just use a const ref if your compiler is only C++03 compliant.

Puppy
  • 144,682
  • 38
  • 256
  • 465
1

@iain: Ran out of room in the comment box so posting it here for clarity.

The use of if statements isn't a bad idea! I know that if statements in macros may have some pitfalls so I'd have to be especially careful constructing them and using them. For example:

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

...would end up executing do_something_for_success() if an error occurs and debug-level trace statements are disabled because the else statement binds with the inner if-statement. However, most coding styles mandate use of curly braces which would solve the problem.

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

In this code fragment, do_something_for_success() is not erroneously executed if debug-level tracing is disabled.

pyrachi
  • 800
  • 6
  • 15
  • @Emanuel, yes you have to be careful of this, I always use brackets for my if statements and for loops. I have seen bugs where people added a new line to the then part of an if statement but forgot to add the curlies, the code was nicely indented so bug was almost impossible to spot. – iain Apr 06 '10 at 14:38