Search code examples
c++memoryformattingfmt

using fmtlib to log but logging macro doesn't expose stream


Preface

I have a LOG macro. In fact it's much more complicated and this is a simplified example. #define LOG(level) if (g_logLevel >= level) std::cout The macro itself cannot be changed and I don't want to create another macro just for fmt logging.

Use cases

LOG(ERROR) << "We got an error"; // Original use cases
LOG(INFO) << "42"; 
LOG(INFO) << fmt::format("42 {}", "is the answer"); // Bad, std::string created...

fmt::print(LOG(INFO), "42 {}", "is the answer"); // Doesn't compile.

// Write an overload for `<<` of the data structure will work, but it's tedious to do it everytime just for a log line..

Limitation

  1. Cannot change the logging macro
  2. Cannot define a new macro for it.
  3. Fit one-liner use case LOG(INFO) << myformat("42 {}", foo).

Requirements

  1. No dynamic memory allocation (within fixed size)
  2. Thread safe without synchronization
  3. Accept solutions for C++ 11 to C++ 20

My attempt

Create a wrapper function for fmt::format_to and it returns std::string_view which wraps a thread local fmt::memory_buffer.

It doesn't dynamically allocate memory as long as buffer size < SIZE.

Is there a better approach? Is returning fmt::memory_buffer better than thread_local?

Live Demo

#include <iostream>
#include <fmt/core.h>
#include <fmt/format.h>
#include <atomic>
#include <string_view>

enum LogLevel{
    CRITICAL = 0,
    ERROR,
    WARN,
    INFO,
};

std::atomic<LogLevel> g_logLevel{ERROR};

#define LOG(level) if (g_logLevel >= level) std::cout

using fmt_membuf = fmt::basic_memory_buffer<char, 2048>;

template <typename... T>
std::string_view myFormat(fmt::format_string<T...>fmt, T&& ...args){
    thread_local fmt_membuf out;
    out.clear();
    fmt::format_to(std::back_inserter(out), fmt, args...);
    return std::string_view(out.data(), out.size());
}

int main()
{ 
    g_logLevel = WARN;
    LOG(ERROR) << myFormat("Expected logged\n");
    LOG(INFO) << myFormat("42 won't show\n");
    LOG(CRITICAL) << myFormat("CRITICAL!\n");
    // fmt::format_to(LOG(ERROR), "42"); // <-- cannot compile
    return 0;
}

Solution

  • If you want efficient lazy evaluation you'll have to change the macro or introduce another one taking format arguments. For example, folly has XLOGF and FB_LOGF for this purpose.

    A less efficient option is to capture arguments and the format string in some object and perform lazy formatting when it is streamed. This is less efficient because you have to copy arguments to prevent lifetime issues. You could use lambda for this as suggested in another answer but in addition to being less efficient it's also more cumbersome to use.