从高层次上讲,我想要实现的是一个具有与 std::print 相同原型的成员日志函数,它还可以捕获源位置,即:
// possible output: "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
位置。我认为这是因为模板参数没有以任何方式连接到要推导的const char*
参数:
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");
我曾见过将函数转变为的,以便该结构的构造函数同时接收格式字符串和 args 参数,帮助编译器推断出正确的类型,但这在这里不起作用,因为它是一个成员函数。info()
template <typename... Args> struct info
这是在 Clang 19 上使用 C++23,但可能的解决方案应该可以在所有兼容 C++23 的编译器上编译。如果即将推出一些超级实验性的 C++26 可以帮助解决这个问题,我也可以接受,只要它是标准 C++。
我是不是在与风车作战,应该将类型检查转移到运行时,还是可以通过某种方式解决这个问题?
2
最佳答案
2
和
export class logger {
public:
template<class... Args>
void info(format_string_with_location<Args...> fmt, Args&&... args);
};
Args
format_string_with_location<Args...>
应该从和(相同地)推导出来Args&&...
此外Args
不能从CTAD推断出const char*
。
您应该删除不需要的 CTAD,并且可能会使某些论点变得不可推论:
export class logger {
public:
template<class... Args>
void info(format_string_with_location<std::type_identity_t<Args>...> fmt, Args&&... args)
{
std::println(fmt.format, std::forward<Args>(args)...);
}
};
|
只是建议颠倒一下位置type_identity_t
。
中指定的方式,你会发现我们有:
// [format.fmt.string], class template basic_format_string
template<class charT, class... Args>
struct basic_format_string;
template<class... Args>
using format_string = basic_format_string<char, type_identity_t<Args>...>;
template<class... Args>
using wformat_string = basic_format_string<wchar_t, type_identity_t<Args>...>;
也就是说,format_string<Args...>
它本身已经是一个非推导论证。这使得它的用法相当好 – 你不必担心它:
template <class... Args>
void log(format_string<Args...> fmt, Args&&... args); // just works
所以你应该做一些类似的事情(请注意,我也在修复你的构造函数,否则是不可行的):
namespace impl {
template <class... Args>
struct format_string_with_location {
template <class T> requires std::constructible_from<std::format_string<Args...>, T const&>
consteval format_string_with_location(T const& fmt,
std::source_location loc = std::source_location::current())
: fmt(fmt), loc(loc) { }
std::format_string<Args...> fmt;
std::source_location loc;
};
}
template <typename... Args>
using format_string_with_location = impl::format_string_with_location<type_identity_t<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)...);
}
};
如何实际区分这两个名称(您在签名中使用的别名模板和实际实现该名称的类模板)取决于您。
。
4
-
1我喜欢这个,谢谢你的建议。这会让签名更简洁,不会太过千篇一律。
– -
您的演示已打印
[/app/example.cpp:8]
并[/app/example.cpp:25]
期待!
– -
修复版本:
– -
@MarekR 谢谢。
–
|
format_string_with_location<std::type_identity_t<Args>...> fmt
(在logger
)禁止类型推断(失败)。–
–
|