20

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.

Daniel
  • 2,657
  • 1
  • 17
  • 22
  • If you need a work around, you could probably use a macro for `my_print`. – NathanOliver Aug 06 '21 at 04:07
  • `fmt::format` is not `constexpr`, so `my_print` cannot be neither. – Jarod42 Aug 06 '21 at 08:28
  • @Jarod42 Oh, you're totally right. It comes from my previous attempts to solve the problem and I forget to remove it when I copied. – Daniel Aug 07 '21 at 18:09
  • Kind of a dupe of [Keeping consteval-ness of function arguments](https://stackoverflow.com/questions/69596108/keeping-consteval-ness-of-function-arguments) in which the author of `fmt` [gave the real answer](https://stackoverflow.com/a/69596901/2757035), which is the same that [Joseph did here](https://stackoverflow.com/a/69647103/2757035). – underscore_d May 17 '22 at 18:31

2 Answers2

15

C++23 may include https://wg21.link/P2508R1, which will expose 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

N. Shead
  • 3,828
  • 1
  • 16
  • 22
  • 1
    They want to continue to catch the same errors. – Yakk - Adam Nevraumont Aug 06 '21 at 02:36
  • @Yakk-AdamNevraumont It will still throw errors at runtime, but that's a good point, I missed that; I'll adjust my answer. – N. Shead Aug 06 '21 at 02:39
  • Why do we need the first parameter to be a constant expression? – 康桓瑋 Aug 06 '21 at 02:44
  • 2
    @康桓瑋 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](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2216r3.html) was the paper to make this change, if you want more details. – N. Shead Aug 06 '21 at 02:46
  • The correct answer was given by Joseph Thomson! This one is wrong! (It points out a workaround which isn't necessary.) – Benjamin Buch Dec 01 '21 at 17:37
  • 1
    @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? – Daniel Dec 02 '21 at 01:01
  • @Daniel You are right, I didn't read your question carefully. Sorry! – Benjamin Buch Dec 02 '21 at 10:24
  • 1
    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. – Joseph Thomson Dec 02 '21 at 19:03
  • 2
    @JosephThomson there is https://wg21.link/P2508 about this topic – Benjamin Buch Mar 13 '22 at 18:54
  • 1
    @BenjaminBuch Woh, [someone](https://stackoverflow.com/users/2069064) read my email :P Thanks for the link! – Joseph Thomson Mar 14 '22 at 23:17
  • since fmtlib-v10.1.0, i have to use `fmt::vformat(fmt, fmt::make_format_args(args...));` – Maadiah Aug 13 '23 at 15:46
12

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)...);
}
Benjamin Buch
  • 4,752
  • 7
  • 28
  • 51
Joseph Thomson
  • 9,888
  • 1
  • 34
  • 38