Здравствуйте, rg45, Вы писали:
S>>Простите, общие слова не интересны. Конкретные примеры -- да, абстрактные рассуждения о том, как у кого-то где-то что-то -- нет.
R>А чего вы решаете за всех что интересно, что нет?
На публичном форуме вы ответили мне, и мне же высказали, что я проигнорировал часть ваших слов. В этом общении между мной и вами я решаю что интересно мне, а вы -- что вам.
Вроде бы по другому и не бывает?
R>Это ж форум, а не личная переписка.
Именно. Я же не затыкаю вам рот, не указываю что говорить, а что нет.
То, что написано вами здесь и остается и если кто-то заинтересуется тем, что вы высказали, то этот кто-то сможет продолжить разговор с заинтересовавшего его (её) места.
Или вот еще интересный сценарий: подход с функциями позволяет, например, легко определить один общий форматтер (или фиксированный набор форматтеров) для всех пользовательских типов, объединенных общим пространством имен. Это может быть как простая шаблонная функция, так и просто using declaration, вносящее имя функции, определенной в каком-то другом пространстве имен. (И разумеется, никто при этом не запрещает использовать в этом же пространсве имен и другие, более специальные форматтеры наряду с общим, если нужно). В подходе со специализациями шаблона класса придется написать тонны кода и постоянно расширять при добавлении новых типов.
Здравствуйте, rg45, Вы писали:
R>Или вот еще интересный сценарий: подход с функциями позволяет, например, легко определить один общий форматтер (или фиксированный набор форматтеров) для всех пользовательских типов, объединенных общим пространством имен. Это может быть как простая шаблонная функция, так и просто using declaration, вносящее имя функции, определенной в каком-то другом пространстве имен. (И разумеется, никто при этом не запрещает использовать в этом же пространсве имен и другие, более специальные форматтеры наряду с общим, если нужно). В подходе со специализациями шаблона класса придется написать тонны кода и постоянно расширять при добавлении новых типов.
Да, все богатые возможности перегрузки функций идут лесом. Сейчас, переписывая форматеры своих типов я постоянно налетаю на грабли ограничении специализации класса.
Здравствуйте, rg45, Вы писали: R>Так о том и речь, что их нет, а вместо функций — шаблон класса std::formatter. А если бы std::format для кастомизации использовал бы не шаблон класса со специализациями, а неквалифицированный вызов функции с каким-то предопределенным именем, это могло бы дать ряд преимуществ:
Могу ошибаться, но может быть есть способ объединить лучшее из двух миров?
Идея в том, что сейчас fmtlib просто инстанциирует тип formatter там, где ей это нужно. Наверное, можно создавать formatter не напрямую, а вызвав функцию-фабрику, которая уже и возвратит экземпляр formatter.
Функция-фабрика будет свободной функцией, которая может быть определена и в пространстве имен пользователя.
Небольшой набросок слепленный на коленке для того, чтобы продемонстрировать идею:
Не знаю насколько реально все это впилить в fmtlib, т.к. там тип formatter нужен для вложенного типа basic_format_context::formatter_type. Но если реально, то можно расширить функциональность fmtlib сохранив при этом совместимость с уже имеющимся кодом, в котором делаются специализации fmt::formatter. И, соответственно, подобный же трюк можно будет проделать и с std::format.
Здравствуйте, so5team, Вы писали:
S>Могу ошибаться, но может быть есть способ объединить лучшее из двух миров?
S>Идея в том, что сейчас fmtlib просто инстанциирует тип formatter там, где ей это нужно. Наверное, можно создавать formatter не напрямую, а вызвав функцию-фабрику, которая уже и возвратит экземпляр formatter.
S>Функция-фабрика будет свободной функцией, которая может быть определена и в пространстве имен пользователя.
Вот такое решение мне нравится гораздо больше. С одной стороны, обеспечивается обратная совместимость с уже написанными форматтерами в виде специализаций. При этом появляются новые возможности, о которых говорилось выше:
можно определять пользовательские форматтеры в одном пространстве имен с пользовательскими типами,
можно предоставлять форматтеры по умолчанию для целых пространств имен
можно повторно использовать предопределенные наборы форматтеров для новых типов при помощи using-declarations
еще одна важная деталь: фабрика форматтера получает параметром объект и теперь форматтер может быть чувствителен к свойствам объекта, для которого он создается, а не просто конструироваться по дефолту, как раньше. Например, форматтер может использовать полиморфно идиому pImpl и создавать различные внутренние имплементации форматирования в зависимости от каких-то runtime свойств объекта.
В конце концов, с таким подходом пользователь может самостоятельно осуществить переход от форматтеров-классов к форматтерам-функциям, если очень хочется. А может использовать какие-то более сложные гибридные схемы. Вместо одной точки кастомизации теперь их целых три: специализация библиотечного шаблона класса; фабрики форматтеров; пользовательский форматтер. Пользовательский форматтер вообще можно навернуть с какой угодно сложностью и обеспечить еще 100500 точек кастомизации, специфичных для прикладной области. В целом гибкость и юзабельность существенно лучше, чем в текущем подходе со специализациями.
Здравствуйте, so5team, Вы писали:
S>Идея в том, что сейчас fmtlib просто инстанциирует тип formatter там, где ей это нужно. Наверное, можно создавать formatter не напрямую, а вызвав функцию-фабрику, которая уже и возвратит экземпляр formatter.
S>Функция-фабрика будет свободной функцией, которая может быть определена и в пространстве имен пользователя.
S>
Можно сделать еще один шажок навстречу пользователю и для основного шаблона formatter предоставить определение, которое будет делегировать вызов (через ADL) пользовательстким функциям parse и format. Тогда у пользователя будет три возможности: специализировать библиотечный formatter, определить собственный форматтер с фабрикой, либо просто определить функции parse и format в том же пространстве имен, что и пользовательский тип (типы). Это для тех типов, которые не требуют передачи состояния между parse и format. Что-то мне подсказывает, что таких типов будет большинство.
Здравствуйте, _NN_, Вы писали:
_NN>Может в https://stdcpp.ru стоит написать ? _NN>А там гляди и лет через 10 в стандарт придёт.
ИМХО, в данном случае логичнее было бы идти через модификацию fmtlib. Если такое изменение в принципе возможно, то внедрить его будет гораздо быстрее, затем можно будет собрать актуальный фидбэк от пользователей, и уже на основании фидбэка обновлять стандарт. Тем более, что скоуп C++23 уже вроде как зафиксировали, новое туда добавлять не будут. А до C++26 можно будет потренироваться на кошках.
Только вот в ближайшее пару месяцев у меня лично возможности заняться pull-request-ом для fmtlib не будет
Здравствуйте, Videoman, Вы писали:
V>Наверное вопрос в этом, как выкрутится теперь. Ситуация простая. У std::formatter есть специализация для вывода std::chrono::duration<>. У меня в библиотеках везде используется своя специализация времени:
я автоматически лишаюсь возможности вызвать стандартную реализацию и вынужден повторять всю логику заново. Это не дело, я считаю
Смотрю код новой библиотеки логирования xtr. В ней чтобы форматировать `std::timespec` по-своему, автор сделал свой тип простым наследованием от стандартной структуры:
Здравствуйте, rg45, Вы писали:
R>В конце концов, с таким подходом пользователь может самостоятельно осуществить переход от форматтеров-классов к форматтерам-функциям, если очень хочется. А может использовать какие-то более сложные гибридные схемы. Вместо одной точки кастомизации теперь их целых три: специализация библиотечного шаблона класса; фабрики форматтеров; пользовательский форматтер. Пользовательский форматтер вообще можно навернуть с какой угодно сложностью и обеспечить еще 100500 точек кастомизации, специфичных для прикладной области. В целом гибкость и юзабельность существенно лучше, чем в текущем подходе со специализациями.
Если можно подсунуть функцию фабрику-форматеров в своё пространство имен, то при желании, можно создать у себя универсальный форматтер для всех типов, который уже будет вызывать специализированные функции форматеры или реализовывать любые другие практики принятые в конкретном проекте.
Здравствуйте, PM, Вы писали:
PM>Смотрю код новой библиотеки логирования xtr. В ней чтобы форматировать `std::timespec` по-своему, автор сделал свой тип простым наследованием от стандартной структуры:
Если оставить за скобками то, что его подход с наследованием является очень опасной практикой, автор решает ту же проблему, что и я — у меня в мультимедиа время, это дискретная величина и мне нельзя выводить его с округлением, как это предлагает делать libfmt. А от чего будет наследоваться автор, если ему понадобиться изменить или расширить форматирование int ?
Здравствуйте, Videoman, Вы писали:
V>Если можно подсунуть функцию фабрику-форматеров в своё пространство имен, то при желании, можно создать у себя универсальный форматтер для всех типов, который уже будет вызывать специализированные функции форматеры или реализовывать любые другие практики принятые в конкретном проекте.
Но есть нюанс: все это будет работать только для типов, определенных пользователем (и производных от них, наподобие std::vector<MyClass>). Настройка форматирования для встроенных типов по-прежнему остается нерешенной проблемой, даже в этом подходе.
Здравствуйте, Videoman, Вы писали:
PM>>Смотрю код новой библиотеки логирования xtr. В ней чтобы форматировать `std::timespec` по-своему, автор сделал свой тип простым наследованием от стандартной структуры:
V>Если оставить за скобками то, что его подход с наследованием является очень опасной практикой, автор решает ту же проблему, что и я — у меня в мультимедиа время, это дискретная величина и мне нельзя выводить его с округлением, как это предлагает делать libfmt. А от чего будет наследоваться автор, если ему понадобиться изменить или расширить форматирование int ?
По-моему, наследование для создания другого типа — вполне безопасная, рабочая альтернатива варианту с strong typedef, который предложил коллега so5team.
Думаю, подавляющее большинство сценариев использования — форматировать пользовательские типы, и это вполне работает. Надеюсь автор fmtlib сможет расширить ее для вашего случая, но в std::format это навряд ли уже случится.
В общем для всех тех, кто активно участвовал в дискуссии, критиковал и помогал решать вопрос с форматированием, наталкивая на правильные решения — возможно будет интересно! Кажется вот возможное решение:
#include <string>
#include <iostream>
#include <typeinfo>
#include <utility>
#include <fmt/core.h>
#include <fmt/chrono.h>
#include <fmt/xchar.h>
namespace my
{
// rg45 forward wrappertemplate <typename T>
struct wrapper
{
T t;
explicit wrapper(T&& t) : t(std::forward<T>(t)) {}
T&& forward() { return std::forward<T>(t); }
};
template <typename T>
wrapper<T> wrap(T&& t) { return wrapper<T>(std::forward<T>(t)); }
// own format wrappertemplate<typename... Params>
std::string format(const std::string_view& format_string, Params&& ...params)
{
return fmt::format(format_string, wrap(params)...);
}
} // namespace my
// dispatch formattertemplate<typename char_t, typename type_t>
class fmt::formatter<my::wrapper<type_t>, char_t>
{
public:
constexpr auto parse(basic_format_parse_context<char_t>& ctx) -> decltype(ctx.begin())
{
auto it = ctx.begin();
auto end = ctx.end();
it = std::find(it, end, '}');
if (it != end && *it != '}')
throw format_error("invalid format");
return it;
}
template<typename it_t>
auto format(my::wrapper<type_t>& data, basic_format_context<it_t, char_t>& ctx) const -> decltype(ctx.out())
{
return format_to(ctx.out(), "***{}***", data.forward());
}
};
// testint main()
{
std::cout << my::format("{} | {} | {:.e}", 1, "str", 2.5);
}
***1*** | ***str*** | ***2.5***
Это как идея и код для подумать. Думаю тут всё понятно. Можно из форматера дергать свои функции с нужной себе логикой, кастомизировать стандартные и примитивные типы, вызывать std::форматтеры, если надо и т.д.
Хорошо бы проверить будет ли такое работать с std::format C++20.
При вызове wrap разве не нужно форвардить? Если не форвардить, то все params всегда будут интерпретироваться как lvalue выражения и врапиться по lvalue ссылкам. Если так и было задумано, то с форвардингом можно было вообще не заморачиваться, просто биндить всегда по константрой lvalue ссылке.
Здравствуйте, rg45, Вы писали:
R>При вызове wrap разве не нужно форвардить? Если не форвардить, то все params всегда будут интерпретироваться как lvalue выражения и врапиться по lvalue ссылкам. Если так и было задумано, то с форвардингом можно было вообще не заморачиваться, просто биндить всегда по константрой lvalue ссылке.
Нужно конечно! Это я пропустил, спешил проверить идею .
Здравствуйте, Videoman, Вы писали:
V>Это как идея и код для подумать. Думаю тут всё понятно. Можно из форматера дергать свои функции с нужной себе логикой, кастомизировать стандартные и примитивные типы, вызывать std::форматтеры, если надо и т.д.
То есть, мы враппим std:/fmt:: format и дальше пользуемся только своим my::format. А formatter специализируем только один раз и пишем его так, что он делегирует вызовы пользовательским функциям с использованием ADL. Пожалуй, это будет работать, причем, даже для встроенных типов. Только для встроенных типов нужно будет заимплементить соответствующие функции в глобальном пространстве имен, причем так, чтоб их объявления (как минимум) были видны из нашей единственной специализации форматтера. Правильно я понял идею?
Один нюанс: необходимость определения функций в глобальном пространстве имен для встроенных типов будет диктовать выбор каких-то достаточно оригинальных имен для этих функций, чтобы исключить возможность нечаянных коллизий. Другой способ решения этой проблемы: добавление какого-нибудь фиктивного параметра, имеющего тип, определенный в пространстве имен my. При таком подходе имена для функций можно будет выбрать максимально простые и комфортные и перенести определения самих функций из глобального пространства имен в пространство имен my (это только для встроенных типов — для пользовательских типов функции можно будет спокойно определять в том же пространстве имен, что и сами типы).
V>Хорошо бы проверить будет ли такое работать с std::format C++20.
Здравствуйте, rg45, Вы писали:
R>То есть, мы враппим std:/fmt:: format и дальше пользуемся только своим my::format. А formatter специализируем только один раз и пишем его так, что он делегирует вызовы пользовательским функциям с использованием ADL. Пожалуй, это будет работать, причем, даже для встроенных типов. Только для встроенных типов нужно будет заимплементить соответствующие функции в глобальном пространстве имен, причем так, чтоб их объявления (как минимум) были видны из нашей единственной специализации форматтера. Правильно я понял идею?
Не совсем. Всё так, но не понятно зачем в глобальном пространстве, когда можно в локальном. Класс диспетчера-то наш и мы вольны вызывать что хотим.
R>Один нюанс: необходимость определения функций в глобальном пространстве имен для встроенных типов будет диктовать выбор каких-то достаточно оригинальных имен для этих функций, чтобы исключить возможность нечаянных коллизий. Другой способ решения этой проблемы: добавление какого-нибудь фиктивного параметра, имеющего тип, определенный в пространстве имен my. При таком подходе имена для функций можно будет выбрать максимально простые и комфортные и перенести определения самих функций из глобального пространства имен в пространство имен my (это только для встроенных типов — для пользовательских типов функции можно будет спокойно определять в том же пространстве имен, что и сами типы).
Опять не понимаю. Зачем это делать в глобальном пространстве, можно ведь:
и пустить свои иерархии объектов или функций в своём пространстве имен. Я не описываю делали сейчас. Просто указываю на точку кастомизации, из которой всё можно перенести в свой неймспейс и там уже делать кастомизацию или вызывать дефолтные реализации std, если нужно.
Здравствуйте, Videoman, Вы писали:
V>Не совсем. Всё так, но не понятно зачем в глобальном пространстве, когда можно в локальном. Класс диспетчера-то наш и мы вольны вызывать что хотим.
Вызывать что хотим, но по неквалифицированному имени — для того чтобы для UDT вызов выполнялся с использованием ADL. И эти же самые вызовы должны работать и для встроенных типов.
V>Опять не понимаю. Зачем это делать в глобальном пространстве, можно ведь:[ccode] V>
Ну и то, что мы получим, будет не многим лучше того, что мы имеем. ADL при таких вызвах не задествуется. И мы снова заставляем пользователя рвать свои пространства имен для того, чтобы определить функции parse и format в пространстве имен my. Ради этого овчинка выделки не стоит, имхо.
Здравствуйте, rg45, Вы писали:
R>Ну и то, что мы получим, будет не многим лучше того, что мы имеем — мы снова заставим пользователя рвать свои пространства имен для того, чтобы определить функции parse и format в пространстве имен my. Ради такого результата овчинка выделки не стоит, имхо.
Я описал просто точку кастомизации. Из нее можно подлезть и сделать как угодно. Я не повторял детали, которые мы до этого тут обсуждали. Можно же сделать и так:
parse(...., my::parse_as<type_t>());
format(...., my::format_as<type_t>());
Тогда ADL начнет работать по полной. Если дашь конкретику чего хочется получить и время, то я постараюсь показать полный код. Или я опять не понял вопроса?