(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?
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);
}