c++c++20fmtstdformat

How to read in std::format a dynamic option if that option is of a custom type


🛈 This is a simplified example of what I have.


struct Color
{
    int r;
    int g;
    int b;
};

struct Text
{
    std::string text;
};

Goal: building a custom formatter for Text that outputs colored text via terminal escape sequences. For simplicity here I am just outputting the color as sort of a html tag.

The color of the text is parsed in the options. E.g. for simplicity r means red (in my actual code I parse full rgb values and both text and background color options).
This works:

std::println("{:r}", Text{ "Hello" });

it outputs:

<Color (255, 0, 0)> Hello </color>

But it is impractical if I can't set the color from a variable. I know to make the option dynamic if it's one of the types from std::basic_format_arg. E.g. I can read it as an int:

std::println("{:{}}", Text{ "Hello" }, 100);
//              ~~
//              ^
//              |
//              {} here means read color from next arg

But I can't figure out how to read it as a custom type, i.e. Color:

std::println("{:{}}", Text{ "Hello" }, Color{100, 200, 300});

std::basic_format_arg has a handle type for custom types, but it doesn't expose the const void* pointer it has to the object. It only exposes a format function which as far as I can see is useless for my purpose.

Text formatter

template <>
struct std::formatter<Text> : std::formatter<std::string_view>
{
    int m_color_dynamic_id = -1;
    Color m_color{};

    constexpr auto parse(format_parse_context& ctx)
    {
        auto pos = ctx.begin();

        if (*pos == 'r')
        {
            // parse r as color red
            m_color = Color{ 255, 0, 0 };
            ++pos;
            return pos;
        }

        if (pos[0] == '{' && pos[1] == '}')
        {
            // parse {} as dynamic option for color
            m_color_dynamic_id = static_cast<int>(ctx.next_arg_id());
            pos += 2;
            return pos;
        }

        return pos;
    }

    template <class FormatContext>
    constexpr auto format(Text text, FormatContext& ctx) const
    {
        Color color = m_color;

        if (m_color_dynamic_id >= 0)
        {
            auto next_arg = ctx.arg(m_color_dynamic_id);

            {
                // as int it works:
                //int next_arg_int = my_get<int>(next_arg);
                //color = Color{ next_arg_int, 0, 0 };
            }
            {
                // as Color
                using Handle = std::basic_format_arg<std::format_context>::handle;

                // cannot get next_arg as Color:
                Handle& handle = my_get<Handle&>(next_arg);
                // color = ???


                // this actually works, but it's implementation defined
                void* vptr_member = (reinterpret_cast<void**>(&handle))[0];
                color = *reinterpret_cast<Color*>(vptr_member);
            }
        }

        std::string formatted = std::format("<{}> {} </color>", color, text.text);
        return std::formatter<std::string_view>::format(formatted, ctx);
    }
};

In parse I have just two simple cases:

In format if I need to read the color from the next argument I use visit_format_arg. As a test it works with int. But with Color I can only get the handle, not the Color type:

using Handle = std::basic_format_arg<std::format_context>::handle;

// cannot get next_arg as Color:
Handle& handle = my_get<Handle&>(next_arg);
// color = ???

Looking around it seems to me it can't be done. I really hope I am wrong.

So how can I have std::println("{:{}}", Text{ "Hello" }, Color{100, 200, 300});?


I can actually get the Color because by looking at the implementation of handle I see that the private const void* member is the first member in the handle class and that the class is not polymorphic:

// msvc implementation (libstc++ seems to be similar; but not libc++):

_EXPORT_STD template <class _Context>
class basic_format_arg {
public:
{
       class handle {
       private:
              const void* _Ptr;
              void(__cdecl* _Format)(basic_format_parse_context<_CharType>& _Parse_ctx, _Context& _Format_ctx, const void*);
             // ...
       };
       // ..
};

void* vptr_member = (reinterpret_cast<void**>(&handle))[0];
color = *reinterpret_cast<Color*>(vptr_member);

But this is implementation specific.


Full code:

https://godbolt.org/z/46b5dWPTs

#include <format>
#include <print>

template <class To, class Context>
constexpr To my_get(const std::basic_format_arg<Context>& arg)
{
    return std::visit_format_arg(
        [](auto&& value) -> To
    {
        if constexpr (std::is_convertible_v<decltype(value), To>)
            return static_cast<To>(value);
        else
            throw std::format_error{ "" };
    },
        arg
    );
}

struct Color
{
    int r;
    int g;
    int b;
};

template <>
struct std::formatter<Color> : std::formatter<std::string_view>
{
    template <class Context>
    constexpr auto format(Color color, Context& ctx) const
    {
        std::string formatted = std::format("Color ({}, {}, {})", color.r, color.g, color.b);
        return std::formatter<std::string_view>::format(formatted, ctx);
    }
};

struct Text
{
    std::string text;
};

template <>
struct std::formatter<Text> : std::formatter<std::string_view>
{
    int m_color_dynamic_id = -1;
    Color m_color{};

    constexpr auto parse(format_parse_context& ctx)
    {
        auto pos = ctx.begin();

        if (*pos == 'r')
        {
            // parse r as color red
            m_color = Color{ 255, 0, 0 };
            ++pos;
            return pos;
        }

        if (pos[0] == '{' && pos[1] == '}')
        {
            // parse {} as dynamic option for color
            m_color_dynamic_id = static_cast<int>(ctx.next_arg_id());
            pos += 2;
            return pos;
        }

        return pos;
    }

    template <class FormatContext>
    constexpr auto format(Text text, FormatContext& ctx) const
    {
        Color color = m_color;

        if (m_color_dynamic_id >= 0)
        {
            auto next_arg = ctx.arg(m_color_dynamic_id);

            {
                // as int it works:
                //int next_arg_int = my_get<int>(next_arg);
                //color = Color{ next_arg_int, 0, 0 };
            }

            {
                // as Color
                using Handle = std::basic_format_arg<std::format_context>::handle;

                // cannot get next_arg as Color:
                Handle& handle = my_get<Handle&>(next_arg);
                // color = ???


                // actually works, but its implementation defined
                void* vptr_member = (reinterpret_cast<void**>(&handle))[0];
                color = *reinterpret_cast<Color*>(vptr_member);
            }
        }

        std::string formatted = std::format("<{}> {} </color>", color, text.text);
        return std::formatter<std::string_view>::format(formatted, ctx);
    }
};


int main()
{
    std::println("{:r}", Text{ "Hello" });
    // std::println("{:r}", Text{ "Hello" }, 100); // works with next_arg_int
    std::println("{:{}}", Text{ "Hello" }, Color{ 100, 200, 300 });
}


Solution

  • Unfortunately it looks like this is not supported.

    One option I explored is to convert the Color to string and parse that:

    std::string dyn_opt(Color c) {  return std::format("{}", c); }
    
    // "baked" color:
    std::println("{:fg#FF00FF}", "Hello"_text};
    
    // "baked" color in dynamic argument
    std::println(":fg{}", "Hello"_text, "#FF00FF"sv);
    
    // dynamic argument with color variable
    Color magenta{255, 0, 255};
    std::println(":fg{}", "Hello"_text, dyn_opt(pink));
    

    Implementation

    Since I already have a color parser for ctx I modified it to work on both on format_parse_context range and std::string_view:

    /*
    * @brief parse a color at the beginning of str
    * in format '#RRGGBB', '(r, g, b)' or 'Color(r, g, b)';
    * Avance str if successful (trim the parsed color)
    */
    template <ParsableRange R>
    std::expected<Color, std::string> parse_color(R& str);
    
    template <>
    struct std::formatter<Text> : std::formatter<std::string_view>
    {
    
        constexpr auto parse(format_parse_context& ctx)
        {
            auto ctx_range = std::ranges::subrange{ctx.begin(), ctx.end()};
    
            // .. // parse fg
    
    
            if (/*{}*/)
            {
                 // parse {} as dynamic option for foreground color
                 m_fg_dynamic_id = static_cast<int>(ctx.next_arg_id());
            } else
            {
                 // parse color here
                 auto expected_fg_color = parse_color(ctx_range);           
            }
        }
    
        template <class FormatContext>
        constexpr auto format(Text text, FormatContext& ctx) const
        {
                 
            if (m_color_dynamic_id >= 0)
            {
                auto next_arg = ctx.arg(m_color_dynamic_id);
                std::string_view fg_arg_str = my_get<std::string_view>(fg_arg);
                Color fg_color = parse_color(fg_arg_str).value();
            }             
        }
    };