我为调试输出实现了ostream
,最终发送调试信息到OutputDebugString
。它的典型用法看起来像这样(其中debug
是一个ostream对象):
debug << "some error\n";
对于发布版本,不输出这些调试语句的最不痛苦和最高效的方法是什么?
答案 0 :(得分:9)
最常见(当然也是性能最佳)的方法是使用预处理器删除它们,使用类似这样的东西(最简单的实现):
#ifdef RELEASE
#define DBOUT( x )
#else
#define DBOUT( x ) x
#endif
然后你可以说
DBOUT( debug << "some error\n" );
编辑:您当然可以让DBOUT更复杂一些:
#define DBOUT( x ) \
debug << x << "\n"
允许更好的语法:
DBOUT( "Value is " << 42 );
第二种方法是将DBOUT定义为流。这意味着您必须实现某种类型的null流类 - 请参阅Implementing a no-op std::ostream。但是,这样的流确实在发布版本中具有运行时开销。
答案 1 :(得分:7)
这个怎么样?您必须检查它在发布中是否实际优化为什么:
#ifdef NDEBUG
class DebugStream {};
template <typename T>
DebugStream &operator<<(DebugStream &s, T) { return s; }
#else
typedef ostream DebugStream;
#endif
您必须将调试流对象作为DebugStream&amp;而不是作为ostream&amp;进行传递,因为在发布版本中它不是一个。这是一个优点,因为如果您的调试流不是ostream,这意味着您不会产生支持ostream接口的空流的常规运行时惩罚(虚拟函数实际上被调用但什么都不做)。
警告:我刚刚做了这个,通常我会做类似于Neil的回答 - 有一个宏意思“只在调试版本中执行此操作”,因此它在源代码中是明确的什么是调试代码,以及什么是“T。我实际上并不想抽象的一些事情。
Neil的宏也有一个属性,绝对肯定不会在发布中评估它的论点。相比之下,即使我的模板内联,你会发现有时:
debug << someFunction() << "\n";
无法优化为零,因为编译器不一定知道someFunction()
没有副作用。当然如果someFunction()
有副作用,那么你可能希望在发布版本中调用它,但这是日志和功能代码的特殊混合。
答案 2 :(得分:6)
更漂亮的方法:
#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;
只要你不做任何奇怪的事情,调用和传递给DBOUT
的函数将不会在发布版本中调用。该宏因运算符优先级和逻辑AND而起作用;因为&&
的优先级低于<<
,所以发布版本将DBOUT << "a"
编译为0 && (cout << "a")
。如果左侧的表达式计算为零或false
,则逻辑AND不会评估右侧的表达式;因为左手表达式总是计算为零,所以任何值得使用的编译器都会删除右手表达式,除非禁用所有优化(即使这样,显然仍然无法访问代码,但仍然可以忽略。)
以下是一个会破坏这个宏的奇怪事情的例子:
DBOUT << "This is a debug build." << endl, doSomething();
观看逗号。无论是否定义doSomething()
,都将始终调用_DEBUG
。这是因为语句在发布版本中评估为:
(0 && (cout << "This is a debug build." << endl)), doSomething();
// evaluates further to:
false, doSomething();
要在此宏中使用逗号,逗号必须用括号括起来,如下所示:
DBOUT << "Value of b: " << (a, b) << endl;
另一个例子:
(DBOUT << "Hello, ") << "World" << endl; // Compiler error on release build
在发布版本中,评估为:
(0 && (cout << "Hello, ")) << "World" << endl;
// evaluates further to:
false << "World" << endl;
导致编译器错误,因为bool
指针除非定义了自定义运算符,否则char
不能向左移位(DBOUT << "Result: ") << doSomething() << endl;
// evaluates to:
false << doSomething() << endl;
。此语法还会导致其他问题:
doSomething()
就像逗号使用不当时一样,bool
仍会被调用,因为它的结果必须传递给左移运算符。 (只有在定义了一个自定义运算符,将char
左移DBOUT << ...
指针时才会发生这种情况;否则会发生编译错误。)
不要括号{{1}}。如果你想用括号括起一个文字整数移位,然后用括号括起来,但我不知道有一个很好的理由来为一个流操作符加上括号。
答案 3 :(得分:3)
与其他人一样,最高效的方式是使用预处理器。通常我会避开预处理器,但这是我发现它唯一有效的用途,它可以保护标头。
通常我希望能够在发布可执行文件和调试可执行文件中打开任何级别的跟踪。调试可执行文件获得更高的默认跟踪级别,但跟踪级别可以由配置文件设置或在运行时动态设置。
为此,我的宏看起来像
#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)
使用if语句的好处是没有输出的跟踪成本,只有在打印时才会调用跟踪代码。
如果您不希望某个级别未出现在发布版本中,请使用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)
这保留了iostream语法,但现在编译器将在发布版本中优化代码中的if语句。
答案 4 :(得分:2)
#ifdef RELEASE
#define DBOUT( x )
#else
#define DBOUT( x ) x
#endif
只需在实际的ostream运算符中使用它。你甚至可以为它编写一个操作符。
template<typename T> Debugstream::operator<<(T&& t) {
DBOUT(ostream << std::forward<T>(t);) // where ostream is the internal stream object or type
}
如果你的编译器无法在发布模式下优化掉空函数,那么就该获得一个新的编译器了。
我当然使用rvalue引用和完美转发,并且不能保证你有这样的编译器。但是,如果编译器只符合C ++ 03,那么你肯定可以使用const ref。
答案 5 :(得分:1)
@iain:在评论栏中离开房间,所以为了清楚起见,将其发布在此处。
使用if语句并不是一个坏主意!我知道如果宏中的语句可能有一些陷阱,所以我必须特别小心地构建它们并使用它们。例如:
if (error) TRACE_DEBUG << "error";
else do_something_for_success();
如果发生错误并且调试级跟踪语句被禁用,那么 ...将最终执行do_something_for_success()
,因为else语句与内部if语句绑定。但是,大多数编码样式都要求使用花括号来解决问题。
if (error)
{
TRACE_DEBUG << "error";
}
else
{
do_something_for_success();
}
在此代码片段中,如果禁用调试级别跟踪,则不会错误地执行do_something_for_success()。