8

我们的项目使用一个宏在一行语句中使日志记录变得容易和简单,如下所示:

DEBUG_LOG(TRACE_LOG_LEVEL, "The X value = " << x << ", pointer = " << *x);

该宏将第二个参数转换为字符串流参数,并将其发送到常规 C++ 记录器。这在实践中效果很好,因为它使多参数日志记录语句非常简洁。然而,Scott Meyers 在Effective C++ 3rd Edition中说过, “通过使用内联函数的模板,您可以获得宏的所有效率以及常规函数的所有可预测行为和类型安全性”(第 2 项)。我知道 C++ 中宏的使用存在许多与可预测行为相关的问题,因此我试图在我们的代码库中消除尽可能多的宏。

我的日志记录宏定义类似于:

#define DEBUG_LOG(aLogLevel, aWhat) {  \
if (isEnabled(aLogLevel)) {            \
  std::stringstream outStr;            \
  outStr<< __FILE__ << "(" << __LINE__ << ") [" << getpid() << "] : " << aWhat;    \
  logger::log(aLogLevel, outStr.str());    \
}

我已经尝试过几次将其重写为不使用宏的东西,包括:

inline void DEBUG_LOG(LogLevel aLogLevel, const std::stringstream& aWhat) {
    ...
}

和...

template<typename WhatT> inline void DEBUG_LOG(LogLevel aLogLevel, WhatT aWhat) {
    ...  }

无济于事(上述 2 次重写都不会针对第一个示例中的日志记录代码进行编译)。还有其他想法吗?这可以做到吗?还是最好将其保留为宏?

4

3 回答 3

6

日志记录仍然是您无法完全取消宏的少数几个地方之一,因为您需要其他方式无法获得的呼叫站点信息( __LINE__, , ...)。__FILE__另请参阅此问题

但是,您可以将日志记录逻辑移动到单独的函数(或对象)中,并通过宏仅提供调用站点信息。你甚至不需要模板函数。

#define DEBUG_LOG(Level, What) \
  isEnabled(Level) && scoped_logger(Level, __FILE__, __LINE__).stream() << What

这样,用法保持不变,这可能是一个好主意,因此您不必更改大量代码。使用,您将获得与使用您的子句&&相同的短期行为。if

现在,这scoped_logger将是一个 RAII 对象,它将实际记录它被销毁时所获得的内容,也就是在析构函数中。

struct scoped_logger
{
  scoped_logger(LogLevel level, char const* file, unsigned line)
    : _level(level)
  { _ss << file << "(" << line << ") [" << getpid() << "] : "; }

  std::stringstream& stream(){ return _ss; }
  ~scoped_logger(){ logger::log(_level, _ss.str()); }
private:
  std::stringstream _ss;
  LogLevel _level;
};

暴露底层std::stringstream对象为我们省去了编写自己的operator<<重载的麻烦(这很愚蠢)。通过函数实际公开它的需要很重要。如果scoped_logger对象是临时对象(右值),则成员也是如此,如果我们不以某种方式将其转换为左值(引用),则只会找到std::stringstream成员重载。您可以在此处operator<<阅读有关此问题的更多信息(请注意,此问题已在 C++11 中通过右值流插入器修复)。这种“转换”是通过调用一个简单地返回对流的正常引用的成员函数来完成的。

Ideone 上的小实例。

于 2012-03-12T15:29:16.343 回答
5

不,由于您在宏中使用运算符 (<<),因此无法将这个确切的宏重写为模板,它不能作为模板参数或函数参数传递。

我们遇到了同样的问题,并使用基于类的方法解决了它,使用类似的语法

DEBUG_LOG(TRACE_LOG_LEVEL) << "The X value = " << x << ", pointer = " << *x << logger::flush;

这确实需要重写代码(通过使用正则表达式)并引入一些类魔法,但会带来更大的灵活性(延迟输出、每个日志级别的输出选项(到文件或标准输出)等等)。

于 2012-03-12T14:04:00.970 回答
3

将该特定宏转换为函数的问题在于,诸如此类"The X value = " << x的东西不是有效的表达式。

运算符是左结合的<<,这意味着表单中的某些A << B << C内容被视为(A << B) << C. iostream 的重载插入运算符始终返回对同一流的引用,因此您可以在同一语句中执行更多插入。也就是说,如果A是 a std::stringstream,因为A << B返回A(A << B) << C;与 具有相同的效果A << B; A << C;

现在你可以传入B << C一个宏就好了。宏只是将其视为一堆标记,并且在所有替换完成之前不担心它们的含义。到那时,左关联规则就可以发挥作用了。但是对于任何函数参数,即使是内联和模板化的,编译器也需要弄清楚参数的类型是什么以及如何找到它的值。如果B << C无效(因为B既不是流也不是整数),编译器错误。即使B << C是有效的,由于函数参数总是在被调用函数中的任何内容之前评估,你最终会得到 behavior A << (B << C),这不是你想要的。

如果您愿意更改宏的所有用途(例如,使用逗号而不是<<标记,或者类似@svenihoney 的建议),有办法做某事。如果不是,该宏就不能被视为一个函数。

不过我想说这个宏并没有什么坏处,只要所有必须使用它的程序员都能理解为什么在以 开头的行上,他们可能会看到与和/或DEBUG_LOG相关的编译器错误。std::stringstreamlogger::log

如果您保留一个宏,请查看 C++ 常见问题解答39.439.5以了解一些技巧,以避免像这样的宏可能会让您感到惊讶。

于 2012-03-12T14:08:36.050 回答