对于任何一个软件日志功能都是必不可少的,日志中的每一条都必备时间、等级、文件名、行号等要素,在传统的C++中往往是通过宏来实现相关功能,如log4cplus中的实现
#define LOG_FORMAT(level, args...) \
if (m_logger != nullptr) { \
m_logger->log_data(__FILE__, __LINE__, __FUNCTION__, level, ##args); \
}
#define LOG_EMERG(args...) LOG_FORMAT((Log::LL_EMERG), ##args)
#define LOG_ALERT(args...) LOG_FORMAT((Log::LL_ALERT), ##args)
#define LOG_CRIT(args...) LOG_FORMAT((Log::LL_CRIT), ##args)
#define LOG_ERROR(args...) LOG_FORMAT((Log::LL_ERR), ##args)
#define LOG_WARN(args...) LOG_FORMAT((Log::LL_WARNING), ##args)
#define LOG_NOTICE(args...) LOG_FORMAT((Log::LL_NOTICE), ##args)
#define LOG_INFO(args...) LOG_FORMAT((Log::LL_INFO), ##args)
#define LOG_DEBUG(args...) LOG_FORMAT((Log::LL_DEBUG), ##args)
但是C++20推出modules后,在modules的设计中是不支持导出宏的,所以以上方式在完全模块化的程序中就不可行了。
对于日志输出位置C++20提供std::source_location 可以用于获取当前位置信息,对于日志的格式化可以使用C++11提供的变参数模板传递任意个参数到日志函数中,所以可以直接定义出这样的日志接口
export
template<class... Args>
void logDebug(const std::source_location&& loc, Args&&... args);
//使用方式如下
logDebug(std::source_location::current(), "test debug log");
但是这样每次都需要写std::source_location::current()就太麻烦了,日志接口应该只用关注输出内容就好了嘛,所以我们自然会想到C++函数参数可以提供默认值,所以我们自然会想到可以将以上接口修改为这样
//error
export
template<class... Args>
void logDebug( Args&&... args,const std::source_location&& loc = std::source_location::current() );
但是这样明显是有问题的,编译器不能推断该模板的参数,导致编译错误。
那么有什么方法可以使编译器能将模板类型推断为我们想要的类型呢?在C++17中提供了类型模板推导功能CTAD,该功能用于对类型缺失模板参数进行自动推导,也可以自定义推导指引协助编译器进行模板参数的推导,该功能只能用于类而不能用于普通函数,所以我们可以这样定义日志接口
export
template<class... Args>
struct LogDebug
{
public:
LogDebug(std::_Fmt_string<Args...> fmt, Args&&... args,
const std::source_location&& loc = std::source_location::current())
{
//输出日志,格式化可以采用std::format(fmt, std::forward<Args>(args)...);
//std::format会进行编译器检查,直接传std::string_view会导致编译失败
//https://stackoverflow.com/questions/72795189/how-can-i-wrap-stdformat-with-my-own-template-function
}
private:
static void* operator new(size_t) {}; // #1: 阻止创建单个对象
static void* operator new[](size_t) {}; // #2: 阻止创建对象数组
};
//类型推导指引
template <typename... Args>
LogDebug(std::_Fmt_string<Args...> fmt, Args&&...)->LogDebug<Args...>;
//使用方式如下
LogDebug("test debug has args {1} {0} ", 1, 0)
这样就完成了一个,在C++20 modules下可以正常使用的日志接口了,通过调用构造函数的方式实现日志输出。在栈上构造一个临时对象消耗可忽略不计,并在编译器as-if规则的优化下与普通函数调用无异。