Намедни вышла очередная версия замечательной библиотеки fmtlib. Одно из нововведений версии 9.0, а именно отказ от поддержки типов для которых определен оператор сдвига в std::ostream, сильно меня расстроило. И заставило потратить около четырех часов на выходных на то, чтобы адаптировать под fmtlib-9.0 наши OpenSource проекты arataga и RESTinio.
Тратить время на то, чтобы исправить сломанный другими людьми код мне не понравилось. Поэтому выскажу несколько своих незлых тихих слов.
Итак, что случилось в fmtlib-9.0.0?
В предыдущих версиях fmtlib можно было легко использовать типы, для которых определен оператор сдвига в std::ostream. Например:
... #include <fmt/ostream.h> namespace some::my::name::space { struct compound_key { int low_; int high_; }; std::ostream & operator<<(std::ostream & to, const compound_key & v) { return (to << '[' << v.low_ << ':' << v.high_ << ']') << std::endl; } ... void some_function() { ... compound_key key = ...; fmt::print(std::cout, "key is {}", key); } } /* some::my::name::space */ |
Начиная с версии 9.0.0 это уже не прокатывает. Теперь нужно либо специфицировать fmt::formatter для собственного типа, либо же использовать функцию fmt::streamed.
Т.е. либо мы поступаем вот так:
... #include <fmt/ostream.h> namespace some::my::name::space { struct compound_key { int low_; int high_; }; std::ostream & operator<<(std::ostream & to, const compound_key & v) { return (to << '[' << v.low_ << ':' << v.high_ << ']') << std::endl; } // Пространство имен временно закрываем. } /* some::my::name::space */ // Специализация fmt::formatter для своего типа. template<> struct fmt::formatter<some::my::name::space::compound_key> : fmt::ostream_formatter {}; // Опять открываем пространство имен. namespace some::my::name::space { ... void some_function() { ... compound_key key = ...; fmt::print(std::cout, "key is {}", key); } } /* some::my::name::space */ |
Либо же вот так:
... #include <fmt/ostream.h> namespace some::my::name::space { struct compound_key { int low_; int high_; }; std::ostream & operator<<(std::ostream & to, const compound_key & v) { return (to << '[' << v.low_ << ':' << v.high_ << ']') << std::endl; } ... void some_function() { ... compound_key key = ...; fmt::print(std::cout, "key is {}", fmt::streamed(key)); } } /* some::my::name::space */ |
В общем, и то, и другое выглядит как известная субстанция и, не удивлюсь, если этим и является.
Попробую пояснить почему все это плохо.
Во-первых, до fmtlib-9.0 не было проблем с печатью чужих типов, для которых был operator<< (вроде asio::ip::address). Теперь же мы не можем просто так напечатать чужие типы в своем коде посредством fmtlib.
Нам остается воспользоваться fmt::streamed.
Но что, если мы пишем шаблонную функцию, которая параметризуется неким типом T и мы не знаем, чей это будет тип: наш, для которого мы можем определить fmt::formatter, или же это чужой тип, для которого есть только operator<<?
А ничего хорошего. Пихай повсюду fmt::streamed. Сразу плюс +100500 к читабельности кода.
Идем далее.
Раньше для своего типа было достаточно определить только operator<< и этот тип можно было использовать хоть с ostream (в том числе и со специализированными реализациями), хоть с fmtlib.
Теперь же просто так свой тип с fmtlib использовать не получится. Либо засирай свой код fmt::streamed, либо делай специализацию fmt::formatter.
Особенно здорово это когда у тебя уже давным-давно куча типов с перегруженным operator<< и туча кода, в котором эти типы давным-давно используются с fmtlib. Придется приложить некоторые усилия, чтобы это все заработало с новым fmtlib. А то неправильно это как-то: работающий код, написанный хз сколько лет назад, и без рефакторинга. Непорядок. Но ничего, fmtlib-9.0 исправит это досадное недоразумение.
Что мне особенно нравится, так это удобство написания специализации fmt::formatter. На примере выше это уже можно увидеть: если мы определяем свой тип в каком-то своем пространстве имен и в этом же пространстве имен прямо рядышком с типом можем определить и operator<<, то вот с fmt::formatter так не получится. Если мы хотим определить специализацию fmt::formatter прямо рядом с типом, то придется свое пространство имен закрыть, а затем открыть заново. Ну или же делать специализации fmt::formatter где-то в отдельном месте.
Отдельные приключения получат мейнтейнеры библиотек (вроде нашей RESTinio), для которых fmtlib -- это зависимость. И версию этой зависимости могут определять пользователи. Ведь теперь нужно заботиться о том, чтобы библиотека компилировалась как с fmtlib-8, так и с fmtlib-9.
Как по мне, то так поднасрать пользователям fmtlib нужно было сильно постараться. И кто-то постарался.
Как включить в fmtlib функцию println в дополнение к print, так нет.
А вот как сломать совместимость в месте, где ломать ну вот вообще не следовало, так да.
Уверен, что найдется множество тех, кто считает, что все сделано в fmtlib-9.0 правильно. Уверен потому, что уже нашлось множество тех, кто считает, что в println нет необходимости. Ну что уж тут поделать, человеческая глупость не знает границ. Достаточно посмотреть на CMake и на количество тех, кто этим кусом известно чего пользуется и не жужжит.
Так и живем.
Чтобы добавить ярких красок, вот пример сообщения об ошибке при попытке перевести работающий код на fmtlib-9.0. Вот просто сразу же видно что к чему и где именно и что именно нужно поменять:
Compiling arataga/authentificator/a_authentificator.cpp ...
In file included from spdlog/include/spdlog/fmt/fmt.h:27,
from spdlog/include/spdlog/common.h:45,
from spdlog/include/spdlog/spdlog.h:12,
from ./arataga/config.hpp:12,
from ./arataga/user_list_processor/notifications.hpp:8,
from ./arataga/authentificator/a_authentificator.hpp:10,
from arataga/authentificator/a_authentificator.cpp:6:
./fmt/include/fmt/core.h: In instantiation of ‘constexpr fmt::v9::detail::value
./fmt/include/fmt/core.h:1753:29: required from ‘constexpr fmt::v9::detail::value
./fmt/include/fmt/core.h:1877:77: required from ‘constexpr fmt::v9::format_arg_store
spdlog/include/spdlog/logger.h:376:5: required from ‘void spdlog::logger::log_(spdlog::source_loc, spdlog::level::level_enum, spdlog::string_view_t, Args&& ...) [with Args = {const std::__cxx11::basic_string
spdlog/include/spdlog/logger.h:90:9: required from ‘void spdlog::logger::log(spdlog::source_loc, spdlog::level::level_enum, fmt::v9::format_string
spdlog/include/spdlog/logger.h:96:9: required from ‘void spdlog::logger::log(spdlog::level::level_enum, fmt::v9::format_string
arataga/authentificator/a_authentificator.cpp:104:5: required from ‘arataga::authentificator::a_authentificator_t::on_auth_request(so_5::agent_t::mhood_t
./arataga/logging/wrap_logging.hpp:173:9: required from ‘void arataga::logging::wrap_logging(arataga::logging::direct_logging_marker_t, spdlog::level::level_enum, Logging_Action&&) [with Logging_Action = arataga::authentificator::a_authentificator_t::on_auth_request(so_5::agent_t::mhood_t
arataga/authentificator/a_authentificator.cpp:119:6: required from here
spdlog/include/spdlog/logger.h:370:68: in ‘constexpr’ expansion of ‘fmt::v9::make_format_args<>(std::forward
./fmt/include/fmt/core.h:1733:7: error: static assertion failed: Cannot format an argument. To make type T formattable provide a formatter
1733 | formattable,
| ^~~~~~~~~~~
6 комментариев:
Может просто нужен fmt/ostream.h ?
https://fmt.dev/dev/api.html#ostream-api
В примерах же он фигурирует :)
У них документация не обновилась.
Ещё лучше :)
Кстати а нельзя ли сделать как-нибудь обобщённый адаптер чтобы не писать fmt::streamed?
> Кстати а нельзя ли сделать как-нибудь обобщённый адаптер чтобы не писать fmt::streamed?
Лучшее, что я смог придумать пока, это вот это :(
Старое поведение было отключено не просто так, а из-за того, что оно приводило к большим проблемам.
Подробнее в issue: https://github.com/fmtlib/fmt/issues/2357
Фактически, до версии fmt-9 безопасно использовать "fmt/ostream.h" можно было либо нигде, либо абсолютно везде (в том числе во всех статически линькуемых зависимостях).
Если в проекте в одном cpp подключался заголовок "fmt/format.h", а в другом - "fmt/ostream.h" и в проекте были типы с оператором вывода в поток, то это закладывало почву под возможный ODR violation, который другими путями к большому сожалению не устранялся.
@phprus
Я подозревал, что все это сделано не просто так. Спасибо за ссылку с объяснением.
Но даже знание причин не отменяет моего отношения к произошедшему. Ну и я бы, ради сохранения совместимости, был бы более склонен к тому, чтобы fmt/format.h сам бы подключал fmt/ostream.h. Поскольку a) для меня одной из основных фишек fmtlib была поддержка типов с operator<< без заморочек и b) уже написана туча кода, менять который -- это непроизводительные расходы (причем не только у меня, тут нужно умножать не количество пользователей fmtlib).
В общем, авторы библиотеки могли допустить просчет при проектировании библиотеки. Но если от текущего поведения зависит чужой код, то вот просто так взять и "все исправить" не самый дешевый вариант. Может быть дешевле (для пользователей) сохранять поведение настолько долго, насколько можно.
Отправить комментарий