I'm using:
What is wrong with the call on line 68 in this godbolt. I tried to use format_as
as shown here, but it fails to compile.
I have an enum Codec
, with a custom fmt::formatter
implementation. I also have a class called CodecMask
, which acts as a bitset of Codec
s. CodecMask
has a public: static constexpr std::array<Codec, _> k_codecs = {...}
, which lists all valid codecs. I would like to provide a formatter implementation for CodecMask
, where the output looks like a list of all the included Codec
s.
For example:
auto mask = CodecMask();
mask.add(Codec::H265);
mask.add(Codec::JPEG);
fmt::println("mask: {}", mask);
// Expected output:
// mask: [H265, jpeg]
At first, I wrote this:
template<>
struct fmt::formatter<CodecMask> {
constexpr auto parse(format_parse_context & ctx)
-> format_parse_context::iterator {
auto it = ctx.begin();
if (it != ctx.end() && *it != '}') throw_format_error("invalid format");
return it;
}
auto format(CodecMask mask, format_context & ctx) const
-> format_context::iterator {
return fmt::format_to(
ctx.out(),
"{}",
CodecMask::k_codecs |
std::views::filter([&](auto c) { return mask.has(c); }));
}
};
I thought, "ooh, how pretty and functional".
Then, I remembered format_as
, from the fmt docs. I should be able to get all of this down to a one line function, like so:
auto format_as(CodecMask m) {
return CodecMask::k_codecs | std::views::filter([&](auto c) { return m.has(c); });
}
This will give the added bonus of supporting forwarding format specifier strings to the underlying types! Plus it's easy on the eyes.
Sadly, this does not compile. I do not understand why not. Is there a way to get my example to compile?
#include <array>
#include <string_view>
#include <ranges>
#include <fmt/format.h>
#include <fmt/ranges.h>
enum class Codec {
H264,
H265,
JPEG
};
template<>
struct fmt::formatter<Codec> : fmt::formatter<std::string_view> {
auto format(Codec c, format_context & ctx) const
-> format_context::iterator {
std::string_view name = "Unknown";
switch (c) {
using enum Codec;
case H264: name = "h264"; break;
case H265: name = "h265"; break;
case JPEG: name = "jpeg"; break;
}
return fmt::formatter<std::string_view>::format(name, ctx);
}
};
class CodecMask {
public:
static constexpr auto k_codecs = std::array{
Codec::H264,
Codec::H265,
Codec::JPEG,
};
constexpr bool has(Codec c) const {
auto m = codec_mask(c);
return (m_mask & m) == m;
}
constexpr void add(Codec c) {
m_mask |= codec_mask(c);
}
private:
static constexpr uint8_t codec_mask(Codec c) {
return 1 << static_cast<uint8_t>(c);
}
uint8_t m_mask;
};
auto format_as(CodecMask m) {
return CodecMask::k_codecs | std::views::filter([&](auto c) { return m.has(c); });
}
int main(void) {
// but this doesn't
auto mask = CodecMask();
mask.add(Codec::H265);
mask.add(Codec::JPEG);
// First println works, second one does not.
// What did I do wrong in my use of format_as?
fmt::println("mask: {}", CodecMask::k_codecs | std::views::filter([&](auto c) { return mask.has(c); }));
//fmt::println("mask: {}", mask);
}
@康桓瑋 mentioned in a comment that this may be a bug of fmt
. The following analysis is purely about why it doesn't work currently, regardless of whether it is a bug or not.
If we use clang 17 to compile the code we can see the following error message
/opt/compiler-explorer/libs/fmt/10.1.1/include/fmt/format.h:4061:18: error: no matching member function for call to 'format'
4061 | return base::format(format_as(value), ctx);
| ~~~~~~^~~~~~
/opt/compiler-explorer/libs/fmt/10.1.1/include/fmt/core.h:1308:22: note: in instantiation of function template specialization 'fmt::formatter<CodecMask>::format<fmt::basic_format_context<fmt::appender, char>>' requested here
1308 | ctx.advance_to(f.format(*static_cast<qualified_type*>(arg), ctx));
| ^
/opt/compiler-explorer/libs/fmt/10.1.1/include/fmt/core.h:1291:21: note: in instantiation of function template specialization 'fmt::detail::value<fmt::basic_format_context<fmt::appender, char>>::format_custom_arg<CodecMask, fmt::formatter<CodecMask>>' requested here
1291 | custom.format = format_custom_arg<
| ^
/opt/compiler-explorer/libs/fmt/10.1.1/include/fmt/core.h:2903:37: note: in instantiation of function template specialization 'fmt::format<CodecMask &>' requested here
2903 | return fmt::print(f, "{}\n", fmt::format(fmt, std::forward<T>(args)...));
| ^
/opt/compiler-explorer/libs/fmt/10.1.1/include/fmt/core.h:2912:15: note: in instantiation of function template specialization 'fmt::println<CodecMask &>' requested here
2912 | return fmt::println(stdout, fmt, std::forward<T>(args)...);
| ^
<source>:68:10: note: in instantiation of function template specialization 'fmt::println<CodecMask &>' requested here
68 | fmt::println("mask: {}", mask);
| ^
/opt/compiler-explorer/libs/fmt/10.1.1/include/fmt/ranges.h:546:8: note: candidate function template not viable: expects an lvalue for 1st argument
546 | auto format(range_type& range, FormatContext& ctx) const
| ^ ~~~~~~~~~~~~~~~~~
That means the issue is that base::format
is expecting a range_type&
as its first argument, but we have provided a rvalue. Indeed, the function format_as
is actually returning a rvalue. Usually, it should just work fine for rvalue if range_type
is const T
for some T
. This is handled by the following lines around line 412 in ranges.h
:
template <typename R>
using maybe_const_range =
conditional_t<has_const_begin_end<R>::value, const R, R>;
Where has_const_begin_end
is defined as
template <typename T>
struct has_const_begin_end<
T,
void_t<
decltype(detail::range_begin(std::declval<const remove_cvref_t<T>&>())),
decltype(detail::range_end(std::declval<const remove_cvref_t<T>&>()))>>
: std::true_type {};
We may roughly consider the detail::range_begin
and detail::range_end
as equivalent to std::ranges::begin
and std::ranges::end
. This is at least true for views. So the real issue is that the filter_view
is not const-iterable (Thanks to @cpplearner for correcting in the comments), and the reason for that is explained in this answer (also by @cpplearner).
Now, none of this matters if we pass the view directly to fmt::format
or fmt::println
, because the correct constructor for fmt::format_arg
will be called for our arguments, and that will handle both lvalues and rvalues correctly.
The solution is already pointed out in a comment: collect them in a vector before you return.
auto format_as(CodecMask m) {
return std::ranges::to<std::vector>(CodecMask::k_codecs | std::views::filter([&](auto c) { return m.has(c); }));
}