struct Color
{
int r;
int g;
int b;
};
struct Text
{
std::string text;
};
Цель: создание специального средства форматирования текста, которое выводит цветной текст с помощью escape-последовательностей терминала. Для простоты здесь я просто вывожу цвет как своего рода HTML-тег.
Цвет текста анализируется в параметрах. Например. для простоты r означает красный (в моем реальном коде я анализирую полные значения RGB, а также параметры цвета текста и фона).
Это работает:
std::println("{:r}", Text{ "Hello" });
выводит:
Hello
Но это непрактично, если я не могу установить цвет из переменной. Я знаю, что параметр следует сделать динамическим, если он принадлежит к одному из типов std::basic_format_arg. Например. Я могу прочитать это как целое число:
std::println("{:{}}", Text{ "Hello" }, 100);
// ~~
// ^
// |
// {} here means read color from next arg
Но я не могу понять, как прочитать его как пользовательский тип, т.е. Цвет:
std::println("{:{}}", Text{ "Hello" }, Color{100, 200, 300});
std::basic_format_arg имеет тип дескриптора для пользовательских типов, но он не предоставляет имеющийся указатель const void* на объект. Он предоставляет только функцию форматирования, которая, насколько я вижу, бесполезна для моей цели.
Text форматировщик
template
struct std::formatter : std::formatter
{
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(ctx.next_arg_id());
pos += 2;
return pos;
}
return pos;
}
template
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(next_arg);
//color = Color{ next_arg_int, 0, 0 };
}
{
// as Color
using Handle = std::basic_format_arg::handle;
// cannot get next_arg as Color:
Handle& handle = my_get(next_arg);
// color = ???
// this actually works, but it's implementation defined
void* vptr_member = (reinterpret_cast(&handle))[0];
color = *reinterpret_cast(vptr_member);
}
}
std::string formatted = std::format(" {} ", color, text.text);
return std::formatter::format(formatted, ctx);
}
};
При анализе у меня есть всего два простых случая:
- r означает красный цвет
- {} означает чтение цвета из следующего аргумента
using Handle = std::basic_format_arg::handle;
// cannot get next_arg as Color:
Handle& handle = my_get(next_arg);
// color = ???
Оглядываясь вокруг, мне кажется, что это невозможно. Я очень надеюсь, что ошибаюсь.
Так как же мне использовать std::println("{:{}}", Text{ "Hello" }, Color{100, 200, 300 });?
Я действительно могу получить цвет, потому что, посмотрев на реализацию handle, я вижу, что частный const void* член — первый член в handle и что класс не является полиморфным:
// msvc implementation (libstc++ seems to be similar; but not libc++):
_EXPORT_STD template
class basic_format_arg {
public:
{
class handle {
private:
const void* _Ptr;
void(__cdecl* _Format)(basic_format_parse_context& _Parse_ctx, _Context& _Format_ctx, const void*);
// ...
};
// ..
};
void* vptr_member = (reinterpret_cast(&handle))[0];
color = *reinterpret_cast(vptr_member);
Но это зависит от реализации.
Полный код:
https://godbolt.org/z/46b5dWPTs
#include
#include
template
constexpr To my_get(const std::basic_format_arg& arg)
{
return std::visit_format_arg(
[](auto&& value) -> To
{
if constexpr (std::is_convertible_v)
return static_cast(value);
else
throw std::format_error{ "" };
},
arg
);
}
struct Color
{
int r;
int g;
int b;
};
template
struct std::formatter : std::formatter
{
template
constexpr auto format(Color color, Context& ctx) const
{
std::string formatted = std::format("Color ({}, {}, {})", color.r, color.g, color.b);
return std::formatter::format(formatted, ctx);
}
};
struct Text
{
std::string text;
};
template
struct std::formatter : std::formatter
{
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(ctx.next_arg_id());
pos += 2;
return pos;
}
return pos;
}
template
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(next_arg);
//color = Color{ next_arg_int, 0, 0 };
}
{
// as Color
using Handle = std::basic_format_arg::handle;
// cannot get next_arg as Color:
Handle& handle = my_get(next_arg);
// color = ???
// actually works, but its implementation defined
void* vptr_member = (reinterpret_cast(&handle))[0];
color = *reinterpret_cast(vptr_member);
}
}
std::string formatted = std::format(" {} ", color, text.text);
return std::formatter::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 });
}
Подробнее здесь: https://stackoverflow.com/questions/792 ... ustom-type