`format_as` use with range types

233 Views Asked by At

I'm using:

  • C++23
  • fmt 10.1.1 (most recent release)
  • clang 17.0.6 (although this fails with clang and gcc trunk)

Summary

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.

In detail

I have an enum Codec, with a custom fmt::formatter implementation. I also have a class called CodecMask, which acts as a bitset of Codecs. 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 Codecs.

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?

Full, Reproducible Example

#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);
}
1

There are 1 best solutions below

0
On BEST ANSWER

@康桓瑋 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); }));
}