суббота, 29 октября 2022 г.

[prog.c++] Дикая идея о том, как можно было бы еще улучшить fmtlib и std::format

Навеяно срачем на RSDN вот в этой теме.

По сути, там были указаны реальные проблемы, которые есть при использовании fmtlib и, соответственно, std::format.

Одна из этих проблем в том, что для описания форматера для моего типа T мне нужно делать специализацию для fmt::formatter, а это нельзя сделать в моем пространстве имен. Поэтому, если я в своем пространстве имен только что определил тип T, то мне нельзя здесь же определить и фоматтер для него, нужно сперва закрыть свое пространство имен, а потом открыть его вновь.

Еще одна проблема показана вот в этом комментарии. Позволю себе процитировать:

Сейчас также все не сахар. Форматер в виде специализации класса не вызывается и всё, пока ты правильно не подберёшь синтаксис перегрузки. С шаблонными классами это бывает не очень просто. Вот пример у меня в коде:

namespace htlib::v2
{
template<typename type_t>
struct is_point {

    static std::false_type test(...);
    template<htlib::v2::uint_t dimensions, typename other_t>
    static std::true_type test(const htlib::v2::pointxd_t<dimensions, other_t>&);

    static constexpr bool value = decltype(test(std::declval<type_t>()))::value;
};
template<typename type_t>
inline constexpr bool is_point_v = is_point<type_t>::value;

// namespace htlib::v2

template<typename char_t, typename point_t>
class std::formatter<point_t, char_t, std::enable_if_t<htlib::v2::is_point_v<point_t>>>
{
//...
}

Вот такие мета-функции приходится писать что бы написать форматер для всех наследников базового шаблонного класса.

Хотя в случае с функциями была простая перегрузка, типа:

template<htlib::v2::uint_t dimensions, typename other_t>
void formatter(const htlib::v2::pointxd_t<dimensions, other_t>&);

В обсуждении прозвучала идея о том, что лучше бы было, если бы за парсинг и форматирование значений отвечали бы не классы, а свободные функции. Свободные функции могли бы размещаться в пространстве имен пользователя, соответственно, они бы подхватывались посредством ADL.

Очень хорошо выгоды от свободных функций сформулированы здесь:

если бы std::format для кастомизации использовал бы не шаблон класса со специализациями, а неквалифицированный вызов функции с каким-то предопределенным именем, это могло бы дать ряд преимуществ:

  • Пользователь мог бы определять кастомные форматтеры в том же пространстве имен, что и пользовательские типы. Это удобнее писать и удобнее читать, потому что не нужно рвать пространства имен или выносить определения в какие-то отдельные места;
  • При определении кастомных форматтеров пользователь мог бы использовать дополнительные параметры по умолчанию — как в списке шаблонных параметров, так и в списке формальных параметров функции, что дает возможность использования SFINAE и вообще дает большую гибкость в тех случаях, когда кастомизацию нужно сделать не для одного конкретного типа, а для какого-нибудь сеймейства типов, объединенных каким-то признаком;
  • Как частный случай — форматтер, определенный для базового класса автоматом будет работать и для всех производных, для которых не предоставлена своя собственая версия форматтера;
  • Используя квалифицированные вызовы, пользователь мог бы внутри кастомго форматтера повторно использовать форматтеры из других пространств имен, что дает возможность декорирования форматтеров.

Хотя лично мне перспектива писать свободные функции parse и format для своих типов не очень нравится, имхо, класс formatter с методами parse и format для таких целей удобнее, имхо. Да и совместимость с уже написанным кодом терять не хочется, так что подход со свободными функциями должен как-то сочетаться с уже написанными formatter-ами.

Посему возникла дурацкая идея о том, как можно подружить оба эти подхода.

Суть в том, чтобы оставить класс formatter как он есть. Но не создавать его напрямую, а получать посредством свободной функции-фабрики. Как раз эту самую функцию-фабрику можно будет определить в собственном пространстве имен рядом со своими типами.

Под катом слепленный на коленке за 10 минут пример того, как это может быть.

Насколько это все реально внедрить в fmtlib не знаю. Заглянул в исходники, но там не то, чтобы все сложно. Но это таки большой проект, в котором я ни бум-бум. Сходу не разобраться.

А копать глубоко нет возможности. Я тут, к сожалению, слегка зашился и чувствую, что едва хватает сил на выполнение обязательств по текущему проекту. Даже за PR для RESTinio не могу взяться :(

Поэтому зафиксирую идею здесь, в блоге. Если у кого-то из читателей будет желание довести это все до мейнтейнеров fmtlib, то хорошо. Если нет, значит кто-то другой придумает что-то получше и сможет довести до внедрения и реализации.

Напоследок скажу, что на RSDN-е были таки обозначены актуальные проблемы, с которыми люди сталкиваются в fmtlib и std::format. И было бы хорошо тем или иным способом решить эти проблемы. Наверняка способы найдутся.

#include <algorithm>
#include <iostream>
#include <string>
#include <string_view>
#include <type_traits>
#include <sstream>

namespace fmtlib
{

template<typename T, typename CharT = char>
struct formatter;

template<typename CharT, typename T>
[[nodiscard]]
formatter<T, CharT>
make_formatter(const T &);

template<typename CharT, typename T>
CharT *
format(CharT * dest, T && value)
   {
      return make_formatter<CharT>(value).format(dest, value);
   }

template<>
struct formatter<std::string_view, char>
   {
      char *
      format(char * dest, const std::string_view & v)
         {
            *(dest++) = '"';
            std::copy(v.begin(), v.end(), dest);
            dest += v.size();
            *(dest++) = '"';
            return dest;
         }
   };

template<>
struct formatter<std::string, char>
   :  public formatter<std::string_view, char>
   {
      char *
      format(char * dest, const std::string & v)
         {
            return formatter<std::string_view, char>::format(
                  dest, std::string_view{v});
         }
   };

template<>
struct formatter<charchar>
   {
      char *
      format(char * dest, const char v)
         {
            *(dest++) = v;
            return dest;
         }
   };

template<typename T>
struct formatter<T, std::enable_if_t<std::is_arithmetic_v<T>, char>>
   {
      char *
      format(char * dest, const T & v)
         {
            std::ostringstream ss;
            ss << v;
            const auto sv = ss.str();
            std::copy(sv.begin(), sv.end(), dest);
            dest += sv.size();
            return dest;
         }
   };

template<typename CharT, typename T>
[[nodiscard]]
formatter<T, CharT>
make_formatter(const T &)
   {
      return {};
   }

/* namespace fmtlib */

namespace one
{

template<typename T>
struct point
   {
      T x;
      T y;
   };

/* namespace one */

template<typename T>
struct fmtlib::formatter<one::point<T>, char>
   {
      char *
      format(char * dest, const one::point<T> & v)
      {
         dest = fmtlib::format(dest, '{');
         dest = fmtlib::format(dest, v.x);
         dest = fmtlib::format(dest, ',');
         dest = fmtlib::format(dest, v.y);
         dest = fmtlib::format(dest, '}');
         return dest;
      }
   };

namespace two
{

template<typename T, typename Tag>
struct value_holder
   {
      T v;
   };

template<typename T, typename Tag, typename CharT>
struct value_holder_formatter
   {
      CharT *
      format(CharT * dest, const value_holder<T, Tag> & v)
      {
         return fmtlib::formatter<T, CharT>{}.format(dest, v.v);
      }
   };

template<typename CharT, typename T, typename Tag>
value_holder_formatter<T, Tag, CharT>
make_formatter(const value_holder<T, Tag> &)
   {
      return {};
   }

/* namespace two */

int
main()
   {
      using namespace std::string_literals;
      using namespace std::string_view_literals;

      char buf[1024];

      struct tag_1 {};
      struct tag_2 {};

      char * p = fmtlib::format(buf, 42);
      p = fmtlib::format(p, ' ');
      p = fmtlib::format(p, "is the answer"s);
      p = fmtlib::format(p, ';' );
      p = fmtlib::format(p, one::point<int>{3,4});
      p = fmtlib::format(p, '-' );
      p = fmtlib::format(p, one::point<float>{0.1,-1.2});
      p = fmtlib::format(p, ';' );
      p = fmtlib::format(p, two::value_holder<short, tag_1>{333});
      p = fmtlib::format(p, '-' );
      p = fmtlib::format(p, two::value_holder<unsigned long long, tag_2>{555});
      *p = 0;

      std::cout << "Result: " << buf << std::endl;
   }

Пример проверялся посредством g++-11 и C++17.

2 комментария:

phprus комментирует...

Класс formatter может хранить состояние. Метод parse его заполняет при разборе строки формата (эта часть может быть во время компиляции), а метод format используя это состояние уже осуществляет форматирование значения.

Так как состояние зависит от типа форматируемого объекта, возникает вопрос, каким образом его хранить, если один класс заменить на две свободные функции. Я сходу не вижу красивого решения для этого.

eao197 комментирует...

@phprus

Я тоже не вижу. И писал об этом в обсуждении на RSDN в разговоре с rg45. Правда, rg45 утверждает, что это не проблема, но я лично не понимаю почему это не проблема.

Собственно, поэтому мои мысли и крутились вокруг сохранения класса formatter, но чтобы можно было подсунуть собственный formatter, а не обязательно специализацию fmt/std::formatter. Как раз функция-фабрика make_formatter такую возможность дает.

Мне еще нравится идея сохранить класс formatter потому, что от formatter можно наследоваться и переиспользовать тем самым то, что уже реализовано в каком-то formatter-е. Со свободными функциями такое делать сложнее, имхо.