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

2

24

I have two broadly related questions.

I want to make a function that forwards the arguments to fmt::format (and later to std::format, when the support increases). Something like this:

#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;
}

Tested with 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

And tested with 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

In the library (core.h) it's declared something like this:

template <typename... T>
auto format(format_string<T...> fmt, T&&... args) -> std::string {
  // ...
}

The problem is that cppreference indicates that the type of the first parameter is unspecified. So

  • How can I make a function like my_print that passes the arguments to fmt::format and still catches the same kind of errors? Is there a more general way to do this for any kind of function?
  • How can I infer the type of a parameter of a function like std::format?

For more context, I want to make a function that calls to std::format conditionally, avoiding the formatting at all if the string won't be needed. If you know a better way to make this leave a comment, I'll be very greatful. However, my question about how to solve the general problem still stands.

Perihelion answered 6/8, 2021 at 2:8 Comment(4)
If you need a work around, you could probably use a macro for my_print.Chloral
fmt::format is not constexpr, so my_print cannot be neither.Shoal
@Shoal Oh, you're totally right. It comes from my previous attempts to solve the problem and I forget to remove it when I copied.Perihelion
Kind of a dupe of Keeping consteval-ness of function arguments in which the author of fmt gave the real answer, which is the same that Joseph did here.Lindsay
D
18

C++23 includes https://wg21.link/P2508R1, which exposes the format-string type used by std::format. This corresponds to the fmt::format_string type provided in libfmt. Example use might be:

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

Before C++23, you can use std::vformat / fmt::vformat instead.

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

The issue is that std::format (and the latest version of fmt::format) require a constant expression for the first parameter, as you have noticed. This is so that it can provide compile-time errors if the format string does not make sense for the passed-in arguments. Using vformat is the way to get around this.

Obviously this sidesteps the compile-time checking normally done for a format string: any errors with the format string will manifest as runtime errors (exceptions) instead.

I'm not sure if there's any easy way to circumvent this, apart from providing the format string as a template parameter. One attempt may be something like this:

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

If you really want to pass the parameter using "normal-ish" syntax, you could use a user-defined literal to construct a type that stores the string at compile time:

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

Daydream answered 6/8, 2021 at 2:25 Comment(11)
They want to continue to catch the same errors.Shirt
@Yakk-AdamNevraumont It will still throw errors at runtime, but that's a good point, I missed that; I'll adjust my answer.Daydream
Why do we need the first parameter to be a constant expression?Meggs
@康桓瑋 By the way that std::format etc. works. As I noted in the answer, this is desirable to catch errors with the format string at compile time. P22163 was the paper to make this change, if you want more details.Daydream
The correct answer was given by Joseph Thomson! This one is wrong! (It points out a workaround which isn't necessary.)Trypanosome
@BenjaminBuch Why do you think is wrong? The other answer does not respect an important point of my question: I want to be able to change to std::format at any time. An answer that only works for fmt::format doesn't satisfy my needs. The issue is that std::format doesn't specify the type of the format string. This answer present a workaround that works well for both cases. Do you know another way to do this with that restriction in mind?Perihelion
@Perihelion You are right, I didn't read your question carefully. Sorry!Trypanosome
I didn't realise that the format string is exposition-only in the standard. It's unfortunate that they overlooked this use case. I would consider it a defect. Better to just continue to use {fmt} until they fix it.Braise
@JosephThomson there is wg21.link/P2508 about this topicTrypanosome
@BenjaminBuch Woh, someone read my email :P Thanks for the link!Braise
since fmtlib-v10.1.0, i have to use fmt::vformat(fmt, fmt::make_format_args(args...));Conquer
B
17

It's the call to the constructor of fmt::format_string that needs to be a constant expression, so your function should take the format string as a fmt::format_string instead of a generic type:

template <typename... Args>
std::string my_print(fmt::format_string<Args...> s, Args&&... args)
{
    return fmt::format(s, std::forward<Args>(args)...);
}
Braise answered 20/10, 2021 at 13:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.