C++20下spdlog使用踩坑
spdlog是一个开源的被广泛使用的C++log系统,于是我模仿Piccolo引擎写了一个Log系统,结果由于C++不允许连续进行两次及以上的用户自定义的隐式构造,使得spdlog的log函数无法正确推断匹配Args...
,最后在公司C++高手的帮助下成功解决了问题,于是在此记录一下踩坑历程
最初的版本
我参考开源项目的实现,使用spdlog写了一个简易的LogManager,将其添加到一共全局变量中,通过宏将不同类型的LOG封装,还传递了函数Module
#define LOG_HELPER(LOG_LEVEL, ...) \ g_global_context.m_log_manager->log(LOG_LEVEL, "[" + std::string(__FUNCTION__) + "] " + __VA_ARGS__);
#define LOG_DEBUG(...) LOG_HELPER(LogManager::LogLevel::Debug, __VA_ARGS__);
#define LOG_INFO(...) LOG_HELPER(LogManager::LogLevel::Info, __VA_ARGS__);
#define LOG_WARN(...) LOG_HELPER(LogManager::LogLevel::Warn, __VA_ARGS__);
#define LOG_ERROR(...) LOG_HELPER(LogManager::LogLevel::Error, __VA_ARGS__);
#define LOG_FATAL(...) LOG_HELPER(LogManager::LogLevel::Fatal, __VA_ARGS__);
|
class LogManager final { public: enum class LogLevel : uint8_t { Debug, Info, Warn, Error, Fatal };
LogManager(); ~LogManager();
template<typename... Args> void log(LogLevel level, Args&&... args) { switch (level) { case LogLevel::Debug: m_logger->debug(std::forward<Args>(args)...); break; case LogLevel::Info: m_logger->info(std::forward<Args>(args)...); break; case LogLevel::Warn: m_logger->warn(std::forward<Args>(args)...); break; case LogLevel::Error: m_logger->error(std::forward<Args>(args)...); break; case LogLevel::Fatal: m_logger->critical(std::forward<Args>(args)...); break; default: break; } }
template<typename... Args> void fatalCallback(Args&&... args) { const std::string format_str = std::format(std::forward<Args>(args)...); throw std::runtime_error(format_str); }
private: std::shared_ptr<spdlog::logger> m_logger; };
|
然而当我测试时,却出现了错误
LOG_INFO("Scene Tick{}", 1);
|
error C7595: “fmt::v9::basic_format_string<char,int>::basic_format_string”: 对即时函数的调用不是常量表达式 message : 因读取超过生命周期的变量而失败 message : 请参见“<args_0>”的用法
|
fmt版本
我求助了公司的C++高手,高手给我改成了这样,他将参数中函数Module分离出来,并将其和表达式宏拼接
#define LOG_HELPER(LOG_LEVEL, MODULE, FMT_STRING, ...) \ g_global_context.m_log_manager->log(LOG_LEVEL, MODULE##FMT_STRING, __VA_ARGS__);
|
template<typename... Args> +void log(LogLevel level, const char* _module, Args&&... args) { switch (level) { case LogLevel::Debug: + m_logger->debug(spdlog::fmt_runtime_string<char>{ _module }, std::forward<Args>(args)...); break; ... } }
|
并让我这样调用函数
LOG_INFO(__FUNCTION__, "xxx{}", 1);
|
结果我发现,在高手电脑上可以正确运行的代码,在我本地缺会丢失"xxx1"
这一部分,经过一段时间的排查,发现是我们的编译器预编译指令处理方式有区别,于是我修改cmake,让编译器使用符合C++标准的行为
if(MSVC) target_compile_options(core PUBLIC "/Zc:preprocessor") endif()
|
添加后"xxx1"
确实成功出现了
我本以为问题就这样解决,但C++高数却让我再实现一个可以只传字符串的重载(调另一个log接口),因为现在这个Log没法传入下面这种纯字符串内容
LOG_INFO(__FUNCTION__, "Hello world!");
|
我整个人傻眼了,心想spdlog这么厉害的库,肯定有很多人在C++20的环境下使用,怎么可能要这么丑陋的实现!
std::fotmat版本
然后我翻看spdlog的源码,发现他有两个log的实现
template <typename T> void debug(const T &msg) { log(level::debug, msg); }
|
template <typename... Args> void debug(format_string_t<Args...> fmt, Args &&...args) { log(level::debug, fmt, std::forward<Args>(args)...); }
|
然后我就发现,原来我们之前一直想匹配到下面这个接口,却由于C++20在预编译处理上的一些改动,使我的代码十分复杂丑陋,而且还使用了spdlog内部的类型,我挺反感这种用一些模块内部类型的操作,我感觉好的工具类应该能黑箱地用
看着看着,我恍然大悟,C++20提供了std::format
函数,我可以自己format字符串,传给spdlog一个普通字符串就行了
#define LOG_HELPER(LOG_LEVEL, ...) \ g_global_context.m_log_manager->log(LOG_LEVEL,"[" __FUNCTION__ "] " +std::format(__VA_ARGS__));
|
template<typename... Args> void log(LogLevel level, Args&&... args) { switch (level) { case LogLevel::Debug: m_logger->debug(std::forward<Args>(args)...); break; ... } }
|
感觉是时候深入学习C++20了