mirror of https://github.com/orefkov/simstr.git
794 lines
59 KiB
Markdown
794 lines
59 KiB
Markdown
# Строки в С++
|
||
(, что с вами не так?)
|
||
|
||
<span class="obfuscator"><a href="overview.md">On English | По-английски</a></span>
|
||
|
||
<cite>
|
||
В ретроспективе 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"
|
||
(«Их отсутствие привело к тому, что все заново изобретали велосипед, и к ненужному разнообразию в самых фундаментальных классах»).
|
||
</cite>
|
||
|
||
## Что было и есть
|
||
Во вступительной части я хочу немного описать, каково ныне состояние со строками в С++, как мы к нему докатились и почему оно таково.
|
||
А также описать недостатки текущих реализаций, чтобы были понятны решения, которые я использую в своей библиотеке строк.
|
||
|
||
Собственно, изначально как такового стандартного типа для строк в С++ не было.
|
||
Для работы со строками использовался подход из C – строка есть указатель на массив байтов, оканчивающихся нулём.
|
||
Недостатки таких строк — невозможно в строке использовать байт `0`, т. е. не подходит для бинарных данных,
|
||
непонятна стратегия управления/владения ресурсами, ну и основной недостаток — длину строки приходится вычислять каждый раз,
|
||
перебирая все её символы.
|
||
|
||
Откуда ноги растут у такого решения вполне понятно — со времён динозавров: как динозавры были большие, с маленьким мозгом и
|
||
короткими ручками, так и компьютеры были большие, память у них была маленькая, а строки короткими. Сэкономить память на хранении длины строки было важнее, чем потерять время на повторный подсчет длины.
|
||
|
||
Первые попытки стандартизировать строки как класс начались только в С++98 - std::string появился, как часть STL, и как
|
||
многое из STL, крайне неоднозначно воспринимался программистами.
|
||
|
||
И первое, что приходит в голову при улучшении C-строк — надо хранить длину строки:
|
||
```cpp
|
||
struct simple_string {
|
||
const char* data;
|
||
size_t length;
|
||
};
|
||
```
|
||
|
||
При наличии такой строки, уже множество алгоритмов значительно оптимизируются.
|
||
Например, при сравнении двух строк на равенство мы можем даже не начинать сравнивать их символы, если длины строк не равны.
|
||
Более того, этих данных абсолютно достаточно для всех методов, которые не модифицируют строку.
|
||
Также заметим, что такой объект на современных 64-битных архитектурах прекрасно передается в функции по значению —
|
||
оба его поля укладываются в регистры (ну, кроме windows), что облегчает работу оптимизатору компилятора.
|
||
|
||
Между тем, такое решение попало в стандарт только аж в С++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 байт, в некоторых до 23.
|
||
Однако эта оптимизация тоже палка о двух концах, и может в различных реализациях усложнить перемещение строки - если она хранит
|
||
указатель на свой внутренний буфер, его придётся корректировать.
|
||
|
||
А без COW мутабельность строк приводит к тому, что любая инициализация объекта строки приводит к копированию байтов.
|
||
Посмотрим такой код:
|
||
```cpp
|
||
const char* text1 = "Hello, World"; // ничего не стоит
|
||
std::string_view text2 = "Hello, World"; // Ничего не стоит, вычисляет длину строки при компиляции
|
||
std::string text3 = "Hello, World"; // В рантайме каждый раз копирует символы строки
|
||
```
|
||
|
||
(Удостоверится в правдивости комментариев можно на https://godbolt.org/z/51oKGWT5T )
|
||
|
||
Но если нам дальше по коду не нужно никак модифицировать строку, мы зря платим за аллокацию, копирование символов,
|
||
а также за деструктор строки. То есть хотелось бы иметь как минимум два варианта строк — мутабельные и иммутабельные,
|
||
чтобы явно дать понять компилятору, что мы не собираемся модифицировать строку.
|
||
Или банальный пример — мы парсим какой-то входящий буфер данных, нам нужно проверить, равен ли некий кусок буфера строке
|
||
”hello” на «чистом С++», т. е. без всяких memcmp и strcmp. До появления string_view приходилось делать примерно так:
|
||
```cpp
|
||
bool is_part_buffer_equal_hello(const char* data, int start, int end) {
|
||
return std::string(data + start, end - start) == "hello";
|
||
}
|
||
```
|
||
Тут получается, сначала копируются символы из буфера data в буфер временной строки, возможно с аллокацией памяти, и лишь
|
||
потом временная строка сравнивается с ”hello”, а потом ещё и деструктор и раскрутка стека на случай исключения.
|
||
|
||
При использовании же вместо `std::string` `std::string_view` – код на C++ почти не меняется:
|
||
```cpp
|
||
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’)).
|
||
Ни создания временного объекта, ни копирования байтов, ни деструктора, ни раскрутки стека для исключений.
|
||
Убедится можно на https://godbolt.org/z/9fo188e7c
|
||
|
||
Казалось бы, ну вот же в С++17 появился `string_view`, пожалуйста, используй его в параметрах своих функций вместо `const std::string&`,
|
||
и будет счастье. Но тут тоже есть нюанс — всё отлично работает, пока нам не нужно передать строку в стороннее C-API: string_view не даёт
|
||
гарантий нуль-терминированности строки, поэтому его data() нельзя передать в стороннее C-API, и потому всё-равно придётся сначала
|
||
скопировать его в `std::string`. А раз нужен `std::string`, то и параметром функции оптимальнее cделать `const std::string&`
|
||
и далее по цепочке, все параметры вновь станут `const std::string&`.
|
||
|
||
### Конкатенация строк
|
||
Далее, после инициализации строки, самая частая мутабельная операция с ними, скорее всего конкатенация строк, либо в виде просто
|
||
сложения строк, либо добавления строки к строке. И именно она легко может вызывать как неоптимальную производительность при неграмотном
|
||
использовании, так и оверхед по памяти, даже при грамотном использовании.
|
||
|
||
Рассмотрим простой код ( https://godbolt.org/z/odx7W1Pv7 )
|
||
```cpp
|
||
#include <string>
|
||
void some_outer_function(const std::string&);
|
||
|
||
void func(const std::string& s1, const std::string& s2) {
|
||
std::string concat = s1 + s2 + "hello";
|
||
some_outer_function(concat);
|
||
}
|
||
```
|
||
Как видим, и в clang, и в GCC создается несколько временных объектов, в которые последовательно перекладываются символы строк,
|
||
и как результат — мы получаем несколько лишних аллокаций для промежуточных буферов, символы из строк копируются несколько лишних
|
||
раз из промежуточных буферов. В идеале для лучшей производительности такой код нужно переписать так:
|
||
```cpp
|
||
#include <string>
|
||
void some_outer_function(const std::string&);
|
||
|
||
void func(const std::string& s1, const std::string& s2) {
|
||
static const std::string_view hello = "hello";
|
||
std::string concat;
|
||
concat.reserve(s1.size() + s2.size() + hello.size());
|
||
concat += s1;
|
||
concat += s2;
|
||
concat += hello;
|
||
some_outer_function(concat);
|
||
}
|
||
```
|
||
|
||
К сожалению, пока ни один компилятор не оптимизирует первый простой код до уровня второго более оптимального кода, а писать
|
||
такой код каждый раз руками довольно неудобно. То есть опять приходится платить за то, чем не пользуешься.
|
||
Да и в этом случае вполне может возникнуть оверхед по памяти — операции добавления строки обычно во всех реализациях увеличивают
|
||
размер буфера строки не меньше, чем в два раза, считая, что скоро к строке могут снова что-нибудь добавить.
|
||
Поэтому если строку не планируется более модифицировать, но время её жизни ещё не подошло к концу (например, это поле
|
||
какого-либо класса), нужно ещё не забыть сделать на ней `shrink_to_fit`.
|
||
|
||
Между тем, часто основной сценарий использования строк — это как раз некая подготовка строки путём нескольких модификаций и конкатенаций,
|
||
а затем она где-то хранится, более не меняясь. При этом программист обычно знает, примерно какой размер строк ожидается в этом месте,
|
||
и мог бы выделить буфер для этих промежуточных модификаций прямо на стеке, прибегая к динамической аллокации только при превышении
|
||
размера этого буфера. Однако с текущей реализацией строк это сделать довольно проблематично, либо неудобно.
|
||
|
||
Подытожим, что имеем на данный момент:
|
||
|
||
- «Из коробки» в С++ для работы со строками сейчас имеется `std::string`.
|
||
- Строки подразумеваются мутабельными, что приводит к обязательному копированию всех символов строки при инициализации и
|
||
копировании объектов строк.
|
||
- Соответственно, не имеем возможности быстрого копирования строк, даже если не планируем потом менять копию.
|
||
- Конкатенация нескольких строк — задача, могущая выполнятся неоптимально, приводить к оверхеду по памяти, написать оптимальный код сложно.
|
||
- Есть костыль для иммутабельных строк в виде `std::string_view`, однако он не решает вопросы владения строкой, поэтому по сути
|
||
годится только как тип для передачи параметров в функции, не меняющие строки, с оговоркой, что не может использоваться в функциях,
|
||
вызывающих C-API, так как не даёт гарантий нуль-терминированности.
|
||
- Ну и к `std::string` есть вопросы, что несмотря на то, что это класс для строк, собственно для работы со строками в нём крайне куцый
|
||
функционал по сравнению с тем, к чему привыкли в других языках — к примеру нет замены подстрок по шаблону (в других языках это обычно
|
||
replace, но в С++ эта функция делает совершенно другое), trim, split, join, upper, lower и т. п.
|
||
Эти функции приходится каждый раз писать самому, и не факт, что у всех это получится оптимально.
|
||
|
||
Надеюсь, после этого небольшого вступления вам будет более понятно, какие проблемы я решал своей строковой библиотекой и каким образом.
|
||
|
||
## Библиотека simstr
|
||
Собственно, нельзя сказать, что «я свелосипедил свою реализацию класса для строк».
|
||
Как я ранее показал, сложно, а то и даже невозможно написать один единый строковый класс, хорошо подходящий для всех сценариев
|
||
использования. Именно поэтому у меня не строковый класс, а строковая библиотека, которая содержит несколько разных строковых типов,
|
||
от более простых к более сложным, каждый из которых имеет свои сильные и слабые стороны, и пользователю нужно грамотно подходить к
|
||
вопросу, какой из этих классов в каком случае стоит использовать.
|
||
|
||
Саму библиотеку я начал потихоньку разрабатывать ещё в 2011-2012 годах, когда у нас уже появилась семантика перемещения, но ещё
|
||
не было std::string_view. Однако сейчас минимальная версия стандарта для работы библиотеки: **C++20** – используются концепты и \<format\>.
|
||
|
||
Сначала я расскажу о классах библиотеки для самих строк, а потом о том, как в ней оптимально решается задача конкатенации строк.
|
||
|
||
Несколько общих моментов:
|
||
- Все классы для работы со строками шаблонизированы типом символов, но подразумевается, что символы могут быть `char`, `char8_t`, `char16_t`,
|
||
`char32_t`, `wchar_t`.
|
||
- Все строки имеют явную длину.
|
||
- Классы владельцы строк хранят их с завершающим нулем в конце, который не входит в длину строки.
|
||
- В самой строке могут содержаться нулевые символы, все алгоритмы работают только через длину строки, не обращая на них внимания.
|
||
- Библиотека считает разные строковые типы "совместимыми", если они имеют одинаковый размер символов.
|
||
То есть `char` и `char8_t` всегда тождественны, а также в Linux тождественны `wchar_t` и `char32_t`, а в Windows `wchar_t` и `char16_t`.
|
||
- Классы владельцы строк могут инициализироваться строками другого типа символов, выполняя конвертацию между UTF-8, UTF-16, UTF-32.
|
||
При этом если строки разных, но совместимых типов, они просто копируются без конвертации.
|
||
- Для смены регистра символов и сравнения строк без учёта регистра используются встроенные таблицы для первой плоскости юникода
|
||
(до 0xFFFF). Строки считаются представленными в кодировке UTF-8, UTF-16, UTF-32 соответственно.
|
||
Однако не делается нормализация строк и не обрабатываются ситуации, когда смена регистра символа приводит к изменению их количества.
|
||
То есть преобразование регистра символов соответствует `std::towupper`, `std::towlower` для unicode локали,
|
||
только быстрее и может работать с любым видом символов.
|
||
Если вам нужна строгая работа с юникодом, используйте другие средства, например ICU.
|
||
|
||
### Классы строк.
|
||
|
||
#### Первый самый простой класс строки называется, естественно, `simple_str` :)
|
||
(simstr::simple_str)
|
||
|
||
Класс просто представляет собой указатель на начало константной строки и её длину, по сути то же самое, что `std::string_view`.
|
||
Предназначен для работы с иммутабельными строками, не владеющий ими, то есть вы должны сами озаботиться тем, что реальная строка,
|
||
представленная через `simple_str` – жива во время его использования.
|
||
|
||
Реализует все строковые методы, не модифицирующие строку.
|
||
|
||
Алиасы:
|
||
- `ssa` для simple_str\<char\>
|
||
- `ssb` для simple_str\<char8_t\>
|
||
- `ssu` для simple_str\<char16_t\>
|
||
- `ssw` для simple_str\<wchar_t>
|
||
- `ssuu` для simple_str\<char32_t\>
|
||
|
||
Применяется в основном для передачи строк как параметр функций, не модифицирующих переданную строку, вместо `const std::string&`,
|
||
а также для локальных переменных при работе с частями строк.
|
||
|
||
#### Второй класс — `simple_str_nt`
|
||
(simstr::simple_str_nt)
|
||
|
||
По устройству и назначению совпадает с `simple_str`, но дает гарантии нуль-терминированности строки.
|
||
То есть если функции надо переданный параметр без изменений передать дальше как C-строку в какое то API, она должна использовать для
|
||
параметра тип `simple_str_nt`.
|
||
Все классы владеющих строк (simstr::sstring, simstr::lstring) могут быть преобразованы в `simple_str_nt`, так как хранят строки с завершающим нулём.
|
||
Это позволяет писать функции с единым типом параметра, принимающим на вход любой тип владеющих строковых объектов.
|
||
|
||
Алиасы:
|
||
- `stra` для simple_str_nt\<char>
|
||
- `strb` для simple_str_nt\<char8_t>
|
||
- `stru` для simple_str_nt\<char16_t>
|
||
- `strw` для simple_str_nt\<wchar_t>
|
||
- `struu` для simple_str_nt\<char32_t>
|
||
|
||
Может инициализироваться строковыми литералами:
|
||
```cpp
|
||
stra text = "Text";
|
||
```
|
||
Длина в этом случае вычисляется сразу при компиляции. Аналогично `simple_str_nt` создается с помощью `operator""_ss`:
|
||
|
||
```cpp
|
||
stringa result = "Count: "_ss + count;
|
||
```
|
||
|
||
#### Класс sstring (shared string).
|
||
(simstr::sstring)
|
||
|
||
Класс, умеющий хранить иммутабельную строку.
|
||
То есть ему можно присвоить некую строку только целиком, модифицировать символы строки нельзя.
|
||
|
||
Владеет строкой, управляет памятью для символов строки.
|
||
Хранит со строками завершающий нуль, и может быть источником для `simple_str_nt`, для передачи в C-API.
|
||
Так же, как и `simple_str`, реализует все методы, не модифицирующие строку.
|
||
|
||
Алиасы:
|
||
- `stringa` для sstring\<char>
|
||
- `stringb` для sstring\<char8_t>
|
||
- `stringu` для sstring\<char16_t>
|
||
- `stringw` для sstring\<wchar_t>
|
||
- `stringuu` для sstring\<char32_t>
|
||
|
||
|
||
То, что хранимая строка иммутабельна, позволяет применить ряд оптимизаций:
|
||
- Для строк, не подходящих для SSO, использует общий разделяемый буфер с атомарным счётчиком ссылок.
|
||
Позволяет быстро копировать строку без необходимости блокировок доступа к содержимому буфера.
|
||
- Нет необходимости хранить размер буфера (capacity) — всё равно мы ничего не дописываем в буфер.
|
||
- Позволяет просто ссылаться на литералы программы, не копируя их символы в какой-либо буфер:
|
||
```cpp
|
||
stringa str = "Hello!"; // Ничего не стоит, не копирует байты строки
|
||
stringa ltr = stra{"Hello!"}; // А вот тут копирует байты строки в ltr
|
||
```
|
||
|
||
Также в классе применяется **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, forShared> (local string)
|
||
(simstr::lstring)
|
||
|
||
Класс, хранящий строку и позволяющий её модифицировать.
|
||
Владеет строкой, управляет памятью для символов строки.
|
||
Хранит со строками завершающий нуль, и может быть источником для `simple_str_nt`, для передачи в C-API.
|
||
Как и все остальные классы, реализует все методы, не модифицирующие строку.
|
||
|
||
В качестве `N` в параметре шаблона задаётся размер внутреннего буфера для хранения символов.
|
||
Строки длиной до N символов хранятся внутри объекта, а при превышении этого количества — аллоцируется динамический буфер,
|
||
в который сохраняются символы. При копировании объекта все символы также всегда копируются.
|
||
|
||
Если `forShare` == true и символы не помещаются в локальный буфер, то динамический буфер создается с дополнительным местом,
|
||
так чтобы совпадать по структуре с буфером `sstring`. Тогда при перемещении `lstring` в `sstring` – переместится только указатель
|
||
на буфер, без излишнего копирования символов.
|
||
|
||
Этот класс удобен для работы со строками как локальная переменная на стеке.
|
||
Обычно мы предполагаем примерный размер строк, с котороми будем работать, и можем создать локальную строку с буфером на стеке,
|
||
и работать с ней. При этом не опасаясь переполнения буфера, так как в этом случае строка переключится на динамический буфер.
|
||
|
||
Алиасы:
|
||
- `lstringa<N=15>` для lsrting\<char, N, false>
|
||
- `lstringb<N=15>` для lsrting\<char8_t, N, false>
|
||
- `lstringu<N=15>` для lsrting\<char16_t, N, false>
|
||
- `lstringw<N=15>` для lsrting\<wchar_t, N, false>
|
||
- `lstringuu<N=15>` для lsrting\<char32_t, N, false>
|
||
- `lstringsa<N=15>` для lsrting\<char, N, true>
|
||
- `lstringsb<N=15>` для lsrting\<char8_t, N, true>
|
||
- `lstringsu<N=15>` для lsrting\<char16_t, N, true>
|
||
- `lstringsw<N=15>` для lsrting\<wchar_t, N, true>
|
||
- `lstringsuu<N=15>` для lsrting\<char32_t, N, true>
|
||
|
||
|
||
Небольшой пример использования с пояснениями:
|
||
```cpp
|
||
#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); }}};
|
||
|
||
/* Эта одна строчка делает примерно то же самое, что и вот такой код.
|
||
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` – помещает символы строки в переданный ей буфер.
|
||
|
||
Любая владеющая строка (simstr::sstring, simstr::lstring) может инициализироваться строковым выражением — она запрашивает у него длину,
|
||
выделяет место для хранения символов, и передает это место строковому выражению, вызывая его функцию place.
|
||
|
||
Кроме того, все входящие в `simstr` строковые выражения могут преобразовываться в стандартные строки (`std::basic_string`)
|
||
совместимых типов символов, что позволяет применять быструю конкатенацию там, где заменить `std::string` пока невозможно.
|
||
Совместимые типы символов - такие, которые совпадают по размеру.
|
||
Для получения стандартной строки до C++23 используется `resize`, а потом заполнение через `data()`, начиная с C++23
|
||
используется более оптимальный `resize_and_overwrite`.
|
||
|
||
Для строковых выражений определена шаблонная функция сложения:
|
||
```cpp
|
||
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` – шаблонный тип, который сам является строковым выражением.
|
||
В себе он хранит ссылки на два переданных ему строковых выражения.
|
||
При запросе длины он выдает сумму длин двух строковых выражений, а при размещении символов — сначала размещает
|
||
в переданном буфере первое выражение, затем второе.
|
||
```cpp
|
||
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 +`, формируя цепочку из нескольких строковых выражений,
|
||
и в итоге "материализовать" последний получившийся объект, который сначала посчитает размер всей общей памяти для
|
||
конечного результата, а затем разместит вложенные подвыражения в один буфер.
|
||
|
||
Операция сложения строковых выражений позволяет конкатенировать строковые выражения разных, но совместимых типов.
|
||
То есть в одном выражении вы можете смешивать `""` и `u8""`, в Linux `L""` и `U""`, в Windows `L""` и `u""`
|
||
типы символов. Примеры есть в `tests\test_str.cpp TEST(SimStr, StrPrintfU8)`.
|
||
|
||
Все строковые типы библиотеки сами являются строковыми выражениями, то есть могут служить слагаемыми в конкатенациях
|
||
строковых выражений.
|
||
|
||
Также `operator+` определён для строковых выражений и строковых литералов, строковых выражений и чисел (числа конвертируются
|
||
в десятичное представление), а также вы можете сами добавить желаемые типы.
|
||
|
||
Пример:
|
||
```cpp
|
||
stringa text = header + " count=" + count + ", done";
|
||
```
|
||
|
||
Для стандартных строк (`std::basic_string` и `std::basic_string_view`) также сделаны операторы сложения со строковыми
|
||
выражениями, поэтому переменные этих типов также напрямую участвовать в операциях конкатенирования.
|
||
Однако cтандартные строки могут напрямую участвовать в строковых выражениях только когда
|
||
другой операнд тоже является строковым выражением. Если другой операнд - не строковое выражение,
|
||
используйте перед стандартными строками унарный `operator+`, чтобы превратить `std::basic_string` или `std::basic_string_view`
|
||
в строковое выражение.
|
||
|
||
Пример:
|
||
```cpp
|
||
std::string make_text(const std::string& text, std::string_view what, int count) {
|
||
// + превращает text в строковое выражение, а дальше они уже складываются между собой
|
||
return +text + " " + count + " " + what + e_if(count > 1, "s");
|
||
// what участвует уже напрямую как операнд со стороковым выражением
|
||
}
|
||
std::string make_answer(const std::string& text, std::string_view what, int count) {
|
||
return "Answer is: " + +text + " " + count + " " + what + e_if(count > 1, "s");
|
||
// + превращает text в строковое выражени, и оно может быть сложено с предыдущим строковым литералом
|
||
}
|
||
```
|
||
То есть унарный `+` нужен только где-то в начале выражения, если другой операнд не строковое выражение.
|
||
|
||
Существует несколько типов строковых выражений "из коробки", для выполнения различных операций со строками:
|
||
|
||
#### expr_spaces<ТипСимвола, КоличествоСимволов, Символ = ' '>{}
|
||
Выдает строку длиной КоличествоСимволов, заполненную заданным символом. Количество символов и символ - константы времени
|
||
компиляции. Для некоторых случаев есть сокращенная запись:
|
||
|
||
e_spca(КоличествоСимволов) - строка char пробелов
|
||
e_spcw(КоличествоСимволов) - строка w_char пробелов
|
||
|
||
#### expr_pad<ТипСимвола>{КоличествоСимволов, Символ = ' '}
|
||
Выдает строку длиной КоличествоСимволов, заполненную заданным символом.
|
||
Количество символов и символ могут задаваться в рантайме. Сокращенная запись:
|
||
|
||
e_c(КоличествоСимволов, Символ)
|
||
|
||
#### e_repeat{Str, count}
|
||
Генерирует повторение строкового литерала или выражения `Str` `count` раз.
|
||
|
||
e_repeat("aa", 10);
|
||
|
||
#### e_choice(bool Condition, StrExpr1, StrExpr2)
|
||
Если Condition == true, результат будет равен StrExpr1, иначе StrExpr2.
|
||
|
||
#### e_if(bool Condition, StrExpr1)
|
||
Если Condition == true, результат будет равен StrExpr1, иначе пустая строка.
|
||
|
||
#### expr_num<ТипСимвола>(ЦелоеЧисло)
|
||
Конвертирует число в десятичное представление. Редко используется, так как для строковых выражений и чисел
|
||
переопределен оператор "+", и число можно просто написать как `text + number`;
|
||
|
||
#### expr_real<ТипСимвола>(ВещественноеЧисло)
|
||
конвертирует число в десятичное представление. Редко используется, так как для строковых выражений и чисел
|
||
переопределен оператор "+", и число можно просто написать как `text + number`;
|
||
|
||
#### e_join<bool ПослеПоследнего = false, bool ТолькоНеПустые = false>(контейнер, "Разделитель")
|
||
Конкатенирует все строки в контейнере, используя разделитель. Если ПослеПоследнего == true,
|
||
то разделитель добавляется и после последнего элемента контейнера, иначе только между элементами.
|
||
Если ТолькоНеПустые == true, то пустые строки пропускаются без добавления разделителя.
|
||
|
||
#### e_repl(ИсходнаяСтрока, "Искать", "Заменять")
|
||
Заменяет в исходной строке вхождения "Искать" на "Заменять".
|
||
Шаблоны поиска и замены - строковые литералы времени компиляции.
|
||
|
||
#### expr_replaced<ТипСимвола>{ИсходнаяСтрока, Искать, Заменять}
|
||
Заменяет в исходной строке вхождения Искать на Заменять.
|
||
Шаблоны поиска и замены - могут быть любыми строковыми объектами в рантайме.
|
||
|
||
#### empty_expr<ТипСимвола>
|
||
Выдает пустую строку. Сокращённая запись — eea, eeu, eew, eeuu. Применяется если формирование строки начинается с числа и строкового литерала:
|
||
```cpp
|
||
str = eea + count + " times.";
|
||
```
|
||
так как оператор сложения определён только для сложения строкового выражения и числа.
|
||
Также замечу, что существует `operator""_ss`, который превращает строковый литерал в объект `simple_str_nt`, который уже является строковым выражением:
|
||
```cpp
|
||
str = "Count = "_ss + count;
|
||
...
|
||
str = count + " times."_ss;
|
||
```
|
||
|
||
#### Свои строковые выражения
|
||
Вы можете сами создавать свои типы строковых выражений для оптимального формирования строк в нужных вам целях и алгоритмах.
|
||
Для этого просто создайте тип с методами `length`, `place` и `typename symb_type`.
|
||
Примеры создания и использования из реальных проектов:
|
||
|
||
```cpp
|
||
/* Сформировать строку в JSON формате, в 16 битных символах */
|
||
struct expr_json_str {
|
||
using symb_type = u16s;
|
||
ssu text;
|
||
size_t l;
|
||
size_t length() const noexcept {
|
||
return l;
|
||
}
|
||
u16s* place(u16s* ptr) const noexcept;
|
||
expr_json_str(ssu t);
|
||
};
|
||
|
||
inline expr_json_str::expr_json_str(ssu t) : text(t) {
|
||
const u16s* ptr = text.symbols();
|
||
size_t add = 0;
|
||
|
||
for (size_t i = 0; i < text.length(); i++) {
|
||
switch (*ptr++) {
|
||
case '\b':
|
||
case '\f':
|
||
case '\r':
|
||
case '\n':
|
||
case '\t':
|
||
case '\"':
|
||
case '\\':
|
||
add++;
|
||
}
|
||
}
|
||
l = text.len + add;
|
||
}
|
||
|
||
inline u16s* expr_json_str::place(u16s* ptr) const noexcept {
|
||
const u16s *r = text.symbols();
|
||
size_t lenOfText = text.length(), lenOfTail = l;
|
||
while (lenOfTail > lenOfText) {
|
||
u16s s = *r++;
|
||
switch (s) {
|
||
case '\b':
|
||
*ptr++ = '\\';
|
||
*ptr++ = 'b';
|
||
lenOfTail--;
|
||
break;
|
||
case '\f':
|
||
*ptr++ = '\\';
|
||
*ptr++ = 'f';
|
||
lenOfTail--;
|
||
break;
|
||
case '\r':
|
||
*ptr++ = '\\';
|
||
*ptr++ = 'r';
|
||
lenOfTail--;
|
||
break;
|
||
case '\n':
|
||
*ptr++ = '\\';
|
||
*ptr++ = 'n';
|
||
lenOfTail--;
|
||
break;
|
||
case '\t':
|
||
*ptr++ = '\\';
|
||
*ptr++ = 't';
|
||
lenOfTail--;
|
||
break;
|
||
case '\"':
|
||
*ptr++ = '\\';
|
||
*ptr++ = '\"';
|
||
lenOfTail--;
|
||
break;
|
||
case '\\':
|
||
*ptr++ = '\\';
|
||
*ptr++ = '\\';
|
||
lenOfTail--;
|
||
break;
|
||
default:
|
||
*ptr++ = s;
|
||
break;
|
||
}
|
||
lenOfTail--;
|
||
lenOfText--;
|
||
}
|
||
if (lenOfTail) {
|
||
std::char_traits<u16s>::copy(ptr, r, lenOfTail);
|
||
ptr += lenOfTail;
|
||
}
|
||
return ptr;
|
||
}
|
||
```
|
||
|
||
Использование:
|
||
|
||
```cpp
|
||
........
|
||
chunked_string_builder<u16s> vtText;
|
||
........
|
||
vtText << uR"({"#type":"jxs:string","#value":")" + expr_json_str(name) + u"\"}";
|
||
.......
|
||
```
|
||
|
||
Ещё пример
|
||
```cpp
|
||
/* Нужно сформировать бинарные данные в BASE64 формате, в 16 битных символах */
|
||
struct expr_str_base64 {
|
||
using symb_type = u16s;
|
||
ssa text;
|
||
size_t length() const noexcept {
|
||
return (text.len + 2) / 3 * 4;
|
||
}
|
||
u16s* place(u16s* ptr) const noexcept;
|
||
expr_str_base64(ssa t) : text(t) {}
|
||
};
|
||
|
||
inline u16s* expr_str_base64::place(u16s* ptr) const noexcept {
|
||
static constexpr u8s alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||
|
||
const unsigned char* t = (const unsigned char*)text.str;
|
||
|
||
size_t i = 0;
|
||
if (text.len > 2) {
|
||
for (; i < text.len - 2; i += 3) {
|
||
*ptr++ = alphabet[(t[i] >> 2) & 0x3F];
|
||
*ptr++ = alphabet[((t[i] & 0x3) << 4) | ((int)(t[i + 1] & 0xF0) >> 4)];
|
||
*ptr++ = alphabet[((t[i + 1] & 0xF) << 2) | ((int)(t[i + 2] & 0xC0) >> 6)];
|
||
*ptr++ = alphabet[t[i + 2] & 0x3F];
|
||
}
|
||
}
|
||
|
||
if (i < text.len) {
|
||
*ptr++ = alphabet[(t[i] >> 2) & 0x3F];
|
||
if (i == (text.len - 1)) {
|
||
*ptr++ = alphabet[((t[i] & 0x3) << 4)];
|
||
*ptr++ = '=';
|
||
} else {
|
||
*ptr++ = alphabet[((t[i] & 0x3) << 4) | ((int)(t[i + 1] & 0xF0) >> 4)];
|
||
*ptr++ = alphabet[((t[i + 1] & 0xF) << 2)];
|
||
}
|
||
*ptr++ = '=';
|
||
}
|
||
return ptr;
|
||
}
|
||
```
|
||
|
||
Использование:
|
||
```cpp
|
||
......
|
||
chunked_string_builder<u16s> vtText;
|
||
......
|
||
vtText << u"{\"#\",87126200-3e98-44e0-b931-ccb1d7edc497,{1,{#base64:" + expr_str_base64(v) + u"}}},";
|
||
......
|
||
```
|
||
|
||
И ещё
|
||
|
||
```cpp
|
||
/* Нужно преобразовать tm в строку даты/времени в 16-битных символах */
|
||
struct expr_str_tm {
|
||
using symb_type = u16s;
|
||
const tm& t;
|
||
size_t length() const noexcept {
|
||
return 19;
|
||
}
|
||
u16s* place(u16s* ptr) const noexcept;
|
||
expr_str_tm(const tm& _t) : t(_t) {}
|
||
};
|
||
|
||
inline u16s* expr_str_tm::place(u16s* ptr) const noexcept {
|
||
if constexpr (sizeof(wchar_t) == 2) {
|
||
// Под Windows можно сразу форматнуть строку в нужный буфер
|
||
std::swprintf((wchar_t*)ptr, 20, L"%04i-%02i-%02i %02i:%02i:%02i", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday,
|
||
t.tm_hour, t.tm_min, t.tm_sec);
|
||
} else {
|
||
// Сначала форматнём в промежуточный буфер, потом скопируем в результат
|
||
char buf[20];
|
||
std::snprintf(buf, 20, "%04i-%02i-%02i %02i:%02i:%02i", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour,
|
||
t.tm_min, t.tm_sec);
|
||
for (unsigned i = 0; i < 19; i++) {
|
||
ptr[i] = buf[i];
|
||
}
|
||
}
|
||
return ptr + 19;
|
||
}
|
||
```
|
||
|
||
Использование
|
||
|
||
```cpp
|
||
......
|
||
bool makeBind(SqliteQuery& query, tVariant& param, unsigned paramNum) {
|
||
switch (param.vt) {
|
||
......
|
||
case VTYPE_DATE:
|
||
query.bind(paramNum, lstringu<30>{expr_str_tm{winDateToTm(param.date)}}.to_str());
|
||
break;
|
||
case VTYPE_TM:
|
||
query.bind(paramNum, lstringu<30>{expr_str_tm{param.tmVal}}.to_str());
|
||
break;
|
||
......
|
||
```
|
||
|
||
ВНИМАНИЕ: обычно поля в объектах строковых выражений являются ссылками на исходные данные.
|
||
И ссылки эти почти всегда ведут на локальные или временные объекты. Поэтому крайне рискованно возвращать строковые выражения
|
||
из функций — надо сто раз проверить, что в них не попали ссылки на локальные или временные переменные.
|
||
Возьмите за правило — можно легко передавать строковые выражения в функции, и опасно возвращать их из функций.
|
||
Лучше при возврате материализовать строковое выражение в строковый объект, содержащий итоговую строку.
|
||
При желании тип возвращаемой строки можно задать шаблонным параметром.
|
||
|
||
|
||
### Класс chunked_string_builder
|
||
|
||
Предназначен для конкатенации множества строк.
|
||
Когда вам нужно последовательно формировать длинный текст из множества небольших кусочков (например, формируете html ответ
|
||
и т. п.) - последовательно складывать всё в один строковый объект крайне неоптимально — будет много переаллокаций и
|
||
перекопирования уже накопленных символов. В этом случае удобно использовать chunked_string_builder — всё, что он умеет,
|
||
это прибавлять строку к накопленным символам. Однако делает он это не в единый последовательный буфер памяти, а в отдельные
|
||
буфера, не меньше чем заданное выравнивание. При заполнении очередного буфера он просто создает ещё один буфер и продолжает
|
||
складывать данные в него.
|
||
|
||
То есть допустим вы задали выравнивание 1024.
|
||
Добавили несколько строк, заполнили буфер на 100 символов. И добавляете строку длинной 3000 символов.
|
||
При этом 924 символа скопируются в первый буфер, заполнив его до конца.
|
||
Для оставшихся 2076 создастся буфер размером 3072 символа, и они скопируются в него, в нём останется место для 996 символов.
|
||
Так последовательно каждый буфер заполняется до конца, и имеет размер кратный заданному выравниванию.
|
||
Таким образом избегаются переаллокации и перекопирование обработанных символов.
|
||
|
||
После окончательного заполнения вы можете работать с накопленными данными — либо слить все буфера в одну последовательную
|
||
строку (размер для буфера которой вы теперь уже знаете), либо перебирать их по отдельности, например, посылая эти буфера
|
||
в сеть. Либо последовательно копируя данные в буфер заданного размера.
|