Не секрет (для тех, кто меня читает), что мои вкусы в области языков программирования весьма странные как для энтузиаста, так и для профессионала. В нескольких моих постах, посвящённых Go, я пишу, что программировать на этом языке — всё равно, что завтракать овсянкой на воде.

Чтобы держать марку, расскажу и ещё об одном языке, программировать на котором — всё равно, что разбавлять утренний кофе самогоном. Естественно, речь пойдёт о C++.

Если вы из тех мучеников, которым довелось работать с C++ профессионально, то вы знаете, насколько в самом деле плоха стандартная библиотека C++. С чего бы начать?

Хотя бы с ассоциативных контейнеров — они ужасны. Поскольку решения об API принимали какие‑то дуболомы, std::unordered_map ОБЯЗАТЕЛЬНО должен иметь закрытую адресацию и строиться как массив связных списков, а не швейцарская таблица, несмотря на то, что технология закрытой адресации уже устарела. Структура данных std::map, которая нечасто нам требуется, обязательно реализуется как красно‑чёрное дерево. Она не может быть b‑деревом, хотя именно b‑дерево кладётся в основу упорядоченного контейнера в любом языке, написанном для людей.

std::optional в использовании — феерический геморрой, там куда ни ступи — выстрелишь себе в ногу. Чего стоит operator*. std::variant, кстати, тоже изрядно меня раздражающий. В std::filesystem повсюду острые углы. Кстати, а где API для обработки сигналов?

Все названия очень длинные. std::hardware_destructive_interference_size вполне мог бы именоваться std::cache_line. std::span::subspan мог бы использовать operator[]. Стандартные алгоритмы слишком многословные, так как в них приходится иметь дело с парами итераторов. Господи, парные итераторы! В язык добавили std::ranges, которые и рядом не стояли с Iterator из Rust!

Меня это всё откровенно бесит. Те, кто рулит современным C++, в открытую ненавидят свою аудиторию. Они хотят, чтобы с языком C++ было настолько сложно и неприятно работать, насколько это возможно. В моём кругу общения есть множество блестящих специалистов, которых я счастлив считать как друзьями, так и коллегами, кто пытался улучшить этот язык — в их числе Титус Уинтерс, Джинхейд Менейд, Мэтт Фаулз‑Кулукундис и Энди Соффер. У них в основном ничего не получилось.

Думаю, достаточно понятно объяснил, почему считаю, что в нынешней форме язык C++ неисправим. Но дело только в узколобости небольшой клики, живущей в окрестностях Редмонда. Что, если бы мы могли делать с языком что хотим? Например, в C++ заложен невероятный потенциал для самостоятельной сборки библиотек и, овладев им, можно было бы написать совершенно новый язык.

Вот уже более года я забавляюсь с безумной идеей: как выглядел бы язык C++, если бы мы переделали его с нуля? Возьмём за основу пустой файл C++20 и не будем обращаться к стандартной библиотеке. Что у нас могло бы получиться вместо привычных плюсов?

Начать всё сначала

Титус начал проект Abseil, ещё будучи сотрудником Google. Говорят, что соответствующее пространство имён называется absl, так как соответствует выражению «a better standard library» (улучшенная стандартная библиотека). На мой взгляд, Abseil важна сама по себе как попытка поработать с имеющейся стандартной библиотекой и улучшить её, в то же время сохраняя на высоком уровне качество реализации, каким не сможет похвастаться никакая кустарная вспомогательная библиотека C++. Кроме того, важно, какого единообразия достиг Титус в своём проекте — ведь по сравнению с ним библиотека Boost слишком рассредоточена и ничего подобного достичь не может.

Я хочу не сосуществовать со стандартной библиотекой, а хочу превзойти её. В качестве художественного перформанса хочу выяснить, как выглядела бы стандартная библиотека C++, если бы её проектировали хотя бы в 2025 году.

В таком смысле можно сказать, что я хочу не просто собрать библиотеку, которая будет лучше стандартной. Нет, это должна быть стандартная библиотека C++ в своей наилучшей ипостаси. Самая лучшая библиотека на C++. Вот почему я выбрал для неё пространство имён best.

В целом, я не собираюсь прямо копировать имеющиеся наработки ни из C++, ни из Abseil, ни из Rust, ни из Go. Но в каждом из этих сообществ есть по‑настоящему интересные идеи, и лучшая из библиотек, вероятно, располагается где‑то на пересечении между ними.

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

Закладываем основы

Избавимся от всего сразу, в том числе, от <type_traits>. Это заголовок, возраст которого угадывается уже по названию: шаблоны псевдонимов появились только в версии C++14, а шаблоны переменных — в C++17. Поэтому сложилось так, что многие вещи, которые на самом деле должны были бы быть концептуальными, на практике называются примерно как best::is_same_v. Для всех них теперь подобраны эквивалентные концепции, собранные во множестве <concepts>.

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

Например, под arrays.h содержатся все типажи массивов, например, best::is_array, best::un_array (чтобы избавиться от расширения массива) и best::as_array, применяющий расширение к типу T так, что best::as_array<T, 0> не является ошибкой.

В types.h содержатся очень низкоуровневые вспомогательные функции для метапрограммирования, например:

  • best::id и best::val, идентификаторы для типо‑ и значимо‑подобных типажей.

  • best::same<...>, возвращающий, равны ли все типы, входящие в целую группу (pack).

  • best::lie, наша версия std::declval.

  • best::select, наша версия std::conditional_t.

  • best::abridge, механизм «символьного сжатия», позволяющий сокращать названия символов, которые без этого механизма получаются огромными.

funcs.h предоставляет best::tame, снимающий квалификаторы с омерзительного типа функции. quals.h предоставляет best::qualifies_to — они необходимы, чтобы определять, является ли некий тип «более константным», чем другой. empty.h предоставляет стандартный пустой тип, аккуратно взаимодействующий с void.

Поверх типажей типов располагается библиотека для метапрограммирования //best/meta, содержащая в файле init.h обобщённые типажи конструируемости (например, позволяющие убедиться, что действительно можно инициализировать T& из T&&,). tlist.h предоставляет самую общую абстракцию неоднородного списка, действующую на уровне типов; группа параметров как тип.

Другим «базовым столпом» служит //best/base, в основном предоставляющий доступ к интринсикам, вспомогательным функциям, обеспечивающим портирование, макросам и «тег‑типам», таким, как наши версии std::in_place. Например, macro.h предоставляет BEST_STRINGIFY(), port.h предоставляет BEST_HAS_INCLUDE(), а hint.h предоставляет best::unreachable().

guard.h предоставляет нашу версию оператора ?, имеющегося в Rust и не являющегося выражением, так как Clang не умеет работать с выражениями‑инструкциями (statement expressions).

Наконец, внутри //best/container находим best::object, специальный тип, позволяющий превратить любой тип C++ в объект (т.e., в тип, на который можно сформировать ссылку). Это полезно в случаях, когда требуется в общем виде манипулировать любым типом, не спотыкаясь о семантику сквозного присваивания, характерную для ссылок. Например, best::object<T&> в сущности является указателем.

ADT‑контейнеры

Базируясь на всём описанном выше, построим базовые алгебраические типы best: best::row и best::choice, которые заменят std::tuple и std::variant.

best::row<A, B, C> — это неоднородная коллекция значений, хранимых в объектах best::object. Таким образом, best::row<int&> присуща семантика естественного повторного связывания, а не сквозного присваивания.

К элементам будем обращаться через at(): my_row.at<0>(), возвращая при этом ссылку на первый элемент. Получать первый элемент — операция настолько распространённая, что для этого также можно использовать my_row.first(). При использовании my_row.object<0>() вместо этого будет возвращена ссылка на best::object, поэтому такой вариант удобно использовать для повторного привязывания ссылок. Например:

int x = 0, y = 0;
best::row<int&> a{x};
a.at<0>() = 42;     // Записывает в x.
a.object<0>() = y;  // Повторно привязывает .0 к y.
a.at<0>() = 2*x;    // Записывает y.

Ещё есть методы second() и last(), для прямого обращения к ещё двум элементам, к которым обращаются чаще, чем к остальным.

best::row назван так с оглядкой на строки базы данных; он предоставляет много операций для сегментирования и сборки из сегментов — а std::tuple этого не делает.

Например, можно не только извлекать отдельные элементы, но и обращаться к непрерывным участкам последовательностей. Это делается при помощи best::bounds: a.at<best::bounds{.start = 1, .end = 10}>()! Также существуют всевозможные операции для внесения изменений (операций мутации):

  • a + b сцепляет кортежи, копируя или перемещая элементы — в зависимости от того, что требуется (так, a + BEST_MOVE(b) совершит переход с применением одного из элементов b).

  • a.push(x) возвращает копию a с прикреплённым к ней x, а a.insert<n>(x)делает то же самое по произвольному индексу.

  • a.update<n>(x)заменяет n‑й элемент x, который потенциально может относиться к другому типу, нежели x.

  • a.remove<n>() удаляет n‑й элемент, тогда как a.erase<...>() удаляет непрерывный диапазон.

  • a.splice<best::bounds{...}>(...) встраивает строку в другую строку, что превращается в универсальную операцию замены/удаления, в контексте которой реализованы все вышеприведённые операции.

  • gather() и scatter() — это ещё более общие методы, обеспечивающие несплошное индексирование.

В свою очередь, std::apply теперь превращается в метод: a.apply(f) вызывает f, принимая при этом элементы a как аргументы. a.each(f) устроен похоже, но, в свою очередь, расширяет унарные вызовы f до n, по одному для каждого элемента.

Причём, естественно, best::row поддерживает структурированные привязки.

В свою очередь, best::choice<A, B, C> содержит ровно по одному значению из разных типов. Существует базовый тип best::pun<A, B, C>, реализующий неразмеченное объединение с переменным количеством аргументов. Он позволяет обойти многие баги, возникающие в C++ при работе с объединениями, элементы в составе которых относятся к нетривиальным типам.

Самая простая операция с вариантом на выбор — применить к нему match:

best::choice<int, *int, void> z = 42;
int result = z.match(
  [](int x) { return x; },
  [](int* x) { return *x; },
  [] { return 0; }
);

Какой именно случай будет здесь вызван, зависит от разрешения перегрузок. Поэтому случай по умолчанию можем записать как [](auto&&) { ... }.

Чтобы проверить, какой вариант выбран в конкретном случае, вызывается z.which(), тогда как к конкретным вариантам можно обращаться через z.at(), точно как при работе с best::row, за тем исключением, что в данном случае возвращается best::option<T&>.

Именно на основе best::choice выстраиваются все остальные тип‑суммы, например, best::option и best::result. Сюда же относятся все умные оптимизации компоновки.

Что касается best::option<T>, это наш опциональный тип. По духу он близок типу Option<T> в Rust. В best есть обобщённый нишевой режим, в который могут переходить пользовательские типы. Благодаря этому, best::option<T&> может быть такого же размера, как указатель, причём для варианта best::none будет использоваться nullptr.

best::option предоставляет обычные операции преобразования: map, then, filter. Проверить, пуст ли элемент, можно при помощи is_empty() или has_value(). Можно даже передать предикат has_value(), чтобы проверить с его помощью значение, если оно есть: x.has_value([](auto& x) { return x == 42; }).

К этому значению можно обратиться при помощи operator* и operator->, как с применением std::optional;. Тем не менее, эта операция проверяется, а не приводит к выбросу неопределённого поведения, если окажется, что опция пуста. value_or() позволяет развернуть опцию, заменив её значением по умолчанию. У значения по умолчанию может быть сколько угодно аргументов, на основе которых и конструируется значение по умолчанию, или даже обратный вызов. Например:

best::option<Foo> x;

// Передать аргументы конструктору.
do_something(x.value_or(args, to, foo));

// Выполнить произвольную логику, если нужного значения нет.
do_something(x.value_or([] {
  return Foo(...);
}))

best::option<void> также берёт и работает (фактически, best::option<T> внутренне устроен как best::choice<void, T>), что обеспечивает по‑настоящему универсальный механизм работы с опциональными результатами.

Неудивительно, что best::result<T, E> аналогичен Result<T, E> из Rust. Поскольку best::choice внутри устроен как best::result<void, E>, он работает так, как вы и ожидаете, причём это обычное возвращаемое значение, часто применяемое в операциях ввода/вывода.

Он подобен best::option, в том числе, предоставляет operator-> для обращения к варианту «ok». Так обеспечиваются лаконичные идиомы:

if (auto r = fallible()) {
  r->do_something();
} else {
  best::println("{}", *r.err());
}

r.ok() и r.err() возвращают best::option, содержащие ссылки на варианты ok и error, в зависимости от того, какой из них реализуется. В свою очередь, best::option можно преобразовать в best::result при помощи ok_or() или err_or(), точно как в Rust.

Экземпляры best::result конструируются при помощи best::ok и best::err. Например:

best::result<Foo, Error> r = best::ok(args, to, foo);

Внутри они используют best::args, это обёртка над best::row, в которой представлена «отложенная инициализация», и эту операцию можно сохранить в значении. Такой код будет неявно преобразовываться в любой тип, который можно собрать из его элементов. Например:

Foo foo = best::args(args, to, foo);  // вызывает Foo::Foo(args, to, foo).

Кроме того, все до одного приведённые выше типы являются структурными, то есть могут использоваться при специализации шаблонов по нечисловому параметру!

Память и указатели

Разумеется, все эти ADT необходимо надстраивать поверх операций с указателями, и именно здесь нам пригодится //best/memory. best::ptr<T> — это обобщённый тип указателя, предоставляющий многие из именно тех операций, которые в Rust делаются при помощи сырых указателей — в том числе, смещение, копирование и индексирование. best::ptr<T>, как и в Rust, может быть жирным указателем, то есть поверх него могут быть записаны дополнительные метаданные. Например, best::ptr<int[]> запоминает размер массива.

Метаданные для best::ptr предоставляются через псевдоним экземпляра под названием BestPtrMetadata. Этот псевдоним должен быть приватным, а best получает доступ к нему, подружившись с best::access. Обычно нельзя напрямую конструировать типы с метаданными, предоставляемыми пользователем (так как они имеют переменный размер), и манипулировать ими необходимо исключительно посредством таких типов как best::ptr.

Указывая пользовательские метаданные, мы тем самым указываем, что разыменуется по указателю. Например, best::ptr<int[]> разыменуется в best::span<int>, то есть, все операции с диапазонами доступны через operator->: например, my_array_ptr->first().

Может показаться, что многое здесь переусложнено, поскольку на практике обычно вполне можно обойтись обычными сырыми указателями C++ и ссылками. Но best::ptr — та основа, на которой строится best::box<T>. best::box<T> заменяет std::unique_ptr<T>, исправляя известную проблему с const‑правильностью и добавляя Box‑подобные вспомогательные функции, такие как в Rust. best::box<T[]> тоже подойдёт, но, в отличие от std::unique_ptr<T[]>, он запоминает свой размер, точно как и best::ptr<T[]>.

best::box параметризуется его же аллокатором, который должен удовлетворять best::allocator — это гораздо более внятный API, чем тот, что предлагается std::allocator. best::malloc — это аллокатор‑синглтон, в таком виде у нас представлен системный аллокатор.

Ранее мы упоминали best::span<T> — это абстракция непрерывной области памяти, заменяющая std::span. best::span<T, n>, подобно std::span — это диапазон фиксированного размера, состоящий из n элементов. Но, в отличие от std::span, вторым параметром здесь идёт best::option<size_t>, а не size_t, использующий в качестве сторожевого значения -1.

best::span<T> пытается аппроксимировать имеющийся в Rust API срезов, обеспечивающий индексирование, сегментирование, вставку, сортировку, поиск и многое другое. Перебор, конечно, тоже, как из начала в конец, так и обратно. Как и в Rust, здесь предоставляются разделяемые итераторы (splitting iterator).

Сегментирование и индексирование всегда выполняется с проверкой границ. Для индексирования подойдут значения size_t, тогда как при сегментировании используется best::bounds:

best::span<int> xs = ...;
auto x = xs[5];
auto ys = xs[{.start = 1, .end = 6}];

best::bounds — это обобщённый механизм, позволяющий указывать границы сегментирования, он поход на применяемые в Rust типы диапазонов. Можно указывать начало и конец диапазона (включительно), как x..y в Rust. Также можно включать только конец, если написать inclusive_end = 5, что эквивалентно x..=y в Rust. Кроме того, можно указать счёт, так, как это предпочтительно делать с операторами сегментирования в C++: {.start = 1, .count = 5}. best::bounds сам по себе предоставляет все необходимые вспомогательные функции, используемые при проверке границ, а в случае отказа выдаёт информативное сообщение об ошибке. Как мы вскоре увидим, best::bounds также можно перебирать.

best::layout — это копия типа Layout из Rust, он предоставляет примерно такие же вспомогательные функции для вычисления размеров и адресов в C++‑специфичном стиле.

Итераторы

Пары итераторов в C++ никуда не годятся. Как и диапазоны. В best предлагается новая парадигма перебора, в сущности, здесь реализованы итераторы Rust, перекованные в форму C++. Эта библиотека находится в //best/iter.

Чтобы определить итератор, сначала определяем тип реализации итератора, который должен определить функцию‑член next(), а она, в свою очередь, возвращает best::option:

class my_iter_impl final {
  public:
    best::option<int> next();
};

Этот тип — деталь реализации; фактически действующий тип итератора — это best::iter<my_iter_impl>. best::iter предоставляет всевозможные вспомогательные функции, точно как Iterator. Это делается для адаптации к итератору или потребления поступающих от него элементов.

Итераторы могут переопределять поведение некоторых из этих адаптеров, повышая их эффективность. Например, count() можно сделать постоянным по времени, а не линейным. Также итераторы могут предоставлять дополнительные методы, если определяют псевдоним экземпляра BestIterArrow. Например, у best::span есть метод ->rest(), позволяющий возвращать такую часть сегмента, которую пока не выдал next().

Одна из самых важных точек расширения — это size_hint(), аналогичная Iterator::size_hint(), для подбора правильного размера тех контейнеров, в которые преобразуется итератор, например, best::vec.

Естественно, best::iter предоставляет begin/end, так, что он может использоваться в составе цикла range-for C++, точно как делается с диапазонами в C++20. best::int_range<I>, который, возможно, будет переименован в best::interval или даже best::range — посмотрим! — это также итератор, одним из воплощений которого является best::bounds. Он может применяться примерно так же, как могли бы использоваться диапазоны Rust:

for (auto i : best::int_range<int>{.start = 1, .count = 200}) {
  // ...
}

best::int_range аккуратно обработает все неудобные пограничные случаи, связанные с переполнением, например, best::int_range<uint8_t>{.end_inclusive = 255}.

Контейнеры кучи

Итераторы подводят нас к контейнерам самого сложного типа, актуального на данный момент: best::vec. Вы можете сами настраивать не только тип аллокатора для данного контейнера, но и тип оптимизации малых векторов.

В libc++ размер строк std::string составляет максимум 23 байта, и они содержатся во встроенном виде. Таким образом, строка держит данные в собственном хранилище, а не хранит в куче. Это обобщается при помощи best::vec, благодаря нему можно встроить любой тривиально копируемый тип. Следовательно, best::vec<int> может содержать во встроенном виде максимум пять int, если программа пишется для 64-разрядных платформ.

best::vec в основном копирует API из std::vector и Vec из Rust. Индексирование и сегментирование устроены так же, как при работе с best::span, и ко всем операциям с best::span можно обращаться через ->. Поэтому допускаются такие вещи как my_vec->sort(...).

У меня есть активный (неудачный) пул‑реквест, в рамках которого в библиотеку добавляется best::table<K, V>, общая реализация хеш‑таблицы. Она может использоваться и как словарь, и как множество. Внутри она опирается на реализацию в виде швейцарской таблицы. Здесь предлагается отличное знакомство с этой структурой данных. Её API не похож ни на std::unordered_map, ни на absl::flat_hash_map, ни даже на HashMap из Rust. Напротив, всё делается через общий входной API, похожий на реализованный в Rust, но оптимизированный для большей ясности и минимизации операций поиска хешей.

Кроме best::table планирую добавить ещё как минимум следующие контейнеры:

  • best::tree, B‑древесный словарь/множество с подобным API.

  • best::heap, простая реализация минимальной кучи.

  • best::lru, вариант best::table с пронизывающим её связным списком, обеспечивающим перебор по порядку и вытеснение самых старых экземпляров.

  • best::ring, кольцевой буфер наподобие VecDeque.

  • best::trie, портированный крейт twie.

Также рассматриваю на уровне идей: разреженный массив Расса, расширяющиеся деревья, нечто наподобие EnumMap из Java, типы битовых множеств и так далее

Обработка текста

Предполагается, что обработка строк в best должна максимально напоминать аналогичный механизм в Rust; он живёт внутри //best/text. best::rune и является скалярным типом Юникод, таким, что его значения всегда находятся в допустимых пределах какого‑то скаляра Юникод, но включают непарные суррогаты. В этом пакете предлагается ряд довольно простых операций над символами.

best::str призван заменить best::string_view и походить на str из Rust: это последовательность валидных байт UTF-8, обеспечивает всевозможные манипуляции со строками, в частности, поиск по глифам, разделение, индексирование, и так далее.

best::rune и best::str используют расширения компилятора, чтобы при сборке из литералов гарантировалось, что для этого всегда будут использоваться валидные литералы. Таким образом, следующий код не скомпилируется!

best::str invalid = "\xFF";

best::str под капотом — это best::span, обращаться к которому и манипулировать которым можно точно так же, как при превращении базового &[u8] в &str.

best::strbuf эквивалентен std::string. О нём особенно нечего рассказывать, поскольку он работает именно так, как вы ожидаете, и предоставляет String‑подобный API в стиле Rust.

Эта библиотека по‑настоящему хороша тем, что на уровне кодировок всё параметризовано. best::str — это, в сущности, best::text<best::utf8>;. Тогда best::str16 — это best::text<best::utf16>. Можно писать и собственные текстовые кодировки, поскольку они сравнительно кроткие, а вы сами можете предоставить для них механизм кодирования/декодирования глифов. Можно познакомиться с best::encoding как с примером концепции.

best::text всегда закодирован в допустимом виде, но иногда этого просто невозможно добиться. Поэтому предусмотрен ещё и best::pretext, при работе которого «предполагается, что всё закодировано правильно». Либо операции из этого контейнера отказывают, либо он сам производит «символы‑заменители» на случай, если в коде будут найдены недействительные единицы. Никакого best::pretextbuf нет; вместо этого, как правило, используется нечто наподобие best::vec<uint8_t>.

Обратите внимание: в отличие от C++ здесь best::textbuf под капотом является best::vec и входит в состав публичного интерфейса. Поэтому преобразования обходятся дёшево и, разумеется, оптимизацию малых векторов best::vec мы получаем вообще бесплатно.

best прямо из коробки предоставляет следующие кодировки: best::utf8, best::utf16, best::utf32, best::wtf8, best::ascii и best::latin1.

Форматирование

В //best/text:format предоставляется библиотека для форматирования текста, написанная в стиле format!() для Rust. Вот как просто она устроена:

auto str = best::format("my number: 0x{:08x}", n);

Благодаря мощи расширений компилятора constexpr удаётся проверять формат прямо во время компиляции!

Доступны все те же форматы, что и при работе с Rust, в том числе, различаются {}и {:?}. Но на практике эта библиотека ещё более гибкая. Можно использовать любые буквы ASCII, а на уровне типов можно (при помощи букв) предоставлять разнообразные пользовательские схемы форматирования. Действует соглашение, по которому x, X, b и o означают основания систем счисления. q заключает в кавычки строки, глифы и другие текстовые объекты; p выводит адреса указателей.

Специальный формат {:!} «переадресует сверху»; когда он используется при реализации форматирования, в составе формата присутствует особый спецификатор, указывающий, кто выступает вызывающей стороной. Это полезно для обеспечения «сквозной проницаемости» форматов, например, при выводе списков или best::option.

Любой тип можно сделать форматируемым, предоставив для этого дружественное шаблонное расширение ADL (FTADLE) под названием BestFmt. Функционально получится то же самое, как например, при реализации типажаfmt::Debug в Rust. Правда, все операции форматирования используют одну и ту же функцию. Поэтому механизм похож на fmt.Formatter в Go.

Тип best::formatter, передаваемый в BestFmt, похож на Formatter из Rust. Он не просто служит стоком, но и предоставляет через спецификатор (через current_spec()) информацию об операции форматирования, а также вспомогательные функции, чтобы выводить на экран списки с отступами и блоки текста.

BestFmtQuery — похожий FTADLE, вызывается, чтобы определить, какие валидные спецификаторы формата существуют для данного типа. Благодаря этому валидатор может отвергать форматы, не поддерживаемые данным типом. Например, {:x} не подходит для форматирования best::str.

best::format возвращает (или прикрепляется к) best::strbuf; best::println и best::eprintln могут применяться для записи в stdout и stderr.

Рефлексия

В рамках библиотеки для метапрограммирования //best/meta:reflect предлагает базовый вариант рефлексии. Это не такая рефлексия как в C++26, поскольку применять последнюю — как палить из пушки по воробьям. Нет, здесь просто предоставляется метод для интроспекции членов структур и перечислений.

Допустим, мы хотим предусмотреть способ для форматирования произвольных сборных структур по умолчанию. Код для этого будет дьявольски прост:

void BestFmt(auto& fmt, const best::is_reflected_struct auto& value) {
  // Отразить тип структуры
  auto refl = best::reflect<decltype(value)>;
  // Приступить к форматированию "записи" (пар ключ-значение).
  auto rec = fmt.record(refl.name());

  // Для каждого поля в структуре...
  refl.each([&](auto field) {
    // Добавить поле в запись форматирования...
    rec.field(
      field.name(),   // ...чьё имя содержит field...
      value->*field,  // ...и с соответствующим значением.
    );
  });
}

best::reflect предоставляет доступ к полям (или вариантам из перечисления) определённого пользователем типа, который обозначает себя пригодным для того, предоставляя FTADLE BestReflect. Тем самым фреймворку рефлексии сообщается, какие здесь поля. Простейшая версия этого FTADLE будет выглядеть так:

friend constexpr auto BestReflect(auto& mirror, MyStruct*) {
  return mirror.infer();
}

best::mirror служит «сборщиком отражений» и позволяет тонко контролировать, какие элементы структуры мы увидим при отражении. Этот механизм позволяет скрывать поля, либо прикреплять метки к конкретным полям. После этого такие поля становятся доступны для интроспекции с использованием best::reflected_field::tags().

Функции best::reflected_type обеспечивают перебор и поиск конкретных полей (или вариантов из перечисления); эти поля best::reflected_field предоставляют метаданные о поле (например, его имя) и позволяют обращаться к нему. При этом применяется тот же синтаксис, что и при обращении к члену по указателю: value->*field.

Чтобы во всей полноте объяснить best::reflect (и фокусы его реализации), потребовалось бы написать целый пост, так что здесь я этого делать не буду.

Модульные тесты и приложения

best предоставляет фреймворк для модульного тестирования, расположенный под //best/test — как и следует любой хорошей стандартной библиотеке. Чтобы определить тест, определяем своеобразную глобальную переменную:

best::test MyTest = [](best::test& t) {
  // Код теста
};

Очень напоминает модульный тест на Go, где мы определяем функцию, начинающуюся с Test и принимающую в качестве аргумента *testing.T. В значении best::test& можно выразить тестовые утверждения и условия, при которых тест не пройден. Поскольку многое можно узнать, заглянув в информацию об отладке (debuginfo), извлечём из двоичного файла имя MyTest и будем напрямую пользоваться им как именем теста.

Вы не ошиблись, это фреймворк для тестирования на C++, в котором вообще нет макросов!

Тем временем, под //best/cli находим устойчивую библиотеку для разбора CLI, написанную в духе #[derive(clap::Parser)] и других подобных библиотек Rust. Вот как она работает: сначала позволяет определить поддающуюся рефлексии структуру, поля которой соответствуют флагам CLI. Простейший пример такого рода находится в test.h, поскольку в двоичных файлах тестов определяются собственные флаги:

struct test::flags final {
  best::vec<best::strbuf> skip;
  best::vec<best::strbuf> filters;

  constexpr friend auto BestReflect(auto& m, flags*) {
    return m.infer()
      .with(best::cli::app{.about = "a best unit test binary"})
      .with(&flags::skip,
            best::cli::flag{
              .arg = "FILTER",
              .help = "Skip tests whose names contain FILTER",
            })
      .with(&flags::filters,
            best::cli::positional{
              .name = "FILTERS",
              .help = "Include only tests whose names contain FILTER",
            });
  }
};

При помощи best::mirror::with можем применять на уровне отдельных полей теги, описывающие, как их следует разбирать и отображать в виде флагов CLI. Более сложный полнофункциональный пример находится в toy_flags.h, здесь показано в действии большинство возможностей парсера CLI.

best::parse_flags<MyFlags>(...) может использоваться для синтаксического разбора структур с конкретными флагами, полученных на вход, независимо от действующих argv программы. В best::cli содержатся действующие метаданные парсера, но обычно они недоступны пользователю, а конструируются автоматически при помощи рефлексии.

Можно оптимизировать выполнение приложения при помощи best::app, полностью заменяющего функцию main(). Определить приложение можно примерно так же, как определяется тест:

best::app MyApp = [](MyFlags& flags) {
  // Сейчас будет круто, смотрите!
};

Так мы автоматически запишем, что поступает на вход в программу, запускаем парсер флагов для MyFlags, а затем вызываем тело лямбда‑выражения.

Лямбда может вернуть либо void, либо int (в качестве кода выхода) или даже best::result, как в Rust. Именно в best::app можно запрашивать argv из других частей программы.

Что дальше

Конечно, в стандартную библиотеку best ещё многое хотелось бы добавить. Здесь нет никаких примитивов синхронизации, ни атомиков, ни блокировок, ни каналов. Пока нет ввода/вывода. Хотелось бы написать собственную математическую библиотеку best::rc (с подсчётом ссылок) и портируемые ОКМД. Кроме того, хотелось бы собрать API для взаимодействия с другими операционными системами, например, сигналы и подпроцессы. Нужно будет добавить надёжный генератор псевдослучайных чисел, API времени, сетевые возможности и символизацию стека.

Написать наилучшую библиотеку для C++ — это большая работа, не в последнюю очередь потому, что в языке C++ множество непростых деталей, и его сложно покрыть исчерпывающими тестами. Но такая работа вновь пробудила во мне интерес к C++!

Не рассчитываю на то, что кто‑нибудь станет пользоваться такой библиотекой, но думаю, что описанный здесь опыт доказывает, что язык C++ может быть гораздо лучше!

Комментарии (0)