Как стать автором
Поиск
Написать публикацию
Обновить

simstr — ещё одна строковая библиотека

Уровень сложностиСредний
Время на прочтение17 мин
Количество просмотров2.8K

В ретроспективе 1991 года по истории C++ его создатель Бьярне Страуструп назвал отсутствие стандартного строкового типа (и некоторых других стандартных типов) в C++ 1.0 худшей ошибкой, которую он допустил при его разработке:

“the absence of those led to everybody re‑inventing the wheel and to an unnecessary diversity in the most fundamental classes”

(«Их отсутствие привело к тому, что все заново изобретали велосипед, и к ненужному разнообразию в самых фундаментальных классах»).

С тех пор минуло уж много лет, и казалось бы, всё устаканилось, но до сих пор использование строк в С++ вызывает боль. По крайней мере у тех жадных программистов, которые как и я, очень не любят платить за то, чем не пользуются (а я ещё и много работаю со скриптовыми языками, где со строками всё гораздо удобнее, хотя и за свою цену, конечно). Так о чём это я? По канону настало время вступления, что ж, давайте же в него вступим.

Для тех, кому некогда много читать

Стандартные строки в C++ неплохи, но можно сделать лучше!
Дарю сообществу свою строковую библиотеку simstr.

  • Удобно используется, работать со строками становится удовольствием

  • Быстра и оптимальна при выполнении

  • Строки UTF-8, UTF-16, UTF-32 с авто-конвертацией (через simdutf)

  • С тестами

  • С впечатляющими бенчмарками

  • Реальный пример использования в утилите обработки бенчмарков

  • Есть doxygen дока

  • Работает и в WASM через Emscripten

  • Нужен C++20 как минимум

В комментах приветствуются идеи по новым бенчмаркам: вы накидываете код с std::string, который доставляет вам боль, я пытаюсь его вылечить и сравнить :)

Строки в C++

(что с вами не так?)

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

Собственно, изначально как такового стандартного типа для строк в С++ не было. Для работы со строками использовался подход из C – строка есть указатель на массив байтов, оканчивающихся нулём. Недостатки таких строк — невозможно в строке использовать байт 0, т. е. не подходит для бинарных данных, непонятна стратегия управления ресурсами, ну и основной недостаток — длину строки приходится вычислять каждый раз, перебирая все её символы.
Откуда ноги растут у такого решения вполне понятно — со времён динозавров:

картинка
Маленькая память, короткие строки лапки
Маленькая память, короткие строки лапки

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

Первые попытки стандартизировать строки как класс начались только в С++98 - std::string появился, как часть STL, и как многое из STL, крайне неоднозначно воспринимался программистами.

Первое, что приходит в голову при улучшении C-строк — надо хранить длину строки:

struct SimpleString {
    const char* data;
    size_t length;
};

При наличии такой строки, уже множество алгоритмов значительно оптимизируются. Например, при сравнении двух строк на равенство мы можем даже не начинать сравнивать их символы, если длины строк не равны. Более того, этих данных абсолютно достаточно для всех методов, которые не модифицируют строку. Также заметим, что такой объект на современных 64-битных архитектурах прекрасно передается в функции по значению — оба его поля укладываются в регистры, что облегчает работу оптимизатору компилятора.

Между тем, такое решение попало в стандарт только аж в С++17, в виде std::string_view. Видимо, только тогда до комитета смогли донести мысль, что строки строкам рознь, и использовать только один универсальный объект для строк — по меньшей мере может приводить к уменьшению производительности, а также нарушает принцип «не плати за то, чем не пользуешься». Почему же «строки строкам рознь» и почему нам мало одного типа для строки, рассмотрим чуть позже.

Ресурсы

Следующий вопрос, возникающий со строками — это владение ресурсами.

Картинка

Практически каждый крупный фреймворк решал эту задачу самостоятельно, изобретая свои велосипеды:
У нас есть std::string, в QT у нас QString, в MFC - CString, в ATL - CAtlString, свои строки есть в Folly, в общем, “тысячи их”, любой игровой движок начинают с того, чтобы написать свои строки.

Картинка
Велосипеды - они повсюду
Велосипеды - они повсюду

Многие из этих реализаций в аспекте управления ресурсами для улучшения производительности использовали подход COW – “Copy On Write”. При этом объект строки ссылался на некий разделяемый между несколькими объектами буфер с символами строки и счётчиком ссылок на этот буфер, что позволяло быстро создавать копию строки, а реально копировать символы только при её модификации.

Мутабельность /иммутабельность

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

Из-за этого подход COW умер к С++11: при каждой операции, могущей модифицировать символы строки приходилось проверять, не ссылаемся ли мы на разделяемый буфер и если да, то копировать символы в другой буфер. В многопоточной же среде потом ещё и проверять, а не надо ли теперь ещё и освободить старый буфер, и естественно — всё это обмазавшись локами или атомиками, что тоже не бесплатно. Поэтому, начиная с С++11 std::string не использует COW, и каждое копирование объекта строки приводит также и к копированию всех символов строки в другой буфер.

Естественно, что каждый новый буфер требует аллокации памяти, что пытаются немного оптимизировать за счёт SSO – “Small String Optimization”, когда объект строки содержит внутри себя небольшой буфер и символы коротких строк располагаются прямо в нём. Но это уже зависит от реализации: в одних библиотеках помещают в объект строки до 15 байт, в некоторых до 22. Однако эта оптимизация тоже палка о двух концах, и может в различных реализациях усложнить перемещение строки - если она хранит указатель на свой внутренний буфер, его придётся корректировать.

А без COW мутабельность строк приводит к тому, что любая инициализация объекта строки приводит к копированию байтов. Посмотрим такой код:

const char* text1 = "Hello, World";     // ничего не стоит
std::string_view text2 = "Hello, World";// Ничего не стоит, вычисляет длину строки при компиляции
std::string text3 = "Hello, World";     // В рантайме каждый раз копирует символы строки

(Удостоверится в правдивости комментариев можно на godbolt)

Но если нам дальше по коду не нужно никак модифицировать строку (допустим, нам надо просто вызвать функцию с параметром const std::string&), то мы зря платим за аллокацию, копирование символов, а также за деструктор строки. То есть хотелось бы иметь как минимум два варианта строк — мутабельные и иммутабельные, чтобы явно дать понять, что мы не собираемся модифицировать строку.

Или банальный пример — мы парсим какой-то входящий буфер данных, нам нужно проверить, равен ли некий кусок буфера строке ”hello” на «чистом С++», т. е. без всяких memcmp и strcmp. До появления string_view приходилось делать примерно так:

bool is_part_buffer_equal_hello(const char* data, int start, int end) {
    return std::string(data + start, end - start) == "hello";
}

Тут получается, сначала копируются символы из буфера data в буфер временной строки, возможно с аллокацией памяти, и лишь потом временная строка сравнивается с ”hello”, а потом ещё и деструктор и раскрутка стека на случай исключения.

Для таких случаев в C++17 придумали std::string_view. При его использовании вместо std::string – код на C++ почти не меняется:

bool is_part_buffer_equal_hello_view(const char* data, int start, int end) {
    return std::string_view(data + start, end - start) == "hello";
}

Однако генерируемый машинный код значительно преобразуется, достигая уровня ручного С-кода — там просто сравнивается, что end – start == 5 и дальше кусок начального буфера сравнивается через memcmp со строкой ”hello” (при -O2 c константами 1819043176 (’hell’) и 111 (’o’)). Ни создания временного объекта, ни копирования байтов, ни деструктора, ни раскрутки стека для исключений. Убедится можно на godbolt.

Казалось бы, ну вот же в С++17 появился string_view, пожалуйста, используй его в параметрах своих функций вместо const std::string&, и будет счастье. Но тут тоже есть нюанс — всё отлично работает, пока нам не нужно передать строку в стороннее C-API: string_view не даёт гарантий нуль-терминированности строки, поэтому его data() нельзя передать в стороннее C-API, и потому всё-равно придётся сначала скопировать его в std::string. А раз нужен std::string, то и параметром функции оптимальнее cделать const std::string& и далее по цепочке, все параметры вновь станут const std::string&. Кроме того, std::string_view никак не управляет ресурсами и не может быть владельцем строки, то есть хранить символы.

Конкатенация строк

После инициализации строки, самая частая мутабельная операция с ними, скорее всего конкатенация строк, либо в виде просто сложения строк, либо последовательного добавления строки к строке. И именно она легко может вызывать как неоптимальную производительность при неграмотном использовании, так и оверхед по памяти, даже при грамотном использовании. Рассмотрим простой код ( godbolt )

#include <string>
void not_remove_var(const std::string&);

void func(const std::string& s1, const std::string& s2) {
    std::string concat = s1 + s2 + "hello";
    not_remove_var(concat);
}

Как видим, и в clang, и в GCC создается несколько временных объектов, в которые последовательно перекладываются символы строк, и как результат — мы получаем несколько лишних аллокаций для промежуточных буферов, символы из строк копируются несколько лишних раз из промежуточных буферов. В идеале для лучшей производительности такой код нужно переписать так:

#include <string>
void not_remove_var(const std::string&);
void func(const std::string& s1, const std::string& s2) {
    const std::string_view hello = "hello";
    std::string concat;
    concat.reserve(s1.size() + s2.size() + hello.size());
    concat += s1;
    concat += s2;
    concat += hello;
    not_remove_var(concat);
}

К сожалению, пока ни один компилятор не оптимизирует первый простой код до уровня второго более оптимального кода, а писать такой код каждый раз руками довольно неудобно. То есть опять приходится платить за то, чем не пользуешься. Да и в этом случае вполне может возникнуть оверхед по памяти — операции добавления строки обычно во всех реализациях увеличивают размер буфера строки не меньше, чем в два раза, считая, что скоро к строке могут снова что-нибудь добавить. Поэтому если строку не планируется более модифицировать, но время её жизни ещё не подошло к концу (например, это поле какого-либо класса), нужно ещё не забыть сделать на ней shrink_to_fit.

Между тем, часто основной сценарий использования строк — это как раз некая подготовка строки путём нескольких модификаций и конкатенаций, а затем она где-то хранится, более не меняясь. При этом программист обычно знает, примерно какой размер строк ожидается в этом месте, и мог бы выделить буфер для этих промежуточных модификаций прямо на стеке, прибегая к динамической аллокации только при превышении размера этого буфера. Однако с текущей реализацией строк это сделать либо довольно проблематично, либо неудобно.

Подытожим, что имеем на данный момент:

  • «Из коробки» в С++ для работы со строками сейчас имеется std::string.

  • Строки подразумеваются мутабельными, что приводит к обязательному копированию всех символов строки при инициализации и копировании объектов строк.

  • Соответственно, не имеем возможности быстрого копирования строк, даже если не планируем потом менять копию.

  • Конкатенация нескольких строк — задача, которая может выполнятся не оптимально, приводить к оверхеду по памяти, написать оптимальный код сложно.

  • Есть костыль для иммутабельных строк в виде std::string_view, однако он не решает вопросы владения строкой, поэтому по сути годится только как тип для передачи параметров в функции, не меняющие строки, с оговоркой, что не может использоваться в функциях, вызывающих C-API, так как не даёт гарантий нуль-терминированности.

  • Ну и к std::string есть вопросы — несмотря на то, что это класс для строк, собственно для работы со строками в нём крайне куцый функционал по сравнению с тем, к чему привыкли в других языках — к примеру нет замены подстрок по шаблону (в других языках это обычно replace, но в С++ эта функция делает совершенно другое), trim, split, join, upper, lower и т. п. Эти функции приходится каждый раз писать самому, и не факт, что у всех это получится оптимально.

Библиотека simstr

Располагается на github.

Собственно, нельзя сказать, что «я свелосипедил свою реализацию класса для строк». Как я показал, сложно, а то и даже невозможно написать один единый строковый класс, хорошо подходящий для всех сценариев использования. Именно поэтому у меня строковая библиотека, которая содержит несколько разных строковых типов, от более простых к более сложным, каждый из которых имеет свои сильные и слабые стороны, и пользователю нужно грамотно подходить к вопросу, какой из этих классов в каком случае стоит использовать.

Саму библиотеку я начал потихоньку разрабатывать ещё в 2011-2012 годах, когда у нас уже появилась семантика перемещения, но ещё не было std::string_view. Однако сейчас минимальная версия стандарта для работы библиотеки: C++20 – используются концепты и <format>.

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

Несколько общих моментов:

  • Все классы для работы со строками шаблонизированы типом символов, но подразумевается, что символы могут быть char, char16_t, char32_t, wchar_t.

  • Все строки имеют явную длину.

  • Классы владельцы строк хранят их с завершающим нулем в конце, который не входит в длину строки.

  • В самой строке могут содержаться нулевые символы, все алгоритмы работают только через длину строки, не обращая на них внимания.

  • Классы владельцы строк могут инициализироваться строками другого типа символов, выполняя конвертацию между UTF-8, UTF-16, UTF-32.

  • Для смены регистра символов и сравнения строк без учёта регистра используются встроенные таблицы для первой плоскости юникода (до 0xFFFF). Строки считаются представленными в кодировке UTF-8, UTF-16, UTF-32 соответственно. Однако не делается нормализация строк и не обрабатываются ситуации, когда смена регистра символа приводит к изменению их количества. Если вам нужна строгая работа с юникодом, используйте другие средства, например ICU.

Классы строк

Первый самый простой класс строки называется, естественно, simple_str :-)

Класс просто представляет собой указатель на начало константной строки и её длину, по сути то же самое, что std::string_view. Предназначен для работы с иммутабельными строками, не владеющий ими, то есть вы должны сами озаботиться тем, что реальная строка, представленная через simple_str – жива во время его использования. Реализует все методы, не модифицирующие строку.

Алиасы:
- ssa для simple_str<char>
- ssu для simple_str<char16_t>
- ssuu для simple_str<char32_t>
- ssw для simple_str<wchar_t>

Применяется в основном для передачи строк как параметр функций, не модифицирующих переданную строку, вместо const std::string&, а также для локальных переменных при работе с частями строк. Вообще, для строковых параметров функций, если функция не меняет строку и не собирается передавать её в C-API, используйте simple_str by value.

Второй класс — simple_str_nt. По устройству и назначению совпадает с simple_str, но дает гарантии нуль-терминированности строки. То есть если функции надо переданный параметр без изменений передать дальше как C-строку в какое то API, она должна использовать для параметра тип simple_str_nt. Все классы владеющих строк могут быть преобразованы в simple_str_nt, так как хранят строки с завершающим нулём. Это позволяет писать функции с единым типом параметра, принимающим на вход любой тип владеющих строковых объектов. Наследуется от simple_str, и поддерживает все константные операции со строками.

Алиасы:
- stra для simple_str_nt<char>
- stru для simple_str_nt<char16_t>
- struu для simple_str_nt<char32_t>
- strw для simple_str_nt<wchar_t>

Класс sstring (shared string)

Класс, могущий хранить иммутабельную строку. То есть ему можно присвоить некую строку только целиком, модифицировать саму строку нельзя. Владеет строкой.

Алиасы:
- stringa для sstring<char>
- stringu для sstring<char16_t>
- stringuu для sstring<char32_t>
- stringw для sstring<wchar_t>

Как и simple_str, реализует все методы, не модифицирующие строку. То, что хранимая строка иммутабельна, позволяет применить ряд оптимизаций:

  • Для строк, не подходящих для SSO, использует общий разделяемый буфер с атомарным счётчиком ссылок. Позволяет быстро копировать строку без необходимости блокировок доступа к содержимому буфера.

  • Нет необходимости хранить размер буфера (capacity) — всё равно мы ничего не дописываем в буфер.

  • Позволяет просто ссылаться на строковые литералы программы, не копируя их символы в какой-либо буфер:
    stringa str = "Hello!"; // Ничего не стоит, не копирует байты строки

    stringa ltr = stra{"Hello!"}; // Копирует байты строки в ltr

    stringa atr = "Hello!"_ss; // Копирует байты строки в atr


Также в классе применяется SSO – Small String Optimization. Короткие строки помещаются внутри самого объекта во внутренний буфер.

Размеры:
Для 64 бит:

  • stringa – класс 24 байта, SSO до 23 символов.

  • stringu – класс 32 байта, SSO до 15 символов.

  • stringuu – класс 32 байта, SSO до 7 символов.

Для 32 бит:

  • stringa – класс 16 байт, SSO до 15 символов.

  • stringu – класс 24 байта, SSO до 11 символов.

  • stringuu – класс 24 байта, SSO до 5 символов.

Класс lstring<K, N, forShare> (local string) - класс, хранящий строку и позволяющий её модифицировать.

Алиасы:

  • lstringa<N=16> для lsrting<char, N, false>

  • lstringu<N=16> для lsrting<char16_t, N, false>

  • lstringuu<N=16> для lsrting<char32_t, N, false>

  • lstringw<N=16> для lsrting<wchar_t, N, true>

  • lstringsa<N=16> для lsrting<char, N, true>

  • lstringsu<N=16> для lsrting<char16_t, N, true>

  • lstringsuu<N=16> для lsrting<char32_t, N, true>

  • lstringsw<N=16> для lsrting<wchar_t, N, true>

В качестве N в параметре шаблона задаётся размер внутреннего буфера для хранения символов. Строки длиной до N символов хранятся внутри объекта, при превышении количества — аллоцируется динамический буфер, в который сохраняются символы. При копировании объекта все символы всегда копируются.

Если forShare == true и символы не влезают в локальный буфер, то динамический буфер создается с дополнительным местом, так чтобы совпадать по структуре с буфером sstring. Тогда при перемещении lstring в sstring – переместится только указатель на буфер, без излишнего копирования символов.

Этот класс удобен для работы со строками как локальная переменная на стеке. Обычно мы предполагаем примерный размер строк, с котороми будем работать, и можем создать локальную строку с буфером на стеке, и работать с ней. При этом не опасаясь переполнения буфера, так как в этом случае строка переключится на динамический буфер.

Небольшой пример использования с пояснениями:

#ifdef _WIN32
const char path_separator = '\\';
#else
const size_t MAX_PATH = 260;
const char path_separator = '/';
#endif

auto get_current_dir() {
#ifdef _WIN32
    /* заполняем буфер wchar_t строки lstringw<MAX_PATH> из GetCurrentDirectoryW с возможным
    увеличением буфера и конвертируем в ut8 char. В конструкторе используется то, что появилось
    только в С++23 как `resize_and_overwrite`, а у нас было изначально :) */
    lstringa<MAX_PATH> path{lstringw<MAX_PATH>{ [](auto p, auto s) { return GetCurrentDirectoryW(DWORD(s + 1), p); }}};
    /* Это одна строка делает то же самое, что и вот такой C код.
    typedef struct lstringa_MAX_PATH_t {
        char* data;
        size_t length;
        size_t capacity;
        char local_buffer[MAX_PATH + 1];
    } lstringa_MAX_PATH;
    lstringa_MAX_PATH* get_current_dir(lstringa_MAX_PATH* result) {
        wchar_t buffer[MAX_PATH + 1], *buf = buffer;
        DWORD size = sizeof(buffer) / sizeof(wchar_t), lengthOfpath;
        for (;;) {
            // Возвращает либо количество скопированных символов без учёта завершающего нуля,
            //   либо если буфер мал, то нужный размер буфера вместе с завершающим нулём *.
            DWORD ret = GetCurrentDirectoryW(size, buf);
            if (ret < size) {
                // Влезло в буфер. Однако в Windows пути могут быть и длиннее, чем MAX_PATH,
                //   если начинаются с \\?\
                //   https://learn.microsoft.com/ru-ru/windows/win32/fileio/maximum-file-path-limitation?tabs=registry
                lenOfpath = ret;
                break;
            }
            size = ret;
            if (buf != buffer)
                free(buf);
            buf = malloc(size);
        }
        utf16toUtf8(buf, lengthOfPath, result);
        if (buf != buffer)
            free(buf);
        return result;
    }
    */
#else
    lstringa<MAX_PATH> path{ [](char* p, size_t s) {
        const char* res = getcwd(p, s + 1);
        if (res) {
            return stra{res}.length();
        }
        if (errno == ERANGE)
            return s * 2;
        return 0ul;
    }};
#endif
    if (!path.length() || path.at(-1) != path_separator) {
        path += e_c(1, path_separator);
    }
    return path;
}

stringa build_full_path(ssa fileName) {
    return get_current_dir() + fileName + ".txt";
    /*
    Здесь сначала на стеке создастся временный объект lstringa<MAX_PATH> для вызова get_current_dir.
    Функция get_current_dir заполнит его названием текущего каталога.
    В 99.9% случаев для этого хватит локального буфера на стеке.
    После рассчитывается общая длина для результата - длина current_dir + длина fileName + 4.
    Определяется буфер для строки конечного результата - если длина меньше 24 — строка будет размещена прямо в stringa,
    иначе аллоцируется буфер для результирующей строки сразу нужного размера.
    Затем в буфер результирующей строки последовательно копируются символы из current_dir, file_name, ".txt";
    Ну и благодаря RVO - место для самого результата (stringa) - отводится в вызывающей функции,
    то есть никакого дополнительного копирования при возврате не будет.

    Таким образом, будет максимум всего две аллокации памяти (если current_dir не влезет в MAX_PATH),
    или одна, если результирующая строка длиннее 23 символов, при этом эта аллокация будет сразу нужного размера.
    */
}

В этом примере вы наверняка заметили, как конкатенируются строки и задались вопросом — как же при двух сложениях длина всего результата считалась всего один раз, чтобы выделить необходимое место сразу?

Ответ на этот вопрос:

Строковые выражения

Дело в том, что в библиотеке нет сложения строк как такового. Сложение выполняется для «строковых выражений». Строковое выражение — это любой объект произвольного типа, имеющий функции length и place. Функция length – возвращает длину строки, функция place – помещает символы строки в переданный ей буфер.

Любая владеющая строка может инициализироваться строковым выражением — она запрашивает у него длину, выделяет место для хранения символов, и передает это место строковому выражению, вызывая его функцию place.

Для строковых выражений определена шаблонная функция сложения:

template<StrExpr A, StrExprForType<typename A::symb_type> B>
inline auto operator + (const A& a, const B& b) {
    return strexprjoin<A, B>{a, b};
}

strexprjoin – шаблонный тип, который сам является строковым выражением. В себе он хранит ссылки на два переданных ему строковых выражения. При запросе длины он выдает сумму длин двух строковых выражений, а при размещении символов — сначала размещает первое выражение, затем второе.

template<StrExpr A, StrExprForType<typename A::symb_type> B>
struct strexprjoin {
    using symb_type = typename A::symb_type;
    const A& a;
    const B& b;
    constexpr strexprjoin(const A& a_, const B& b_) : a(a_), b(b_){}
    constexpr size_t length() const noexcept { return a.length() + b.length(); }
    constexpr symb_type* place(symb_type* p) const noexcept {return b.place(a.place(p));}
};

Таким образом, операция сложения строковых выражений создает объект, также являющийся строковым выражением, к которому может быть применена следующая операция сложения, и который рекурсивно хранит ссылки на слагаемые части, каждая из которых знает свой размер и умеет размещать себя в буфере результата. И так далее, к каждому получаемому строковому выражению можно снова применить operator +, формируя цепочку из нескольких строковых выражений, и в итоге "материализовать" последний получившийся объект, который сначала посчитает размер всей общей памяти для конечного результата, а затем разместит вложенные подвыражения в один буфер.

Все строковые типы библиотеки сами являются строковыми выражениями, просто копирующими свои символы.

Также operator + определён для строковых выражений и строковых литералов, строковых выражений и чисел (числа конвертируются в десятичное представление), а также вы можете сами добавить желаемые типы.

Пример:

stringa text = header + " count=" + count + ", done";

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

  - expr_spaces<ТипСимвола, КоличествоСимволов, Символ>{}: выдает строку длиной КоличествоСимволов, заполненную заданным символом. Количество символов и символ - константы времени компиляции. Для некоторых случаев есть сокращенная запись:
- e_spca<КоличествоСимволов>(): строка char пробелов
    - e_spcw<КоличествоСимволов>(): строка w_char пробелов

  - expr_pad<ТипСимвола>{КоличествоСимволов, Символ}: выдает строку длинной КоличествоСимволов, заполненную заданным символом. Количество символов и символ могут задаваться в рантайме. Сокращенная запись: e_c(КоличествоСимволов, Символ)

  - e_choice(bool Condition, StrExpr1, StrExpr2): если Condition == true, результат будет равен StrExpr1, иначе StrExpr2

  - e_num<ТипСимвола>(ЦелоеЧисло): конвертирует число в десятичное представление. Редко используется, так как для строковых выражений и чисел переопределен оператор "+", и число можно просто написать как text + number;

  - e_real<ТипСимвола>(ВещественноеЧисло): конвертирует число в десятичное представление. Редко используется, так как для строковых выражений и чисел переопределен оператор "+", и число можно просто написать как text + number;

  - e_join<bool ПослеПоследнего = false, bool ПропускатьПустые = false>(контейнер, "Разделитель"): конкатенирует все строки в контейнере, используя разделитель. Если ПослеПоследнего == true, то разделитель добавляется и после последнего элемента контейнера, иначе только между элементами. Если ПропускатьПустые == true, то пустые строки не добавляют разделитель, иначе для каждой пустой строки тоже вставляется разделитель

  - e_repl(ИсходнаяСтрока, "Искать", "Заменять"): заменяет в исходной строке вхождения "Искать" на "Заменять".  Шаблоны поиска и замены - строковые литералы времени компиляции.

  - expr_replaced<ТипСимвола>{ИсходнаяСтрока, Искать, Заменять}: заменяет в исходной строке вхождения Искать на Заменять. Шаблоны поиска и замены - могут быть любыми строковыми объектами в рантайме.

 и т.д. и т.п.

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

Заключение

На этом я заканчиваю вводный обзор библиотеки simstr. Более подробно различные аспекты оптимального использования библиотеки будут рассматриваться в следующих статьях.

Кратко опишу ещё раз её особенности:

  • Удобно используется, работать со строками становится удовольствием

  • Быстра и оптимальна при выполнении

  • Строки UTF-8, UTF-16, UTF-32 с авто-конвертацией (через simdutf)

  • С тестами

  • С впечатляющими бенчмарками

  • Реальный пример использования в утилите обработки бенчмарков

  • Есть doxygen дока

  • Работает и в WASM через Emscripten

В комментах приветствуются идеи по новым бенчмаркам: вы накидываете код с std::string, который доставляет вам боль, я пытаюсь его вылечить и сравнить :-)

Теги:
Хабы:
+16
Комментарии10

Публикации

Ближайшие события