简体   繁体   English

我可以使用流操作符重写日志宏以使用C ++模板函数吗?

[英]Can I rewrite a logging macro with stream operators to use a C++ template function?

Our project uses a macro to make logging easy and simple in one-line statements, like so: 我们的项目使用宏来使单行语句中的日志记录变得简单和简单,如下所示:

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

The macro translates the 2nd parameter into stringstream arguments, and sends it off to a regular C++ logger. 宏将第二个参数转换为stringstream参数,并将其发送到常规C ++记录器。 This works great in practice, as it makes multi-parameter logging statements very concise. 这在实践中非常有用,因为它使多参数记录语句非常简洁。 However, Scott Meyers has said, in Effective C++ 3rd Edition , "You can get all the efficiency of a macro plus all the predictable behavior and type safety of a regular function by using a template for an inline function" (Item 2). 但是,Scott Meyers在Effective C ++ 3rd Edition中说过,“通过使用内联函数的模板,你可以获得宏的所有效率以及常规函数的所有可预测行为和类型安全性”(第2项)。 I know there are many issues with macro usage in C++ related to predictable behavior, so I'm trying to eliminate as many macros as possible in our code base. 我知道在C ++中与可预测行为相关的宏用法有很多问题,所以我试图在代码库中尽可能多地消除宏。

My logging macro is defined similar to: 我的日志宏定义类似于:

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

I've tried several times to rewrite this into something that doesn't use macros, including: 我已多次尝试将其重写为不使用宏的内容,包括:

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

And... 和...

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

To no avail (neither of the above 2 rewrites will compile against our logging code in the 1st example). 无济于事(以上两个重写都不会在第一个例子中编译我们的日志代码)。 Any other ideas? 还有其他想法吗? Can this be done? 可以这样做吗? Or is it best to just leave it as a macro? 或者最好把它留作宏?

No, it is not possible to rewrite this exact macro as a template since you are using operators (<<) in the macro, which can't be passed as a template argument or function argument. 不,由于您在宏中使用运算符(<<)而无法将此精确宏重写为模板,因此无法将其作为模板参数或函数参数传递。

We had the same issue and solved it with a class based approach, using a syntax like 我们遇到了同样的问题,并使用类似的语法通过基于类的方法解决了它

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

This would indeed require to rewrite the code (by using a regular expression) and introduce some class magic, but gives the additional benefit of greater flexibiliy (delayed output, output options per log level (to file or stdout) and things like that). 这确实需要重写代码(通过使用正则表达式)并引入一些类魔术,但是提供了更大灵活性(延迟输出,每个日志级别的输出选项(到文件或标准输出)和类似的东西)的额外好处。

Logging remains one of the few places were you can't completely do away with macros, as you need call-site information ( __LINE__ , __FILE__ , ...) that isn't available otherwise. 记录仍然是您无法完全取消宏的少数几个地方之一,因为您需要其他方式无法使用的呼叫站点信息( __LINE__ __FILE____FILE__ ,...)。 See also this question . 另见这个问题

You can, however, move the logging logic into a seperate function (or object) and provide just the call-site information through a macro. 但是,您可以将日志记录逻辑移动到单独的函数(或对象)中,并通过宏提供调用站点信息。 You don't even need a template function for this. 你甚至不需要模板功能。

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

With this, the usage remains the same, which might be a good idea so you don't have to change a load of code. 有了这个,用法保持不变,这可能是一个好主意,因此您不必更改代码的负载。 With the && , you get the same short-curcuit behaviour as you do with your if clause. 使用&& ,您将获得与if子句相同的短路行为。

Now, the scoped_logger will be a RAII object that will actually log what it gets when it's destroyed, aka in the destructor. 现在, 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;
};

Exposing the underlying std::stringstream object saves us the trouble of having to write our own operator<< overloads (which would be silly). 公开底层的std::stringstream对象使我们不得不编写自己的operator<< overloads(这将是愚蠢的)。 The need to actually expose it through a function is important; 通过函数实际暴露它的需要很重要; if the scoped_logger object is a temporary (an rvalue), so is the std::stringstream member and only member overloads of operator<< will be found if we don't somehow transform it to an lvalue (reference). 如果scoped_logger对象是临时(右值),那么std::stringstream成员也是如此,如果我们不以某种方式将它转换为左值(引用),则只会发现operator<<成员重载。 You can read more about this problem here (note that this problem has been fixed in C++11 with rvalue stream inserters). 您可以在此处阅读有关此问题的更多信息(请注意,此问题已在使用rvalue流插入器的C ++ 11中得到修复)。 This "transformation" is done by calling a member function that simply returns a normal reference to the stream. 这种“转换”是通过调用一个简单地返回流的常规引用的成员函数来完成的。

Small live example on Ideone. Ideone上的小型实例。

The problem with converting that particular macro into a function is that things like "The X value = " << x are not valid expressions. 将特定宏转换为函数的问题是"The X value = " << x这样的东西不是有效的表达式。

The << operator is left-associative, which means something in the form A << B << C is treated as (A << B) << C . <<运算符是左关联的,这意味着形式为A << B << C被视为(A << B) << C The overloaded insertion operators for iostreams always return a reference to the same stream so you can do more insertions in the same statement. iostream的重载插入运算符始终返回对同一流的引用,因此您可以在同一语句中执行更多插入操作。 That is, if A is a std::stringstream , since A << B returns A , (A << B) << C; 也就是说,如果Astd::stringstream ,因为A << B返回A(A << B) << C; has the same effect as A << B; A << C; A << B; A << C;具有相同的效果A << B; A << C; A << B; A << C; .

Now you can pass B << C into a macro just fine. 现在你可以把B << C传递给宏了。 The macro just treats it as a bunch of tokens, and doesn't worry about what they mean until all the substituting is done. 宏只是将它视为一堆令牌,并且在完成所有替换之前不会担心它们的含义。 At that point, the left-associative rule can kick in. But for any function argument, even if inlined and templated, the compiler needs to figure out what the type of the argument is and how to find its value. 此时,左关联规则可以启动。但是对于任何函数参数,即使内联和模板化,编译器也需要弄清楚参数的类型以及如何查找其值。 If B << C is invalid (because B is neither a stream nor an integer), compiler error. 如果B << C无效(因为B既不是流也不是整数),编译错误。 Even if B << C is valid, since function parameters are always evaluated before anything in the invoked function, you'll end up with the behavior A << (B << C) , which is not what you want here. 即使B << C有效,因为函数参数总是在被调用函数中的任何内容之前进行求值,你最终会得到行为A << (B << C) ,这不是你想要的。

If you're willing to change all the uses of the macro (say, use commas instead of << tokens, or something like @svenihoney's suggestion), there are ways to do something. 如果你愿意改变宏的所有用途(例如,使用逗号代替<<标记,或类似@svenihoney的建议),有办法做某事。 If not, that macro just can't be treated like a function. 如果没有,那个宏就不能像功能一样对待。

I'd say there's no harm in this macro though, as long as all the programmers who have to use it would understand why on a line starting with DEBUG_LOG , they might see compiler errors relating to std::stringstream and/or logger::log . 我会说这个宏没有什么坏处,只要所有必须使用它的程序员都能理解为什么在以DEBUG_LOG开头的行上,他们可能会看到与std::stringstream和/或logger::log相关的编译器错误logger::log

If you keep a macro, check out C++ FAQ answers 39.4 and 39.5 for tricks to avoid a few nasty ways macros like this can surprise you. 如果您保留一个宏,请查看C ++ FAQ答案39.439.5以获取技巧,以避免一些令人讨厌的方式,这样的宏会让您大吃一惊。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM