I have been dabbling with new C++20 features such as modules and concepts. One of the inherent properties of the new modules is that they do not leak pre-processor definitions to consumers -- this is both a blessing and a curse because some behaviours, such as logging, in C++ have traditionally been implemented with #define macros so that they can be #defined to nothing in release builds.
My question is simple: how should one go about implementing logging without macros, today, assuming that one still wants to retain behaviours like having the compiler entirely remove logging calls, with no side effects, in release builds?
My endeavour to achieve this exploits lambdas and C++20 concepts to drive template specialization.
#define NOOP /* no operation */
template <typename T>
concept printable = requires (const T & message) {
std::cout << message;
};
template <typename F>
concept format_factory = std::regular_invocable<F>
&& std::convertible_to<std::invoke_result_t<F>, std::string_view>;
...
#ifdef _DEBUG
private:
template<std::regular_invocable F>
static inline const std::invoke_result_t<F> map(F f) {
return f();
}
template<typename T>
static inline constexpr const T& map(const T& value) {
return value;
}
public:
template<printable T>
static inline void trace(const T& message) {
std::cout << message << std::endl;
}
template<typename... Args>
static inline void trace(const std::string_view& format, Args&&... args) {
std::cout << std::format(format, map(args)...) << std::endl;
}
template<format_factory F, typename... Args>
static inline void trace(const F& format, Args&&... args) {
std::cout << std::format(format(), map(args)...) << std::endl;
}
#else
public:
template<typename... Args>
static inline constexpr void trace(const Args&... args) { NOOP; }
#endif
The idea is that...
- any literals and values to be logged can be passed, normally, because, in release builds, the compiler will optimise out any copies or moves since they will not be accessed.
- anything 'expensive' to be logged can be passed as an accessor lambda which will not be invoked in release builds and, consequently, also be optimised out.
For example, user-code might look like this:
Log::trace("format literal ({}, {})", []() { return "expensive value"; }, "cheap value");
I have tried this with Visual C++ 2022 (preview) and I can confirm that it does work as intended but is it a good idea? How could I make it better?
Remember that this is done because I want to export this from a C++20 module and I can't do that with preprocessor macros as far as I understand.