C++中的调试日志语句是否需要使用预处理宏?

7

最近我一直在阅读Scott Meyers撰写的《Effective C++第二版》,以提高C++最佳实践。他列举了其中一个条目,鼓励C++程序员避免使用预处理宏,并“更喜欢编译器”。他甚至说除了#include和#ifdef / #ifndef之外,在C ++中几乎没有宏的理由。

我同意他的推理,因为你可以通过以下方式实现宏

#define min(a,b) ((a) < (b) ? (a) : (b))

使用以下C++语言特性

template<class T>
inline const T & min(const T & a, const T & b) {
    return a < b ? a : b;
}

使用inline关键字可以使编译器有选择地移除函数调用并插入内联代码和模板,以处理具有重载或内置>操作符的多个数据类型。

编辑-- 如果a和b的数据类型不同,则此模板声明将不能完全匹配所述的宏。请参见Pete的评论以获取示例。

然而,我很想知道在C++中是否可以使用宏进行调试日志记录。如果我下面提出的方法不是良好的实践,那么是否有人能够建议替代方法?

在过去的一年里,我一直在编写Objective-C,并且我的最喜欢的2D引擎(cocos2d)利用宏创建日志语句。该宏如下:

/*


* if COCOS2D_DEBUG is not defined, or if it is 0 then
 *  all CCLOGXXX macros will be disabled
 *
 * if COCOS2D_DEBUG==1 then:
 *      CCLOG() will be enabled
 *      CCLOGERROR() will be enabled
 *      CCLOGINFO() will be disabled
 *
 * if COCOS2D_DEBUG==2 or higher then:
 *      CCLOG() will be enabled
 *      CCLOGERROR() will be enabled
 *      CCLOGINFO() will be enabled
 */


#define __CCLOGWITHFUNCTION(s, ...) \
NSLog(@"%s : %@",__FUNCTION__,[NSString stringWithFormat:(s), ##__VA_ARGS__])

#define __CCLOG(s, ...) \
NSLog(@"%@",[NSString stringWithFormat:(s), ##__VA_ARGS__])


#if !defined(COCOS2D_DEBUG) || COCOS2D_DEBUG == 0
#define CCLOG(...) do {} while (0)
#define CCLOGWARN(...) do {} while (0)
#define CCLOGINFO(...) do {} while (0)

#elif COCOS2D_DEBUG == 1
#define CCLOG(...) __CCLOG(__VA_ARGS__)
#define CCLOGWARN(...) __CCLOGWITHFUNCTION(__VA_ARGS__)
#define CCLOGINFO(...) do {} while (0)

#elif COCOS2D_DEBUG > 1
#define CCLOG(...) __CCLOG(__VA_ARGS__)
#define CCLOGWARN(...) __CCLOGWITHFUNCTION(__VA_ARGS__)
#define CCLOGINFO(...) __CCLOG(__VA_ARGS__)
#endif // COCOS2D_DEBUG

这个宏提供了非常实用的功能,我希望能将其融入我的C++程序中。编写有用的日志语句就像这样简单:

CCLOG(@"Error in x due to y");

更棒的是,如果将COCOS2D_DEBUG设置为0,则这些语句永远不会被执行。检查条件语句以查看是否应使用日志记录语句,不会产生额外开销。在从开发环境过渡到生产环境时非常方便。如何在C++中重新创建这种效果?
那么,这种类型的宏适用于C++程序吗?有没有更好的、更符合C++风格的方法来实现这一点?

4
我认为你的“CCLOG()”是宏非常有用和合适的一个很好的例子。在我看来... - paulsm4
1
嗯,那个“min”模板函数并不与宏执行相同的操作。请尝试使用min(1, 2L) - Pete Becker
@PeteBecker 我不是模板大师,但这可能是因为1是int类型而2L是long类型吗?我提供的模板声明仅会为两个相同类型的对象生成函数。 - Paul Renton
1
@PaulRenton - 是的,确实如此。这并不意味着模板不好;而是因为min在标准库中被定义的方式。 - Pete Becker
@PeteBecker 很好的发现。我会记下它们并不完全相似。 - Paul Renton
在C++代码中使用Objective C通常不被认为是良好的实践。你至少应该明确你在做什么。 - Jonathan Leffler
2个回答

6
首先,Scott的声明是在宏被大量滥用的历史原因下做出的。虽然这通常是正确的,但也有一些情况下宏是有意义的,其中之一就是日志记录,因为只有宏可以自动插入__FILE__和__LINE__。此外,只有宏才能解析为空白(尽管根据我的经验,这不是什么大问题)。
像您展示的这样的宏在C++中并不常见。对于日志记录,有两个常见的变体:
#define LOG( message ) ... << message ...

该功能允许以“ x = ”<< x 的形式发送消息,并且可以通过重新定义宏来完全禁用。

#define LOG() logFile( __FILE__, __LINE__ )

logFile返回一个std::ostream的包装器,其中定义了operator<<,并允许进行如下操作:

LOG() << "x = " << x;

这样做,LOG() 右侧的所有表达式都将被评估,但只有在日志处于活动状态时才会进行格式化。


2

在使用宏时,有正确的用法和错误的用法。在函数可以胜任的情况下使用宏是个坏主意。而在函数无法完成相同任务时使用宏则是完全可行的。

我经常使用如下结构:

#defien my_assert(x) do { if (!x) assert_failed(x, #x, __FILE__, __LINE__); } while(0)

template<typename T> 
void assert_failed(T x, const char *x_str, const char *file, int line)
{
   std::cerr << "Assertion failed: " << x_str << "(" << x << ") at " << file << ":" << line << std::endl;
   std::terminate();
}

使用字符串化“运算符”的另一个技巧如下:

enum E
{
   a, 
   b, 
   c,
   d
 };

 struct enum_string
 {
    E v;
    const char *str;
 };

 #define TO_STR(x) { x, #x }

 enum_string enum_to_str[] = 
 {
    TO_STR(a),
    TO_STR(b),
    TO_STR(c),
    TO_STR(d),
  };

节省了很多重复的事情......

所以,在某些情况下确实很有用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接