如何创建一个函数,它将其参数传递给fmt::format并保持类型安全性?

20

我有两个广泛相关的问题。

我想要创建一个函数,将参数转发到fmt::format (以后当支持增加时也可以转发到std::format)。类似于这样:

#include <iostream>
#include <fmt/core.h>

constexpr auto my_print(auto&& fmt, auto&&... args) {
    // Error here!
    //         ~~~~~~~~v~~~~~~~~
    return fmt::format(fmt, args...);
}

int main() {
    std::cout << my_print("{}", 42) << std::endl;
}

使用gcc 11.1.0进行测试:

In instantiation of ‘constexpr auto my_print(auto:11&&, auto:12&& ...) [with auto:11 = const char (&)[3]; auto:12 = {int}]’:
error: ‘fmt’ is not a constant expression

并使用clang 12.0.1进行了测试:

error: call to consteval function 'fmt::basic_format_string<char, int &>::basic_format_string<char [3], 0>' is not a constant expression
在图书馆中(core.h)声明了如下内容:
template <typename... T>
auto format(format_string<T...> fmt, T&&... args) -> std::string {
  // ...
}

问题在于cppreference指示第一个参数的类型是未指定的。因此,

  • 我该如何创建一个类似于my_print的函数,将参数传递给fmt::format,同时捕获相同类型的错误?是否有一种更普遍的方法来为任何类型的函数执行此操作?
  • 我该如何推断出像std::format这样的函数参数的类型?

更具体地说,我想创建一个条件调用std::format的函数,如果不需要字符串,则完全避免格式化。如果您知道更好的方法,请留言,我将非常感激。但是,我关于如何解决通用问题的问题仍然存在。


如果你需要一个解决方法,你可能可以使用my_print的宏。 - NathanOliver
fmt::format 不是 constexpr,所以 my_print 也不能是。 - Jarod42
@Jarod42 哦,你说得对。这是来自我之前尝试解决问题的代码,当我复制时忘记删除它了。 - Daniel
这是一个与“保持函数参数的consteval性”(Keeping consteval-ness of function arguments)非常相似的问题,fmt的作者在这里给出了真正的答案,这个答案和Joseph在这里给出的答案是一样的。 - underscore_d
2个回答

15

C++23可能会包含https://wg21.link/P2508R1,该功能将公开std::format使用的格式字符串类型。这对应于libfmt提供的fmt::format_string类型。示例用法可能如下:

template <typename... Args>
auto my_print(std::format_string<Args...> fmt, Args&&... args) {
  return std::format(fmt, std::forward<Args>(args)...);
}

在 C++23 之前,你可以使用 std::vformat / fmt::vformat 替代。
template <typename... Args>
auto my_print(std::string_view fmt, Args&&... args) {
    return std::vformat(fmt, std::make_format_args(std::forward<Args>(args)...));
}

https://godbolt.org/z/5YnY11vE4

问题在于,正如你所注意到的那样,std::format(以及最新版本的fmt::format)要求第一个参数是常量表达式。这是为了在格式字符串对传入参数没有意义时提供编译时错误。使用vformat可以解决这个问题。
显然,这种方法规避了通常针对格式字符串进行的编译时检查:任何格式字符串的错误都会表现为运行时错误(异常)。
我不确定是否有任何简单的方法来规避这个问题,除了将格式字符串作为模板参数提供。一种尝试可能是像这样:
template <std::size_t N>
struct static_string {
    char str[N] {};
    constexpr static_string(const char (&s)[N]) {
        std::ranges::copy(s, str);
    }
};

template <static_string fmt, typename... Args>
auto my_print(Args&&... args) {
    return std::format(fmt.str, std::forward<Args>(args)...);
}

// used like

my_print<"string: {}">(42);

https://godbolt.org/z/5GW16Eac1

如果您真的想使用“正常”的语法传递参数,您可以使用用户定义字面量来构造一个类型,在编译时存储字符串:

template <std::size_t N>
struct static_string {
    char str[N] {};
    constexpr static_string(const char (&s)[N]) {
        std::ranges::copy(s, str);
    }
};

template <static_string s>
struct format_string {
    static constexpr const char* string = s.str;
};

template <static_string s>
constexpr auto operator""_fmt() {
    return format_string<s>{};
}

template <typename F, typename... Args>
auto my_print(F, Args&&... args) {
    return std::format(F::string, std::forward<Args>(args)...);
}

// used like

my_print("string: {}"_fmt, 42);

https://godbolt.org/z/dx1TGdcM9


2
顺便提一下,std::format等的工作方式。正如我在答案中所指出的那样,这是为了在编译时捕获格式字符串错误而希望实现的。如果您想了解更多详细信息,请参阅P22163 - N. Shead
1
@BenjaminBuch 你认为哪里出了问题?另一个答案没有考虑到我问题的一个重要点:我想随时能够更改为 std::format。只适用于 fmt::format 的答案不能满足我的需求。问题在于 std::format 没有指定格式字符串的类型。这个答案提供了一个解决方法,适用于两种情况。你知道其他考虑到这个限制的方法吗? - Daniel
1
我没有意识到格式化字符串在标准中仅限于展示。很遗憾,他们忽略了这种用例。我认为这是一个缺陷。最好继续使用 {fmt} 直到他们修复它。 - Joseph Thomson
2
@JosephThomson 这个主题有https://wg21.link/P2508 - Benjamin Buch
1
@BenjaminBuch 哇,某人看了我的邮件 :P 感谢提供链接! - Joseph Thomson
显示剩余6条评论

14

需要将fmt::format_string的构造函数调用作为常量表达式,因此您的函数应该将格式字符串作为fmt::format_string类型而不是一般类型进行处理:

template <typename... Args>
std::string my_print(fmt::format_string<Args...> s, Args&&... args)
{
    return fmt::format(s, std::forward<Args>(args)...);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接