如何实现带编译期检查的捕获源位置的日志成员函数?
我想要实现一个和std::print原型一致的日志成员函数,同时能捕获源位置,示例调用如下:
// 可能输出: "file.cc:22: some format args" my_logger.info("some {} args", "format");
我需要编译期检查格式字符串,不想用std::string_view把格式错误推迟到运行时处理。这意味着format_string_with_location要对Args...泛化,用来存储std::format_string<Args...>。
我当前的实现代码如下:
template <typename... Args> struct format_string_with_location { consteval format_string_with_location( std::format_string<Args...> fmt, std::source_location location = std::source_location::current()) noexcept : format{fmt}, location{location} { } std::format_string<Args...> format; std::source_location location; }; template <typename... Args> format_string_with_location(char const*) -> format_string_with_location<Args...>; export class logger { public: template<class... Args> void info(format_string_with_location<Args...> fmt, Args&&... args) { std::println(fmt.format, std::forward<Args>(args)...); } };
但调用时编译失败,编译器把格式字符串识别为const char*,无法推导为format_string_with_location类型,报错信息如下:
logging.cxx:28:8: note: candidate template ignored: could not match 'format_string_with_location<Args...>' against 'const char *' 28 | void info(format_string_with_location<Args...> fmt, Args&&... args) | ^ logging_test.cxx:124:13: error: no matching member function for call to 'info' 124 | log.info("Log me");
我见过把info()改成模板结构体的变通方案,但这是成员函数,该方案不适用。当前使用Clang 19与C23,希望解决方案兼容所有C23合规编译器,也可接受C++26标准特性。请问是否有可行的解决办法,还是只能将类型检查推迟到运行时?
问题核心是模板参数推导歧义:编译器无法自动将const char*隐式转换为format_string_with_location<Args...>,因为Args...需要同时从格式字符串和后续参数推导,导致冲突。以下是两种合规的解决方式:
方法1:拆分模板参数消除推导歧义
调整logger::info的模板参数结构,将格式字符串对应的参数组与后续实参组分离,让编译器优先推导格式字符串的类型:
#include <format> #include <source_location> #include <iostream> template <typename... FmtArgs> struct format_string_with_location { consteval format_string_with_location( std::format_string<FmtArgs...> fmt, std::source_location loc = std::source_location::current()) noexcept : format(fmt), location(loc) {} std::format_string<FmtArgs...> format; std::source_location location; }; // 修正推导指引:基于std::format_string推导,而非直接用const char* template <typename... FmtArgs> format_string_with_location(std::format_string<FmtArgs...>) -> format_string_with_location<FmtArgs...>; export class logger { public: // 拆分模板参数,让FmtArgs...由格式字符串推导,Args...由后续实参推导 template <typename... FmtArgs, typename... Args> void info(format_string_with_location<FmtArgs...> fmt, Args&&... args) { std::println("{}:{}: {}", fmt.location.file_name(), fmt.location.line(), std::format(fmt.format, std::forward<Args>(args)...)); } };
调用时,编译器会先将const char*转换为std::format_string<FmtArgs...>,再通过推导指引生成format_string_with_location<FmtArgs...>,同时Args...由后续实参独立推导,彻底解决歧义。
方法2:直接利用C++23特性简化实现
无需额外结构体,直接让info函数接收std::format_string和默认源位置参数,天然支持编译期检查:
#include <format> #include <source_location> #include <iostream> export class logger { public: template <typename... Args> void info(std::format_string<Args...> fmt, Args&&... args, std::source_location loc = std::source_location::current()) { std::println("{}:{}: {}", loc.file_name(), loc.line(), std::format(fmt, std::forward<Args>(args)...)); } };
这种方式最简洁,完全符合C++23标准,所有合规编译器都支持,同时保留了格式字符串的编译期检查能力。
验证效果
调用代码示例:
int main() { logger log; log.info("some {} args", "format"); // 正常编译,输出带源位置的日志 // log.info("some {} args"); // 编译报错:格式参数不匹配(编译期检查生效) return 0; }
输出示例:
main.cpp:15: some format args
内容的提问来源于stack exchange,提问作者chmod222

