🛈 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:
r
means the color red{}
means read the color from the next argumentIn 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 });
}
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));
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();
}
}
};