1

I am currently developing a small and simple logging library that acts as a Facade for the Boost.Log v2 C++ library.

My library is almost finished and I have successfully encapsulated Boost.Log, i.e.:

  1. the API of my library is free of Boost.Log types, i.e. users do only have to deal with my types
  2. the ABI of my library is free of Boost.Log types, i.e. dependencies do not have to link against Boost.Log.

There is one last thing I am not able to solve at the moment: Lazy parameter evaluation

I'll give a short usage example how Boost.Log behaves:

#include <chrono>
#include <thread>

#include <boost/log/core.hpp>
#include <boost/log/expressions.hpp>
#include <boost/log/trivial.hpp>

std::string time_costly_function(
    const std::chrono::seconds seconds = std::chrono::seconds{1}) {
  std::this_thread::sleep_for(seconds);
  return "DONE with time_costly_function";
}

int main() {
  boost::log::core::get()->set_filter(boost::log::trivial::severity >=
                                      boost::log::trivial::warning);
  BOOST_LOG_TRIVIAL(warning) << "This is evaluated: " << time_costly_function();
  BOOST_LOG_TRIVIAL(info) << "This is NOT evaluated: "
                          << time_costly_function();
}

If you run the example above you can see that only the arguments for the first BOOST_LOG_TRIVIAL call are evaluated, i.e. time_costly_function() is only called once in total.

That is exactly the behavior that I want in my library, but as I mentioned I do not want that users have to deal with Boost.Log directly, but with my small Facade.

The following code illustrates the problem (very simplified to tackle down the actual problem):

#include <boost/log/core.hpp>
#include <boost/log/expressions.hpp>
#include <boost/log/trivial.hpp>

#include <chrono>
#include <thread>

std::string time_costly_function(
    const std::chrono::seconds seconds = std::chrono::seconds{1}) {
  std::this_thread::sleep_for(seconds);

  return "DONE with time_costly_function";
}

// Encapsulates Boost.Log. The API of my logging library does not know anything
// about Boost.Log (e.g. the Pimpl idiom is used).
//
// In reality this is a member function of a class in my library with the
// following signature:
//
// void log(const SeverityLevel level, const std::string& message) override;
//
void log(const std::string& message) { BOOST_LOG_TRIVIAL(warning) << message; }

// A custom logging macro.
//
// In reality this is a macro taking the object of a class with the member
// function illustrated above:
//
// FW_LOG(logger, level, message)
//
#define FW_LOG(message) log(message)

int main() {
  // TODO(wolters): This does not work, since everything is evaluated outside of
  // the Boost.Log macro(s). I want to call my macro as follows.
  //     FW_LOG(logger, level) << message << time_costly_function();
  // I.e. it should work like the Boost.Log macro(s) illustrated above.
  FW_LOG("foo" + time_costly_function());
}

I identified the following problems in my code:

  1. The function log may not take a std::string reference, since that will always lead to an evaluation outside of that function.
  2. The macro FW_LOG has to be rewritten in a manner that the following syntax is supported: FW_LOG(logger, level) << message << time_costly_function();

I've thought about using streams and/or perfect forwarding using template functions, but I was unable to come up with a working solution (yet).

Requirements: The code has to be compilable using MSVC 12.0, Boost.Log 1.59.0. C+11 is allowed.

So my actual question is:

How do I have to rewrite the code in the second example (using both the macro and the function) to get the same behavior as in the first example?

Florian Wolters
  • 3,820
  • 5
  • 35
  • 55

2 Answers2

1

Something like

#define FW_LOG(logger, level, message) \
    do { if (logger.check_level(level)) logger.log(message); while (false)
Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • Yes, something like that works. But it is **not** a solution in my case, since the logger does not know anything about the severity level. I've forgot to mention that I seperated my logging Facade into one Logger class and several Handler classes (e.g. `StreamHandler`). Only the Handlers have knowledge about the severity level. Everything is handled via the Boost.Log core, therefore the control flow always has to reach the `BOOST_LOG` macro inside of the member function. It also does not provide the syntax I asked for in bullet point 2. Thanks anyway! – Florian Wolters Mar 31 '17 at 13:05
0

After playing around with my code I've come up with a solution, thanks to @Jarod42 and @Nicolás (https://stackoverflow.com/a/2196183/891439).

  1. I've added a member function bool isLevelEnabledForAtLeastOneHandler(const SeverityLevel level) const to my Logger class in order to check if the severity level provided with the logging macro is enabled for at least one of the Handler classes added to the Logger instance.
  2. I've modified the solution by @Nicolás (the Logger class extends a LoggerInterface Interface Class in my library):

    class LoggerStream final : public std::ostringstream {
     public:
      LoggerStream(LoggerInterface& logger, const SeverityLevel level) : logger_{logger}, level_{level} {
        // NOOP
      }
    
      ~LoggerStream() {
        logger_.log(level_, str());
      }
    
     private:
      LoggerInterface& logger_;
      SeverityLevel level_;
    };
    
  3. I've modified the macro provided by @Jarod42:

    #define FW_LOG(logger, level) \
      if (logger.isLevelEnabledForAtLeastOneHandler(level)) LoggerStream(logger, level)
    
  4. I've changed the FW_LOG macro calls:

    FW_LOG(logger, SeverityLevel::WARNING) << "IS EVALUATED " << time_costly_function();
    FW_LOG(logger, SeverityLevel::DEBUG) << "IS NOT EVALUATED " << time_costly_function();
    

If I configure the handler(s) added to the Logger with the severity level WARNING, the second call to FW_LOG leads to the following behavior:

  1. The condition if (logger.isLevelEnabledForAtLeastOneHandler(level)) in the macro FW_LOG evaluates to false.
  2. The "stream arguments" << "IS NOT EVALUATED << time_costly_function() inserted into the macro FW_LOG are not evaluated, since no instance of the LoggerStream class is created.

So the only overhead my current solution has, is the call to isLevelEnabledForAtLeastOneHandler.

Not a perfect solution, but much better as before. I do not even had to modify the LoggerInterface Interface Class with the log member function for this solution.

Still my original question remains: Is it possible to forward the FW_LOG macro "stream arguments" to the member function Logger.log in a manner, that the Boost.Log macro can do all the work, i.e. all arguments are lazely evaluated inside of the member function and not inside of the macro?

Using the Boost.Log macros directly does not cause the same overhead as the solution provided. Therefore it feels like I've reinvented some parts of the wheel...

Florian Wolters
  • 3,820
  • 5
  • 35
  • 55