c++c++20range-v3fmt

Creating custom view::format for piping tuples to fmt::format


(Since I am working with c++20 I am relying on range-v3 for my ranges!)

I am trying to build a range view which pipes tuples (e.g. from a zip_view) to fmt::format automatically. The syntax should be intuitive, e.g.:

std::vector v1{"a",     "b",    "c",     "d"};
std::vector v2{"alpha", "beta", "gamma", "delta"};

fmt::print(
    "", 
    fmt::join(views::zip(v1, v2) | views::format("{}: {}"), "\n")
);

I know this can be achieved with a simple custom view::transform which extracts the relevant tuple args and passes them manually to fmt::format but I would like to eliminate this boilerplate. My attempts are:


#define FWD(x) std::forward<decltype(x)>(x)
#define FWD_UNPACK(x) std::forward<decltype(x)>(x)...

template <auto str>
struct formatter {
    constexpr auto operator()(auto&&... args) const {
        return fmt::format(str, FWD_UNPACK(args));    
    }
};

namespace ranges::views {

consteval auto format(auto&& format_str) {
    constexpr auto to_format = [=](auto&& args_tuple) {
        return std::apply(
            formatter<format_str>{},
            FWD(args_tuple));
    };
    return transform(to_format);
}

}

I have made format consteval to ensure that format_str would be a constant-time variable, since fmt::format requires a constexpr format string to be passed as first argument. However, I am getting the error:

<source>:25:23: error: non-type template argument is not a constant expression
   25 |             formatter<format_str>{},

(AUTHOR edit: This approach was never going to work <-- consteval functions do not make the parameters constexpr, see e.g. this SO post and the related proposal for this language feature)

So as a 2nd attempt I conceded an uglier syntax for a working method:

template <size_t N>
struct StringLiteral {
    constexpr StringLiteral(const char (&str)[N]) {
        std::copy_n(str, N, value);
    }
    char value[N];
};

template <StringLiteral str>
struct formatter {
    constexpr auto operator()(auto&&... args) const {
        return fmt::format(str.value, FWD_UNPACK(args));
    }
};

template <StringLiteral format_str>
consteval auto format2() {
    constexpr auto to_format = [=](auto&& args_tuple) {
        return std::apply(formatter<format_str>{}, FWD(args_tuple));
    };
    return transform(to_format);
}

which would need to be called like

fmt::println(
     "{}", fmt::join(views::zip(v1, v2) | views::format2<"{}: {}">(), "\n"));

which works and prints

a: alpha
b: beta
c: gamma
d: delta

but I am not happy with the unintuitive syntax of it.

Everything is captured in this godbolt example https://godbolt.org/z/x9G96Mncs.

Can someone explain how to make view::format("{}: {}") work?


Solution

  • It's easy to accomplish if you don't need to check the format string at compile time. Just use vformat instead of format.

    constexpr auto format(std::string_view format_str) {
        auto to_format = [=](auto&& args_tuple) {
            return std::apply([&](auto&... args) {
                return fmt::vformat(format_str, fmt::make_format_args(args...)); },
                args_tuple);
        };
        return std::views::transform(to_format);
    }
    

    But it is much tricker if you do want to add compile-time checking, although it can be done by performing the check in a consteval constructor.

    struct format_impl {
        std::string_view str;
    };
    
    template<class Tuple>
    struct format_checker;
    
    template<template<class...> class TupleOrPair, class... Args>
    struct format_checker<TupleOrPair<Args...>> {
        consteval format_checker(format_impl f) : str(f.str) {}
    
        fmt::format_string<Args...> str;
    };
    
    template<class R>
    auto operator|(R&& range, format_checker<std::ranges::range_reference_t<R>> f) {
        auto to_format = [=](auto&& args_tuple) {
            return std::apply([&](auto&... args) {
                return fmt::format(f.str, args...); },
                args_tuple);
        };
    
        return std::forward<R>(range) | std::views::transform(to_format);
    }
    
    consteval auto format(std::string_view format_str) {
        return format_impl(format_str);
    }