如何创建一个将其参数转发给 fmt::format 以保持类型安全的函数?

How to create a function that forwards its arguments to fmt::format keeping the type-safeness?

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

我想创建一个函数,将参数转发给 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表示第一个参数的类型未指定。所以


对于更多上下文,我想创建一个有条件地调用 std::format 的函数,如果不需要字符串,则完全避免格式化。如果您知道更好的方法来发表评论,我将非常感激。但是,我关于如何解决一般问题的问题仍然存在。

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

如果你真的想使用“normal-ish”语法传递参数,你可以使用用户定义的文字来构造一个在编译时存储字符串的类型:

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

调用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)...);
}