I'm developing a C++ library that uses some type of information logging using spdlog. The library can be used by a CLI tool, or directly by the end-user. I would like to be able to pass an empty interface implementation in cases where I don't care about the logging (for example for unit tests or whenever I don't want to include the spdlog stuff and do not care about logging).
The only solution that comes to my mind is to create a wrapper around spdlog functions (info/error/warn/debug). I tried doing that but since these are templated variadic functions that I'd have to make virtual to be able to override them, it's just not possible.
Could you guide me if my reasoning about the problem is wrong (passing around logger object to different modules), and if there is any other solution to such problem?
I'm open to using other logging libraries, it's just I cant find the one that would be easy to wrap.
EDIT: Setting the SPDLOG_LEVEL will not solve my problem as spdlog will still be a dependency in my code. Ideally I'd like to depend only on my interface and pass a valid spdlog wrapper or an empty implementation when needed. This would also be beneficial in situations when I'd like to change the logging library.
This seems to be a clash between compile-time polymorphism (template code) and run-time polymorphism (virtual functions), for which there is no simple solution.
To elaborate on that, the typical log function takes a variable number of parameters of various types. This allows logging any type of data, the typical requirement is only that it has a conversion to a string defined. In order to implement that, the C++ tool of choice is to use templates. The mechanism for that is that the compiler looks at the types of the parameters and then creates code depending on those types.
Now, if you want to exchange the logger library at run-time, you need to use regular, virtual
-based polymorphism. This means you write an abstract baseclass which defines the interface and according concrete classes implementing that interface. The problem here is that there is an infinite number of possible member functions you'd have to provide, so that is clearly not feasible.
If you look at spdlog, you will find a log_it_()
method. This is the central point to which a prepared record is passed. All log methods converge at that single spot which does the actual work. One approach would be to make this single function virtual, which would allow writing different implementations for it.
Notes:
If you define a set of templates similar to spdlog's which take parameters and then store them as std::any
, you could then pass them through a single virtual function, which you could then use to plug in different logging libraries.
Notes:
std::any
also has allocation overhead. How it compares to the Polymorphic Sink approach above isn't clear.std::any
.The macros like SPDLOG_TRACE()
can be replaced with YOURPROJECT_TRACE()
, which is then defined in a way that it forwards to spdlog, some other log or that it does nothing. Depending on build options, that should allow you to completely remove the dependency on spdlog code.
Notes:
These facades would each implement one logging library. They would all have a set of templated log functions taking different parameter types, which they would then forward to the according logging libraries. These facades all share a common interface, but they don't have a common abstract baseclass defining this interface. This is also known as "duck typing".
Notes: