Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c++

c++20

fmt

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.

like image 426
Daniel Avatar asked Mar 01 '23 12:03

Daniel


2 Answers

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

like image 53
N. Shead Avatar answered Apr 08 '23 22:04

N. Shead


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)...);
}
like image 29
Joseph Thomson Avatar answered Apr 08 '23 20:04

Joseph Thomson