olafwx
olafwx

Reputation: 67

C++23 formatting mdspan as matrix

I'm trying to specify a formatter to print mdspan of rank 2 as a matrix, quite useful. I would like at least for element formatting to work, e.g. std::println("{::.2e}", matrix) would print some double matrix with format .2e for the doubles. The difficulty is this has to be done at compile time, I don't understand how this is generally done, "{:" + std::string{spec} + "}" here is not compile time so it fails. Would be useful to see how this is done or get general tips for implementing formatting.

#include <print>
#include <mdspan>
#include <format>

template<class T, class Extents, class Layout, class Accessor>
requires (Extents::rank() == 2)
struct std::formatter<std::mdspan<T, Extents, Layout, Accessor>> {

    std::string_view spec;
    
    constexpr auto parse(format_parse_context& ctx) {
        auto it = ctx.begin();
        auto end = ctx.end();
        spec = std::string_view{it, end};
        return end;
    }
    
    auto format(const std::mdspan<T, Extents, Layout, Accessor>& md, format_context& ctx) const {
        auto out = ctx.out();
        *out++ = '[';
        for (size_t i = 0; i < md.extent(0); ++i) {
            if (i > 0) {
                *out++ = '\n';
                *out++ = ' ';
            }
            *out++ = '[';
            for (size_t j = 0; j < md.extent(1); ++j) {
                if (j > 0)
                    *out++ = ' ';
                if (spec.empty())
                    out = std::format_to(out, "{}", md[i, j]);
                else
                    out = std::format_to(out, "{:" + std::string{spec} + "}", md[i, j]);
            }
            *out++ = ']';
        }
        *out++ = ']';
        
        return out;
    }
};

int main() {
    std::println("{::.2e}", std::mdspan((double []) {1.1111, 2, 3, 4}, 2, 2));
}

Upvotes: 1

Views: 93

Answers (1)

Milster
Milster

Reputation: 681

In the past it worked best for me to use as much already implemented functionality as possible, especially when it comes to std::formatter. One way to avoid having to do the heavy lifting would be the following changes:

  1. Inherit from std::formatter<T> to benefit from an already implemented parse functionallity.
  2. Use std::formatter<T>::format(md[i,j], ctx); to take care of the number formatting and also advance the context output iterator.

A working example could look like this:

#include <print>
#include <mdspan>
#include <format>  

template<class T, class Extents, class Layout, class Accessor>
requires (Extents::rank() == 2)
struct std::formatter<std::mdspan<T, Extents, Layout, Accessor>> : std::formatter<T>
{
    auto format(const std::mdspan<T, Extents, Layout, Accessor> & md, format_context & ctx) const {
        auto out = ctx.out();
        *out++ = '[';
        for (size_t i = 0; i < md.extent(0); ++i) 
        {
            if (i > 0) 
            {
                *out++ = '\n';
                *out++ = ' ';
            }
            *out++ = '[';
            for (size_t j = 0; j < md.extent(1); ++j) 
            {
                if (j > 0)
                {
                    *out++ = ' ';
                }
                std::formatter<T>::format(md[i, j], ctx);
            }
            *out++ = ']';
        }
        *out++ = ']';

        return out;
    }
};

int main() 
{
    auto arr = std::array{ 1.1111, 2.0, 3.0, 4.0 };
    auto span = std::mdspan(arr.data(), 2, 2);
    std::println("{:.2e}", span);
}

Additionally, the iterator returned by parse should return to ctx.begin() if .begin() == .end() - if not it then it should point to the first unmatched character if it is }, otherwise it should throw a format_error. Your implementation points beyond the last character and will/might lead to compile errors.

Upvotes: 1

Related Questions