Ogre Psalm33
Ogre Psalm33

Reputation: 21946

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

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?

Upvotes: 8

Views: 4679

Answers (3)

Xeo
Xeo

Reputation: 131789

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. 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.

Now, the scoped_logger will be a RAII object that will actually log what it gets when it's destroyed, aka in the destructor.

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). 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). You can read more about this problem here (note that this problem has been fixed in C++11 with rvalue stream inserters). This "transformation" is done by calling a member function that simply returns a normal reference to the stream.

Small live example on Ideone.

Upvotes: 6

aschepler
aschepler

Reputation: 72271

The problem with converting that particular macro into a function is that things like "The X value = " << x are not valid expressions.

The << operator is left-associative, which means something in the form A << B << C is treated as (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. That is, if A is a std::stringstream, since A << B returns A, (A << B) << C; has the same effect as A << B; A << C;.

Now you can pass B << C into a macro just fine. 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. 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.

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. 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.

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.

Upvotes: 3

svenihoney
svenihoney

Reputation: 66

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

Upvotes: 5

Related Questions