bolov
bolov

Reputation: 75668

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

Upvotes: 7

Views: 261

Answers (3)

bolov
bolov

Reputation: 75668

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();
        }             
    }
};

Upvotes: 0

vitaut
vitaut

Reputation: 55524

You can't pass text and color information in separate arguments. A proper way to do this is to pass both in a single argument, possibly via a wrapper type:

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

struct ColoredText {
  std::string text;
  Color color;
};

template <>
struct std::formatter<ColoredText> { ... };

auto s = std::format("{}", ColoredText{"Hello", Color{255, 0, 0}});

This is essentially what the fmt::styled does except that it supports arbitrary types and not just std::string and captures them by reference.

I wouldn't recommend passing color components as separate arguments because it would mess up argument indexing and would be confusing to users.

Upvotes: 1

Barry
Barry

Reputation: 302643

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

All user-defined types are stored in a basic_format_arg as a handle, which basically only exposes a function pointer to do parsing and formatting. You can't pull out any values. This isn't really a supported use-case.

The closest you can achieve here is to... use a void*? You could make this work:

auto color = Color{100, 200, 300};
std::println("{:{}}", Text{"Hello"}, (void*)&color);

Then you can recover the void* and cast it to Color. This is neither type-safe (because... void*) nor lifetime-safe (because... void*). But it's really the only way to smuggle arbitrary types like this.

You could also simply take three ints, like this:

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

Nothing requires you to only pull one extra argument at a time. This at least is both type- and lifetime-safe, at the cost of just some weird-looking free floating arguments.

Upvotes: 0

Related Questions