抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

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了

评论