Search code examples
c++macroseffective-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. 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?


Solution

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