rhalbersma / bit_set

Rebooting the std::bitset franchise
Boost Software License 1.0
40 stars 5 forks source link

Make bit_set compatible with std::print #20

Open rhalbersma opened 6 months ago

rhalbersma commented 6 months ago

xstd::bit_set<N, Block> is a bona fide std::bidirectional_range. We also provide a format_as overload for xstd::bit_set<N, Block>::proxy_reference. This makes it currently printable with fmt::print.

If and when P3070R0 gets adopted, we will also support std::print.

rhalbersma commented 6 months ago

backup of removed comment on P3070R0

This proposal by @vitaut is great! In fact, if anything, the paper undersells itself. In my experience, there are considerable other benefits from overloading format_as compared to specializing std::formatter for user-defined types.

  1. Users wanting to provide both fmtlib and the Standard Library with a convenient way to format their user-defined types currently need to provide specializations for both fmt::formatter and std::formatter. A single format_as function overload (in the user-defined type's own namespace!) can be hooked into by both fmtlib and the Standard Library (or any other formatting library build on top of them).
  2. Providing the correct formatting for nested classes inside class templates is a pain since the template parameters cannot be deduced. This applies to both the format_as function overload and the formatter class specialization.
  3. Case in point: the fmtlib sources mention this problem for std::vector<bool, Allocator>::reference and std::bitset<N>::reference and work around it by adding ad hoc constraints that try to infer "bitlikeness".
  4. Below a worked example of the general problem and ways around it:
namespace acme {

template<class T, class A>
struct container 
{
    struct proxy_reference
    {
        explicit(false) auto operator T() const; // implicitly convert to T
    };
};

template<class T, class A>
auto format_as(container<T, A>::proxy_reference const & ref) // error: cannot deduce the template arguments

} // namespace acme

template<class T, class A>
struct std::formatter<acme::container<T, A>::proxy_reference> // error: cannot deduce the template arguments

Factoring the reference class outside its container will enable template argument deduction for both the format_as overload and the formatter specialization

namespace acme {

template<class T, class A>
struct container_reference
{
    explicit(false) auto operator T() const; // implicitly convert to T
};

template<class T, class A>
struct container 
{
    using proxy_reference = container_reference<T, A>;
};

template<class T, class A>
auto format_as(container_reference<T, A> const& ref) // now compiles
{
    return static_cast<T>(ref);
}

} // namespace acme

template<class T, class A, class CharT>
struct std::formatter<acme::container_reference<T, A>> // now compiles
: std::formatter<T, CharT>
{
    template<class T, class A, class Context>
    auto format(acme::container_reference<T, A> const& ref, Context& ctx) 
    {
        return std::formatter<T, CharT>::format(static_cast<T>(ref), ctx);
    }
}

However, a much less intrusive refactoring of acme::container is possible by simply adding a friend declaration for format_as inside the container class that will be found by ADL

namespace acme {

template<class T, class A>
struct container 
{
    struct proxy_reference
    {
        explicit(false) auto operator T() const; // implicitly convert to T
    };

    friend auto format_as(proxy_reference const& ref) // will be found by ADL
    {
        return static_cast<T>(ref);
    }
};

} // namespace acme
  1. I have a fully worked bitset container with a nested proxy reference class available on GitHub and Compiler Explorer that adds just such a friend format_as overload for proxy references inside the container class.
#include <https://raw.githubusercontent.com/rhalbersma/bit_set/master/include/xstd/bit_set.hpp>
#include <fmt/ranges.h>

int main()
{
    // xstd::bit_set is a packed version of a std::set<int> container with proxy references/iterators
    fmt::println("{}", xstd::bit_set<20>({2, 3, 5, 7, 11, 13, 17, 19}));
}
  1. TLDR: overloading format_as is considerably more flexible and general than specializing formatter, especially for nested classes inside class templates. Please standardize such a customization point!