Translation of the description and documentation into English has begun.

This commit is contained in:
Aleksandr Orefkov 2025-11-16 11:17:02 +03:00
parent 8d8ec95faa
commit b76486ff2b
8 changed files with 1354 additions and 426 deletions

View File

@ -48,7 +48,7 @@ PROJECT_NAME = "simstr"
# could be handy for archiving the generated documentation or if some version # could be handy for archiving the generated documentation or if some version
# control system is used. # control system is used.
PROJECT_NUMBER = 1.0 PROJECT_NUMBER = 1.2.4
# Using the PROJECT_BRIEF tag one can provide an optional one line description # Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewers a # for a project that appears at the top of each page and should give viewers a
@ -290,7 +290,8 @@ TAB_SIZE = 4
# with the commands \{ and \} for these it is advised to use the version @{ and # with the commands \{ and \} for these it is advised to use the version @{ and
# @} or use a double escape (\\{ and \\}) # @} or use a double escape (\\{ and \\})
ALIASES = ALIASES = ru=\~russian \
en=\~english
# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources # Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
# only. Doxygen will then generate output that is more tailored for C. For # only. Doxygen will then generate output that is more tailored for C. For

View File

@ -1,30 +1,31 @@
# Строки в С++ # Strings in C++
(, что с вами не так?) (, what's wrong with you?)
[On Russian|По-русски](overview_ru.md)
<cite> <cite>
В ретроспективе 1991 года по истории C++ его создатель Бьярне Страуструп назвал отсутствие стандартного строкового типа In a 1991 retrospective on the history of C++, its creator Bjarne Stroustrup called the lack of a standard string type
(и некоторых других стандартных типов) в C++ 1.0 худшей ошибкой, которую он допустил при его разработке: (and some other standard types) in C++ 1.0 the worst mistake he made in its development:
"the absence of those led to everybody re-inventing the wheel and to an unnecessary diversity in the most fundamental classes" "Their absence led to everyone reinventing the wheel and to an unnecessary diversity in the most fundamental classes"
(«Их отсутствие привело к тому, что все заново изобретали велосипед, и к ненужному разнообразию в самых фундаментальных классах»).
</cite> </cite>
## Что было и есть ## What was and is
Во вступительной части я хочу немного описать, каково ныне состояние со строками в С++, как мы к нему докатились и почему оно таково. In the introductory part, I want to briefly describe the current state of strings in C++, how we got to it, and why it is so.
А также описать недостатки текущих реализаций, чтобы были понятны решения, которые я использую в своей библиотеке строк. I will also describe the shortcomings of current implementations so that the solutions I use in my string library are clear.
Собственно, изначально как такового стандартного типа для строк в С++ не было. Actually, initially there was no standard type for strings in C++.
Для работы со строками использовался подход из C строка есть указатель на массив байтов, оканчивающихся нулём. The approach from C was used to work with strings a string is a pointer to an array of bytes ending in zero.
Недостатки таких строк — невозможно в строке использовать байт `0`, т. е. не подходит для бинарных данных, The disadvantages of such strings are that it is impossible to use the byte `0` in the string, i.e. it is not suitable for binary data,
непонятна стратегия управления/владения ресурсами, ну и основной недостаток — длину строки приходится вычислять каждый раз, the resource management/ownership strategy is unclear, and the main disadvantage is that the length of the string has to be calculated each time,
перебирая все её символы. iterating over all its characters.
Откуда ноги растут у такого решения вполне понятно — со времён динозавров: как динозавры были большие, с маленьким мозгом и The origin of this solution is quite clear from the time of the dinosaurs: just as dinosaurs were large, with small brains and
короткими ручками, так и компьютеры были большие, память у них была маленькая, а строки короткими. Сэкономить память на хранении длины строки было важнее, чем потерять время на повторный подсчет длины. short arms, so computers were large, memory was small, and strings were short. Saving memory on storing the length of a string was more important than losing time on repeatedly calculating the length.
Первые попытки стандартизировать строки как класс начались только в С++98 - std::string появился, как часть STL, и как The first attempts to standardize strings as a class began only in C++98 - std::string appeared as part of STL, and like
многое из STL, крайне неоднозначно воспринимался программистами. much of STL, it was extremely ambiguously perceived by programmers.
И первое, что приходит в голову при улучшении C-строк — надо хранить длину строки: And the first thing that comes to mind when improving C-strings is that you need to store the length of the string:
```cpp ```cpp
struct simple_string { struct simple_string {
    const char* data;     const char* data;
@ -32,92 +33,92 @@
}; };
``` ```
При наличии такой строки, уже множество алгоритмов значительно оптимизируются. With such a string, many algorithms are significantly optimized.
Например, при сравнении двух строк на равенство мы можем даже не начинать сравнивать их символы, если длины строк не равны. For example, when comparing two strings for equality, we may not even start comparing their characters if the lengths of the strings are not equal.
Более того, этих данных абсолютно достаточно для всех методов, которые не модифицируют строку. Moreover, this data is absolutely sufficient for all methods that do not modify the string.
Также заметим, что такой объект на современных 64-битных архитектурах прекрасно передается в функции по значению — Also note that such an object on modern 64-bit architectures is perfectly passed to functions by value
оба его поля укладываются в регистры (ну, кроме windows), что облегчает работу оптимизатору компилятора. both of its fields fit into registers (well, except for Windows), which makes it easier for the compiler optimizer to work.
Между тем, такое решение попало в стандарт только аж в С++17, в виде `std::string_view`. Meanwhile, such a solution only made it into the standard in C++17, in the form of `std::string_view`.
Видимо, только тогда до комитета смогли донести мысль, что строки строкам рознь, и использовать только один универсальный объект Apparently, only then could the committee be convinced that strings are different, and using only one universal object
для строк — по меньшей мере может приводить к уменьшению производительности, а также нарушает принцип «не плати за то, for strings at the very least, can lead to a decrease in performance, and also violates the principle of "don't pay for what you
чем не пользуешься». Почему же «строки строкам рознь» и почему нам мало одного типа для строки, рассмотрим как раз далее. don't use". Why are "strings different" and why is one type of string not enough for us, we will consider just below.
### Ресурсы ### Resources
И следующий вопрос, возникающий со строками — это владение ресурсами. And the next question that arises with strings is resource ownership.
Практически каждый крупный фреймворк решал эту задачу самостоятельно, изобретая свои велосипеды. Almost every major framework solved this problem on its own, inventing its own bicycles.
У нас есть `std::string`, в QT у нас `QString`, в MFC - `CString`, в ATL - `CAtlString`, свои строки есть в Folly, We have `std::string`, in QT we have `QString`, in MFC - `CString`, in ATL - `CAtlString`, there are own strings in Folly,
в общем, “тысячи их”, любой игровой движок начинают с того, чтобы написать свои строки. in general, "thousands of them", any game engine starts with writing its own strings.
Многие из этих реализаций в аспекте управления ресурсами для улучшения производительности использовали подход Many of these implementations in the aspect of resource management used the approach to improve performance
**COW** “Copy On Write”. При этом объект строки ссылался на некий разделяемый между несколькими объектами буфер с символами **COW** “Copy On Write”. In this case, the string object referred to a buffer shared between several objects with the characters
строки и счётчиком ссылок на этот буфер, что позволяло быстро создавать копию строки, а реально копировать символы только of the string and a reference counter to this buffer, which allowed you to quickly create a copy of the string, and actually copy the characters only
при её модификации. when it is modified.
Но все они совпадали в одном — строка всегда предполагалась мутабельной, то есть что мы можем модифицировать символы в буфере строки. But they all coincided in one thing the string was always assumed to be mutable, that is, we can modify the characters in the string buffer.
### Мутабельность / иммутабельность ### Mutability / immutability
Из-за этого подход **COW** умер к С++11: при каждой операции, могущей модифицировать символы строки приходилось проверять, Because of this, the **COW** approach died by C++11: for each operation that could modify the characters of the string, it was necessary to check
не ссылаемся ли мы на разделяемый буфер и если да, то копировать символы в другой буфер. whether we are referring to a shared buffer, and if so, copy the characters to another buffer.
В многопоточной же среде потом ещё и проверять, не надо ли теперь освобождать старый буфер, и естественно всё это обмазавшись In a multithreaded environment, you also need to check whether you now need to free the old buffer, and of course, all this is smeared with
локами или атомиками, что тоже не бесплатно. locks or atomics, which is also not free.
Поэтому, начиная с С++11 `std::string` не использует **COW**, и каждое копирование объекта строки приводит и к копированию Therefore, starting with C++11, `std::string` does not use **COW**, and each copying of a string object also leads to copying
всех символов строки в другой буфер. all the characters of the string to another buffer.
Естественно, что каждый новый буфер требует аллокации памяти, что пытаются немного оптимизировать за счёт **SSO** Naturally, each new buffer requires memory allocation, which they are trying to slightly optimize through **SSO**
“Small String Optimization”, когда объект строки содержит внутри себя небольшой буфер и символы коротких строк “Small String Optimization”, when the string object contains a small buffer inside itself and the characters of short strings
располагаются прямо в нём. are located directly in it.
Но это уже зависит от реализации: в одних библиотеках помещают в объект строки до 15 байт, в некоторых до 23. But this already depends on the implementation: in some libraries they place up to 15 bytes in the string object, in some up to 23.
Однако эта оптимизация тоже палка о двух концах, и может в различных реализациях усложнить перемещение строки - если она хранит However, this optimization is also a double-edged sword, and in various implementations it can complicate the movement of a string - if it stores
указатель на свой внутренний буфер, его придётся корректировать. a pointer to its internal buffer, it will have to be adjusted.
А без COW мутабельность строк приводит к тому, что любая инициализация объекта строки приводит к копированию байтов. And without COW, the mutability of strings leads to the fact that any initialization of a string object leads to copying bytes.
Посмотрим такой код: Let's look at this code:
```cpp ```cpp
const char* text1 = "Hello, World";         // ничего не стоит const char* text1 = "Hello, World";         // costs nothing
std::string_view text2 = "Hello, World";    // Ничего не стоит, вычисляет длину строки при компиляции std::string_view text2 = "Hello, World";    // Costs nothing, calculates the length of the string at compile time
std::string text3 = "Hello, World";         // В рантайме каждый раз копирует символы строки std::string text3 = "Hello, World";         // Copies the characters of the string every time at runtime
``` ```
(Удостоверится в правдивости комментариев можно на https://godbolt.org/z/51oKGWT5T ) (You can verify the truth of the comments at https://godbolt.org/z/51oKGWT5T )
Но если нам дальше по коду не нужно никак модифицировать строку, мы зря платим за аллокацию, копирование символов, But if we dont need to modify the string in any way further in the code, we are wasting money on allocation, copying characters,
а также за деструктор строки. То есть хотелось бы иметь как минимум два варианта строк — мутабельные и иммутабельные, as well as on the string destructor. That is, I would like to have at least two versions of strings mutable and immutable,
чтобы явно дать понять компилятору, что мы не собираемся модифицировать строку. to explicitly make it clear to the compiler that we are not going to modify the string.
Или банальный пример — мы парсим какой-то входящий буфер данных, нам нужно проверить, равен ли некий кусок буфера строке Or a banal example we are parsing some incoming data buffer, we need to check whether a certain piece of the buffer is equal to the string
”hello” на «чистом С++», т. е. без всяких memcmp и strcmp. До появления string_view приходилось делать примерно так: "hello" in "pure C++", i.e. without any memcmp and strcmp. Before the advent of string_view, it had to be done something like this:
```cpp ```cpp
bool is_part_buffer_equal_hello(const char* data, int start, int end) { bool is_part_buffer_equal_hello(const char* data, int start, int end) {
    return std::string(data + start, end - start) == "hello";     return std::string(data + start, end - start) == "hello";
} }
``` ```
Тут получается, сначала копируются символы из буфера data в буфер временной строки, возможно с аллокацией памяти, и лишь Here it turns out that first the characters from the data buffer are copied to the buffer of the temporary string, possibly with memory allocation, and only
потом временная строка сравнивается с ”hello”, а потом ещё и деструктор и раскрутка стека на случай исключения. then the temporary string is compared with "hello", and then also the destructor and stack unwinding in case of an exception.
При использовании же вместо `std::string` `std::string_view` код на C++ почти не меняется: When using `std::string_view` instead of `std::string` the code in C++ almost does not change:
```cpp ```cpp
bool is_part_buffer_equal_hello_view(const char* data, int start, int end) { bool is_part_buffer_equal_hello_view(const char* data, int start, int end) {
    return std::string_view(data + start, end - start) == "hello";     return std::string_view(data + start, end - start) == "hello";
} }
``` ```
Однако генерируемый машинный код значительно преобразуется, достигая уровня ручного С-кода — там просто сравнивается, However, the generated machine code is significantly transformed, reaching the level of manual C-code there it is simply compared
что end start == 5 и дальше кусок начального буфера сравнивается через memcmp со строкой ”hello” that end start == 5 and then a piece of the initial buffer is compared via memcmp with the string "hello"
(при -O2 c константами 1819043176 (hell) и 111 (o)). (with -O2 with constants 1819043176 ('hell') and 111 ('o')).
Ни создания временного объекта, ни копирования байтов, ни деструктора, ни раскрутки стека для исключений. No creation of a temporary object, no copying of bytes, no destructor, no stack unwinding for exceptions.
Убедится можно на https://godbolt.org/z/9fo188e7c You can verify this at https://godbolt.org/z/9fo188e7c
Казалось бы, ну вот же в С++17 появился `string_view`, пожалуйста, используй его в параметрах своих функций вместо `const std::string&`, It would seem, well, `string_view` appeared in C++17, please, use it in the parameters of your functions instead of `const std::string&`,
и будет счастье. Но тут тоже есть нюанс — всё отлично работает, пока нам не нужно передать строку в стороннее C-API: string_view не даёт and there will be happiness. But there is also a nuance here everything works fine, as long as we dont need to pass the string to a third-party C-API: string_view does not give
гарантий нуль-терминированности строки, поэтому его data() нельзя передать в стороннее C-API, и потому всё-равно придётся сначала guarantees of null-termination of the string, therefore its data() cannot be passed to a third-party C-API, and therefore you will still have to
скопировать его в `std::string`. А раз нужен `std::string`, то и параметром функции оптимальнее cделать `const std::string&` copy it to `std::string` first. And since `std::string` is needed, then it is more optimal to make `const std::string&` the parameter of the function
и далее по цепочке, все параметры вновь станут `const std::string&`. and further down the chain, all parameters will again become `const std::string&`.
### Конкатенация строк ### String concatenation
Далее, после инициализации строки, самая частая мутабельная операция с ними, скорее всего конкатенация строк, либо в виде просто Next, after initializing a string, the most frequent mutable operation with them is most likely string concatenation, either in the form of simply
сложения строк, либо добавления строки к строке. И именно она легко может вызывать как неоптимальную производительность при неграмотном adding strings, or adding a string to a string. And it is she who can easily cause both suboptimal performance with illiterate
использовании, так и оверхед по памяти, даже при грамотном использовании. use, and memory overhead, even with competent use.
Рассмотрим простой код ( https://godbolt.org/z/odx7W1Pv7 ) Consider a simple code ( https://godbolt.org/z/odx7W1Pv7 )
```cpp ```cpp
#include <string> #include <string>
void some_outer_function(const std::string&); void some_outer_function(const std::string&);
@ -127,9 +128,9 @@
    some_outer_function(concat);     some_outer_function(concat);
} }
``` ```
Как видим, и в clang, и в GCC создается несколько временных объектов, в которые последовательно перекладываются символы строк, As we can see, both in clang and in GCC, several temporary objects are created, into which the characters of the strings are sequentially shifted,
и как результат — мы получаем несколько лишних аллокаций для промежуточных буферов, символы из строк копируются несколько лишних and as a result we get several extra allocations for intermediate buffers, the characters from the strings are copied several extra
раз из промежуточных буферов. В идеале для лучшей производительности такой код нужно переписать так: times from intermediate buffers. Ideally, for better performance, this code needs to be rewritten like this:
```cpp ```cpp
#include <string> #include <string>
void some_outer_function(const std::string&); void some_outer_function(const std::string&);
@ -145,180 +146,180 @@
} }
``` ```
К сожалению, пока ни один компилятор не оптимизирует первый простой код до уровня второго более оптимального кода, а писать Unfortunately, so far no compiler optimizes the first simple code to the level of the second more optimal code, and writing
такой код каждый раз руками довольно неудобно. То есть опять приходится платить за то, чем не пользуешься. such code every time by hand is quite inconvenient. That is, again you have to pay for what you dont use.
Да и в этом случае вполне может возникнуть оверхед по памяти — операции добавления строки обычно во всех реализациях увеличивают And in this case, memory overhead may well occur string addition operations usually increase
размер буфера строки не меньше, чем в два раза, считая, что скоро к строке могут снова что-нибудь добавить. the size of the string buffer in all implementations by at least two times, assuming that something may soon be added to the string again.
Поэтому если строку не планируется более модифицировать, но время её жизни ещё не подошло к концу (например, это поле Therefore, if the string is no longer planned to be modified, but its lifetime has not yet come to an end (for example, this is a field
какого-либо класса), нужно ещё не забыть сделать на ней `shrink_to_fit`. of some class), you should not forget to do `shrink_to_fit` on it.
Между тем, часто основной сценарий использования строк — это как раз некая подготовка строки путём нескольких модификаций и конкатенаций, Meanwhile, often the main scenario for using strings is just some preparation of the string by several modifications and concatenations,
а затем она где-то хранится, более не меняясь. При этом программист обычно знает, примерно какой размер строк ожидается в этом месте, and then it is stored somewhere, no longer changing. In this case, the programmer usually knows approximately what size of strings is expected in this place,
и мог бы выделить буфер для этих промежуточных модификаций прямо на стеке, прибегая к динамической аллокации только при превышении and could allocate a buffer for these intermediate modifications directly on the stack, resorting to dynamic allocation only when exceeding
размера этого буфера. Однако с текущей реализацией строк это сделать довольно проблематично, либо неудобно. the size of this buffer. However, with the current implementation of strings, this is quite problematic, or inconvenient.
Подытожим, что имеем на данный момент: Let's summarize what we have at the moment:
- «Из коробки» в С++ для работы со строками сейчас имеется `std::string`. - "Out of the box" in C++ for working with strings there is now `std::string`.
- Строки подразумеваются мутабельными, что приводит к обязательному копированию всех символов строки при инициализации и - Strings are assumed to be mutable, which leads to mandatory copying of all characters of the string during initialization and
копировании объектов строк. copying of string objects.
- Соответственно, не имеем возможности быстрого копирования строк, даже если не планируем потом менять копию. - Accordingly, we do not have the ability to quickly copy strings, even if we do not plan to change the copy later.
- Конкатенация нескольких строк — задача, могущая выполнятся неоптимально, приводить к оверхеду по памяти, написать оптимальный код сложно. - Concatenating several strings is a task that can be performed suboptimally, lead to memory overhead, and it is difficult to write optimal code.
- Есть костыль для иммутабельных строк в виде `std::string_view`, однако он не решает вопросы владения строкой, поэтому по сути - There is a crutch for immutable strings in the form of `std::string_view`, but it does not solve the issues of string ownership, so in fact
годится только как тип для передачи параметров в функции, не меняющие строки, с оговоркой, что не может использоваться в функциях, it is only suitable as a type for passing parameters to functions that do not change strings, with the caveat that it cannot be used in functions
вызывающих C-API, так как не даёт гарантий нуль-терминированности. calling C-API, since it does not guarantee null-termination.
- Ну и к `std::string` есть вопросы, что несмотря на то, что это класс для строк, собственно для работы со строками в нём крайне куцый - Well, and there are questions to `std::string` that despite the fact that this is a class for strings, in fact, for working with strings it has an extremely meager
функционал по сравнению с тем, к чему привыкли в других языках — к примеру нет замены подстрок по шаблону (в других языках это обычно functionality compared to what they are used to in other languages for example, there is no replacement of substrings by a pattern (in other languages this is usually
replace, но в С++ эта функция делает совершенно другое), trim, split, join, upper, lower и т. п. replace, but in C++ this function does something completely different), trim, split, join, upper, lower, etc.
Эти функции приходится каждый раз писать самому, и не факт, что у всех это получится оптимально. These functions have to be written by yourself every time, and it is not a fact that everyone will be able to do this optimally.
Надеюсь, после этого небольшого вступления вам будет более понятно, какие проблемы я решал своей строковой библиотекой и каким образом. I hope that after this small introduction you will better understand what problems I solved with my string library and how.
## Библиотека simstr ## Simstr library
Собственно, нельзя сказать, что «я свелосипедил свою реализацию класса для строк». Actually, you can't say that "I reinvented my implementation of the class for strings."
Как я ранее показал, сложно, а то и даже невозможно написать один единый строковый класс, хорошо подходящий для всех сценариев As I showed earlier, it is difficult, or even impossible, to write one single string class that is well suited for all scenarios
использования. Именно поэтому у меня не строковый класс, а строковая библиотека, которая содержит несколько разных строковых типов, of use. That is why I dont have a string class, but a string library, which contains several different string types,
от более простых к более сложным, каждый из которых имеет свои сильные и слабые стороны, и пользователю нужно грамотно подходить к from simpler to more complex, each of which has its own strengths and weaknesses, and the user needs to competently approach the
вопросу, какой из этих классов в каком случае стоит использовать. question of which of these classes should be used in which case.
Саму библиотеку я начал потихоньку разрабатывать ещё в 2011-2012 годах, когда у нас уже появилась семантика перемещения, но ещё I started developing the library itself little by little back in 2011-2012, when we already had move semantics, but not yet
не было std::string_view. Однако сейчас минимальная версия стандарта для работы библиотеки: **C++20** используются концепты и \<format\>. there was std::string_view. However, now the minimum standard version for the library to work is: **C++20** concepts and \<format\> are used.
Сначала я расскажу о классах библиотеки для самих строк, а потом о том, как в ней оптимально решается задача конкатенации строк. First, I will talk about the library classes for the strings themselves, and then about how the string concatenation problem is optimally solved in it.
Несколько общих моментов: Several general points:
- Все классы для работы со строками шаблонизированы типом символов, но подразумевается, что символы могут быть char, char16_t, - All classes for working with strings are templated by the type of characters, but it is assumed that the characters can be char, char16_t,
char32_t, wchar_t. char32_t, wchar_t.
- Все строки имеют явную длину. - All strings have an explicit length.
- Классы владельцы строк хранят их с завершающим нулем в конце, который не входит в длину строки. - The string owner classes store them with a trailing zero at the end, which is not included in the length of the string.
- В самой строке могут содержаться нулевые символы, все алгоритмы работают только через длину строки, не обращая на них внимания. - The string itself can contain zero characters, all algorithms work only through the length of the string, without paying attention to them.
- Классы владельцы строк могут инициализироваться строками другого типа символов, выполняя конвертацию между UTF-8, UTF-16, UTF-32. - The string owner classes can be initialized with strings of another character type, performing conversion between UTF-8, UTF-16, UTF-32.
- Для смены регистра символов и сравнения строк без учёта регистра используются встроенные таблицы для первой плоскости юникода - Built-in tables for the first plane of Unicode are used to change the case of characters and compare strings case-insensitively
(до 0xFFFF). Строки считаются представленными в кодировке UTF-8, UTF-16, UTF-32 соответственно. (up to 0xFFFF). Strings are considered to be represented in UTF-8, UTF-16, UTF-32 encoding, respectively.
Однако не делается нормализация строк и не обрабатываются ситуации, когда смена регистра символа приводит к изменению их количества. However, string normalization is not done and situations where changing the case of a character leads to a change in their number are not handled.
То есть преобразование регистра символов соответствует `std::towupper`, `std::towlower` для unicode локали, That is, the case conversion of characters corresponds to `std::towupper`, `std::towlower` for the unicode locale,
только быстрее и может работать с любым видом символов. only faster and can work with any type of characters.
Если вам нужна строгая работа с юникодом, используйте другие средства, например ICU. If you need strict work with unicode, use other tools, such as ICU.
### Классы строк. ### String classes.
#### Первый самый простой класс строки называется, естественно, `simple_str` :) #### The first simplest string class is called, of course, `simple_str` :)
(simstr::simple_str) (simstr::simple_str)
Класс просто представляет собой указатель на начало константной строки и её длину, по сути то же самое, что `std::string_view`. The class simply represents a pointer to the beginning of a constant string and its length, in fact the same as `std::string_view`.
Предназначен для работы с иммутабельными строками, не владеющий ими, то есть вы должны сами озаботиться тем, что реальная строка, It is intended for working with immutable strings, not owning them, that is, you must take care that the real string,
представленная через `simple_str` жива во время его использования. represented through `simple_str` is alive during its use.
Реализует все строковые методы, не модифицирующие строку. Implements all string methods that do not modify the string.
Алиасы: Aliases:
- `ssa` для simple_str\<char\> - `ssa` for simple_str\<char\>
- `ssu` для simple_str\<char16_t\> - `ssu` for simple_str\<char16_t\>
- `ssw` для simple_str\<wchar_t> - `ssw` for simple_str\<wchar_t>
- `ssuu` для simple_str\<char32_t\> - `ssuu` for simple_str\<char32_t\>
Применяется в основном для передачи строк как параметр функций, не модифицирующих переданную строку, вместо `const std::string&`, It is used mainly for passing strings as a parameter to functions that do not modify the passed string, instead of `const std::string&`,
а также для локальных переменных при работе с частями строк. as well as for local variables when working with parts of strings.
#### Второй класс — `simple_str_nt` #### The second class is `simple_str_nt`
(simstr::simple_str_nt) (simstr::simple_str_nt)
По устройству и назначению совпадает с `simple_str`, но дает гарантии нуль-терминированности строки. In terms of structure and purpose, it coincides with `simple_str`, but guarantees null-termination of the string.
То есть если функции надо переданный параметр без изменений передать дальше как C-строку в какое то API, она должна использовать для That is, if the function needs to pass the passed parameter further as a C-string to some API without changes, it should use the
параметра тип `simple_str_nt`. `simple_str_nt` type for the parameter.
Все классы владеющих строк (simstr::sstring, simstr::lstring) могут быть преобразованы в `simple_str_nt`, так как хранят строки с завершающим нулём. All classes of owning strings (simstr::sstring, simstr::lstring) can be converted to `simple_str_nt`, since they store strings with a trailing zero.
Это позволяет писать функции с единым типом параметра, принимающим на вход любой тип владеющих строковых объектов. This allows you to write functions with a single parameter type that accepts any type of owning string objects as input.
Алиасы: Aliases:
- `stra` для simple_str_nt\<char> - `stra` for simple_str_nt\<char>
- `stru` для simple_str_nt\<char16_t> - `stru` for simple_str_nt\<char16_t>
- `strw` для simple_str_nt\<wchar_t> - `strw` for simple_str_nt\<wchar_t>
- `struu` для simple_str_nt\<char32_t> - `struu` for simple_str_nt\<char32_t>
Может инициализироваться строковыми литералами: Can be initialized with string literals:
```cpp ```cpp
stra text = "Text"; stra text = "Text";
``` ```
Длина в этом случае вычисляется сразу при компиляции. Аналогично `simple_str_nt` создается с помощью `operator""_ss`: In this case, the length is calculated immediately at compile time. Similarly, `simple_str_nt` is created using `operator""_ss`:
```cpp ```cpp
stringa result = "Count: "_ss + count; stringa result = "Count: "_ss + count;
``` ```
#### Класс sstring (shared string). #### Sstring class (shared string).
(simstr::sstring) (simstr::sstring)
Класс, умеющий хранить иммутабельную строку. A class that can store an immutable string.
То есть ему можно присвоить некую строку только целиком, модифицировать символы строки нельзя. That is, you can only assign a string to it entirely, you cannot modify the characters of the string.
Владеет строкой, управляет памятью для символов строки. Owns the string, manages the memory for the characters of the string.
Хранит со строками завершающий нуль, и может быть источником для `simple_str_nt`, для передачи в C-API. Stores a trailing zero with the strings, and can be a source for `simple_str_nt`, for passing to C-API.
Так же, как и `simple_str`, реализует все методы, не модифицирующие строку. Like `simple_str`, it implements all methods that do not modify the string.
Алиасы: Aliases:
- `stringa` для sstring\<char> - `stringa` for sstring\<char>
- `stringu` для sstring\<char16_t> - `stringu` for sstring\<char16_t>
- `stringw` для sstring\<wchar_t> - `stringw` for sstring\<wchar_t>
- `stringuu` для sstring\<char32_t> - `stringuu` for sstring\<char32_t>
То, что хранимая строка иммутабельна, позволяет применить ряд оптимизаций: The fact that the stored string is immutable allows you to apply a number of optimizations:
- Для строк, не подходящих для SSO, использует общий разделяемый буфер с атомарным счётчиком ссылок. - For strings that are not suitable for SSO, it uses a common shared buffer with an atomic reference counter.
Позволяет быстро копировать строку без необходимости блокировок доступа к содержимому буфера. Allows you to quickly copy a string without the need to block access to the contents of the buffer.
- Нет необходимости хранить размер буфера (capacity) — всё равно мы ничего не дописываем в буфер. - There is no need to store the buffer size (capacity) we are not adding anything to the buffer anyway.
- Позволяет просто ссылаться на литералы программы, не копируя их символы в какой-либо буфер: - Allows you to simply refer to program literals without copying their characters to any buffer:
```cpp ```cpp
stringa str = "Hello!"; // Ничего не стоит, не копирует байты строки stringa str = "Hello!"; // Costs nothing, does not copy the bytes of the string
stringa ltr = stra{"Hello!"}; // А вот тут копирует байты строки в ltr stringa ltr = stra{"Hello!"}; // But here it copies the bytes of the string to ltr
``` ```
Также в классе применяется **SSO** Small String Optimization. The class also uses **SSO** Small String Optimization.
Короткие строки помещаются внутри самого объекта во внутренний буфер. Short strings are placed inside the object itself in an internal buffer.
Размеры: Sizes:
Для 64 бит: For 64 bits:
- `stringa` класс 24 байта, SSO до 23 символов. - `stringa` class 24 bytes, SSO up to 23 characters.
- `stringu` класс 32 байта, SSO до 15 символов. - `stringu` class 32 bytes, SSO up to 15 characters.
- `stringuu` класс 32 байта, SSO до 7 символов. - `stringuu` class 32 bytes, SSO up to 7 characters.
Для 32 бит: For 32 bits:
- `stringa` класс 16 байт, SSO до 15 символов. - `stringa` class 16 bytes, SSO up to 15 characters.
- `stringu` класс 24 байта, SSO до 11 символов. - `stringu` class 24 bytes, SSO up to 11 characters.
- `stringuu` класс 24 байта, SSO до 5 символов. - `stringuu` class 24 bytes, SSO up to 5 characters.
#### Класс lstring<K, N, forShared> (local string) #### Class lstring<K, N, forShared> (local string)
(simstr::lstring) (simstr::lstring)
Класс, хранящий строку и позволяющий её модифицировать. A class that stores a string and allows it to be modified.
Владеет строкой, управляет памятью для символов строки. Owns the string, manages the memory for the characters of the string.
Хранит со строками завершающий нуль, и может быть источником для `simple_str_nt`, для передачи в C-API. Stores a trailing zero with the strings, and can be a source for `simple_str_nt`, for passing to C-API.
Как и все остальные классы, реализует все методы, не модифицирующие строку. Like all other classes, it implements all methods that do not modify the string.
В качестве `N` в параметре шаблона задаётся размер внутреннего буфера для хранения символов. The size of the internal buffer for storing characters is specified as `N` in the template parameter.
Строки длиной до N символов хранятся внутри объекта, а при превышении этого количества — аллоцируется динамический буфер, Strings up to N characters long are stored inside the object, and when this number is exceeded, a dynamic buffer is allocated,
в который сохраняются символы. При копировании объекта все символы также всегда копируются. in which the characters are saved. When copying an object, all characters are also always copied.
Если `forShare` == true и символы не помещаются в локальный буфер, то динамический буфер создается с дополнительным местом, If `forShare` == true and the characters do not fit into the local buffer, then a dynamic buffer is created with additional space,
так чтобы совпадать по структуре с буфером `sstring`. Тогда при перемещении `lstring` в `sstring` переместится только указатель so that it matches the structure of the `sstring` buffer. Then, when moving `lstring` to `sstring` only the pointer will move
на буфер, без излишнего копирования символов. to the buffer, without unnecessary copying of characters.
Этот класс удобен для работы со строками как локальная переменная на стеке. This class is convenient for working with strings as a local variable on the stack.
Обычно мы предполагаем примерный размер строк, с котороми будем работать, и можем создать локальную строку с буфером на стеке, Usually we assume the approximate size of the strings we will be working with, and we can create a local string with a buffer on the stack,
и работать с ней. При этом не опасаясь переполнения буфера, так как в этом случае строка переключится на динамический буфер. and work with it. At the same time, without fear of buffer overflow, since in this case the string will switch to a dynamic buffer.
Алиасы: Aliases:
- `lstringa<N=16>` для lsrting\<char, N, false> - `lstringa<N=16>` for lsrting\<char, N, false>
- `lstringu<N=16>` для lsrting\<char16_t, N, false> - `lstringu<N=16>` for lsrting\<char16_t, N, false>
- `lstringw<N=16>` для lsrting\<wchar_t, N, false> - `lstringw<N=16>` for lsrting\<wchar_t, N, false>
- `lstringuu<N=16>` для lsrting\<char32_t, N, false> - `lstringuu<N=16>` for lsrting\<char32_t, N, false>
- `lstringsa<N=16>` для lsrting\<char, N, true> - `lstringsa<N=16>` for lsrting\<char, N, true>
- `lstringsu<N=16>` для lsrting\<char16_t, N, true> - `lstringsu<N=16>` for lsrting\<char16_t, N, true>
- `lstringsw<N=16>` для lsrting\<wchar_t, N, true> - `lstringsw<N=16>` for lsrting\<wchar_t, N, true>
- `lstringsuu<N=16>` для lsrting\<char32_t, N, true> - `lstringsuu<N=16>` for lsrting\<char32_t, N, true>
Небольшой пример использования с пояснениями: A small example of use with explanations:
```cpp ```cpp
#ifdef _WIN32 #ifdef _WIN32
const char path_separator = '\\'; const char path_separator = '\\';
@ -329,13 +330,13 @@
auto get_current_dir() { auto get_current_dir() {
#ifdef _WIN32 #ifdef _WIN32
    /* заполняем буфер wchar_t строки lstringw<MAX_PATH> из GetCurrentDirectoryW с возможным     /* fills the buffer of the wchar_t string lstringw<MAX_PATH> from GetCurrentDirectoryW with possible
увеличением буфера и конвертируем в ut8 char. В конструкторе используется то, что появилось increasing the buffer and converting to ut8 char. The constructor uses what appeared
только в С++23 как `resize_and_overwrite`, а у нас было изначально :) */ only in C++23 as `resize_and_overwrite`, and we had it originally :) */
    lstringa<MAX_PATH> path{lstringw<MAX_PATH>{ [](auto p, auto s) { return GetCurrentDirectoryW(DWORD(s + 1), p); }}};     lstringa<MAX_PATH> path{lstringw<MAX_PATH>{ [](auto p, auto s) { return GetCurrentDirectoryW(DWORD(s + 1), p); }}};
    /* Эта одна строчка делает примерно то же самое, что и вот такой код.     /* This one line does approximately the same thing as this code.
typedef struct lstringa_MAX_PATH_t { typedef struct lstringa_MAX_PATH_t {
char* data; char* data;
size_t length; size_t length;
@ -347,11 +348,11 @@
    wchar_t buffer[MAX_PATH + 1], *buf = buffer;     wchar_t buffer[MAX_PATH + 1], *buf = buffer;
    DWORD size = sizeof(buffer) / sizeof(wchar_t), lengthOfpath;     DWORD size = sizeof(buffer) / sizeof(wchar_t), lengthOfpath;
    for (;;) {     for (;;) {
// Возвращает либо количество скопированных символов без учёта завершающего нуля, // Returns either the number of copied characters without taking into account the trailing zero,
// либо если буфер мал, то нужный размер буфера вместе с завершающим нулём // or if the buffer is small, then the required buffer size along with the trailing zero
        DWORD ret = GetCurrentDirectoryW(size, buf);         DWORD ret = GetCurrentDirectoryW(size, buf);
        if (ret < size) {         if (ret < size) {
// Влезло в буфер, хотя в Windows пути могут быть и длиннее, чем MAX_PATH, если начинаются с \\?\ // Fits into the buffer, although in Windows paths can be longer than MAX_PATH if they start with \\?\
// https://learn.microsoft.com/ru-ru/windows/win32/fileio/maximum-file-path-limitation?tabs=registry // https://learn.microsoft.com/ru-ru/windows/win32/fileio/maximum-file-path-limitation?tabs=registry
            lenOfpath = ret;             lenOfpath = ret;
            break;             break;
@ -371,14 +372,14 @@
    lstringa<MAX_PATH> path{ [](char* p, size_t s) {     lstringa<MAX_PATH> path{ [](char* p, size_t s) {
        const char* res = getcwd(p, s + 1);         const char* res = getcwd(p, s + 1);
        if (res) {         if (res) {
            return stra{res}.length(); // Возвращаем длину строки             return stra{res}.length(); // Returns the length of the string
        }         }
        if (errno == ERANGE) // Не влезло в буфер, попробуем в два раза больше         if (errno == ERANGE) // Did not fit into the buffer, let's try twice as much
            return s * 2;             return s * 2;
        return 0ul;         return 0ul;
    }};     }};
#endif #endif
// Удостоверимся, что строка будет заканчиваться разделителем директорий // Let's make sure that the string will end with a directory separator
    if (!path.length() || path.at(-1) != path_separator) {     if (!path.length() || path.at(-1) != path_separator) {
        path += e_c(1, path_separator);         path += e_c(1, path_separator);
    }     }
@ -388,37 +389,37 @@
stringa build_full_path(ssa fileName) { stringa build_full_path(ssa fileName) {
    return get_current_dir() + fileName + ".txt";     return get_current_dir() + fileName + ".txt";
    /*     /*
    Здесь сначала на стеке создастся временный объект lstringa<MAX_PATH> для вызова get_current_dir.     Here, a temporary lstringa<MAX_PATH> object will first be created on the stack to call get_current_dir.
    Функция get_current_dir заполнит его названием текущего каталога.     The get_current_dir function will fill it with the name of the current directory.
    В 99.9% случаев для этого хватит локального буфера на стеке.     In 99.9% of cases, the local buffer on the stack will be enough for this.
    После рассчитывается общая длина для результата: длина current_dir + длина fileName + 4.     After that, the total length for the result is calculated: the length of current_dir + the length of fileName + 4.
    Определяется буфер для строки конечного результата - если длина меньше 24 — строка будет размещена прямо в stringa,     The buffer for the string of the final result is determined - if the length is less than 24 - the string will be placed directly in stringa,
    иначе аллоцируется буфер для результирующей строки сразу нужного размера.     otherwise a buffer for the resulting string is allocated immediately of the required size.
    Затем в буфер результирующей строки последовательно копируются символы из current_dir, file_name, ".txt";     Then the characters from current_dir, file_name, ".txt" are sequentially copied to the buffer of the resulting string;
    Ну и благодаря RVO - место для самого результата (stringa) - отводится в вызывающей функции,     Well, thanks to RVO - the place for the result itself (stringa) - is allocated in the calling function,
    то есть никакого дополнительного копирования при возврате не будет.     that is, there will be no additional copying upon return.
    Таким образом, будет максимум всего две аллокации памяти (если current_dir не влезет в MAX_PATH),     Thus, there will be a maximum of only two memory allocations (if current_dir does not fit into MAX_PATH),
или одна, если результирующая строка длиннее 23 символов, при этом эта аллокация будет сразу нужного размера. or one, if the resulting string is longer than 23 characters, while this allocation will be immediately of the required size.
    */     */
} }
``` ```
В этом примере вы наверняка заметили, как конкатенируются строки и задались вопросом — как же при двух сложениях считалась In this example, you probably noticed how strings are concatenated and wondered how was the
длина всего результата, чтобы выделить необходимое место сразу за один раз, без промежуточных буферов? length of the entire result calculated with two additions in order to allocate the necessary space at once, without intermediate buffers?
Ответ на этот вопрос: The answer to this question:
### Строковые выражения ### String Expressions
Дело в том, что в библиотеке нет сложения строковых объектов как такового. Сложение выполняется для «строковых выражений». The fact is that there is no addition of string objects as such in the library. Addition is performed for "string expressions".
*Строковое выражение* — это любой объект произвольного типа, имеющий функции `length` и `place`. A *string expression* is any object of arbitrary type that has `length` and `place` functions.
Функция `length` возвращает длину строки, функция `place` помещает символы строки в переданный ей буфер. The `length` function returns the length of the string, and the `place` function places the characters of the string into the buffer passed to it.
Любая владеющая строка (simstr::sstring, simstr::lstring) может инициализироваться строковым выражением — она запрашивает у него длину, Any owning string (simstr::sstring, simstr::lstring) can be initialized with a string expression — it requests its length,
выделяет место для хранения символов, и передает это место строковому выражению, вызывая его функцию place. allocates space for storing characters, and passes this space to the string expression, calling its place function.
Для строковых выражений определена шаблонная функция сложения: A template addition function is defined for string expressions:
```cpp ```cpp
template<StrExpr A, StrExprForType<typename A::symb_type> B> template<StrExpr A, StrExprForType<typename A::symb_type> B>
inline auto operator + (const A& a, const B& b) { inline auto operator + (const A& a, const B& b) {
@ -426,10 +427,10 @@
} }
``` ```
`strexprjoin` шаблонный тип, который сам является строковым выражением. `strexprjoin` is a template type that is itself a string expression.
В себе он хранит ссылки на два переданных ему строковых выражения. It stores references to the two string expressions passed to it.
При запросе длины он выдает сумму длин двух строковых выражений, а при размещении символов — сначала размещает When the length is requested, it returns the sum of the lengths of the two string expressions, and when placing characters, it first places
в переданном буфере первое выражение, затем второе. the first expression in the passed buffer, then the second.
```cpp ```cpp
template<StrExpr A, StrExprForType<typename A::symb_type> B> template<StrExpr A, StrExprForType<typename A::symb_type> B>
struct strexprjoin { struct strexprjoin {
@ -441,86 +442,83 @@
    constexpr symb_type* place(symb_type* p) const noexcept { return b.place(a.place(p)); }     constexpr symb_type* place(symb_type* p) const noexcept { return b.place(a.place(p)); }
}; };
``` ```
Таким образом, операция сложения строковых выражений создает объект, также являющийся строковым выражением, Thus, the addition operation of string expressions creates an object that is also a string expression,
к которому также может быть применена следующая операция сложения, и который рекурсивно хранит ссылки на слагаемые части, to which the next addition operation can also be applied, and which recursively stores references to the component parts,
каждая из которых знает свой размер и умеет размещать себя в буфере результата. И так далее, к каждому получаемому each of which knows its size and knows how to place itself in the result buffer. And so on, to each resulting
строковому выражению можно снова применить `operator +`, формируя цепочку из нескольких строковых выражений, string expression, you can reapply `operator +`, forming a chain of several string expressions,
и в итоге "материализовать" последний получившийся объект, который сначала посчитает размер всей общей памяти для and eventually "materialize" the last resulting object, which first calculates the size of the entire total memory for
конечного результата, а затем разместит вложенные подвыражения в один буфер. the final result, and then places the nested subexpressions into one buffer.
Все строковые типы библиотеки сами являются строковыми выражениями, то есть могут служить слагаемыми в конкатенациях All string types in the library are themselves string expressions, that is, they can serve as terms in concatenations
строковых выражений. of string expressions.
Также `operator+` определён для строковых выражений и строковых литералов, строковых выражений и чисел (числа конвертируются Also, `operator+` is defined for string expressions and string literals, string expressions and numbers (numbers are converted
в десятичное представление), а также вы можете сами добавить желаемые типы. to decimal representation), and you can add the desired types yourself.
Пример: Example:
```cpp ```cpp
stringa text = header + " count=" + count + ", done"; stringa text = header + " count=" + count + ", done";
``` ```
Существует несколько типов строковых выражений "из коробки", для выполнения различных операций со строками: There are several types of string expressions "out of the box" for performing various operations on strings:
#### expr_spaces<ТипСимвола, КоличествоСимволов, Символ = ' '>{} #### expr_spaces<CharacterType, NumberOfCharacters, Symbol = ' '>{}
Выдает строку длиной КоличествоСимволов, заполненную заданным символом. Количество символов и символ - константы времени Returns a string of length NumberOfCharacters, filled with the specified character. The number of characters and the symbol are compile-time constants. For some cases, there is a shorthand notation:
компиляции. Для некоторых случаев есть сокращенная запись:
e_spca(КоличествоСимволов) - строка char пробелов e_spca(NumberOfCharacters) - string of char spaces
e_spcw(КоличествоСимволов) - строка w_char пробелов e_spcw(NumberOfCharacters) - string of w_char spaces
#### expr_pad<ТипСимвола>{КоличествоСимволов, Символ = ' '} #### expr_pad<CharacterType>{NumberOfCharacters, Symbol = ' '}
Выдает строку длиной КоличествоСимволов, заполненную заданным символом. Returns a string of length NumberOfCharacters, filled with the specified character.
Количество символов и символ могут задаваться в рантайме. Сокращенная запись: The number of characters and the symbol can be specified at runtime. Shorthand notation:
e_c(КоличествоСимволов, Символ) e_c(NumberOfCharacters, Symbol)
#### e_choice(bool Condition, StrExpr1, StrExpr2) #### e_choice(bool Condition, StrExpr1, StrExpr2)
Если Condition == true, результат будет равен StrExpr1, иначе StrExpr2. If Condition == true, the result will be StrExpr1, otherwise StrExpr2.
#### e_if(bool Condition, StrExpr1) #### e_if(bool Condition, StrExpr1)
Если Condition == true, результат будет равен StrExpr1, иначе пустая строка. If Condition == true, the result will be StrExpr1, otherwise an empty string.
#### expr_num<ТипСимвола>(ЦелоеЧисло) #### expr_num<CharacterType>(Integer)
Конвертирует число в десятичное представление. Редко используется, так как для строковых выражений и чисел Converts a number to decimal representation. Rarely used, since the "+" operator is overloaded for string expressions and numbers, and the number can simply be written as `text + number`;
переопределен оператор "+", и число можно просто написать как `text + number`;
#### expr_real<ТипСимвола>(ВещественноеЧисло) #### expr_real<CharacterType>(RealNumber)
конвертирует число в десятичное представление. Редко используется, так как для строковых выражений и чисел converts a number to decimal representation. Rarely used, since the "+" operator is overloaded for string expressions and numbers, and the number can simply be written as `text + number`;
переопределен оператор "+", и число можно просто написать как `text + number`;
#### e_join<bool ПослеПоследнего = false, bool ТолькоНеПустые = false>(контейнер, "Разделитель") #### e_join<bool AfterLast = false, bool OnlyNotEmpty = false>(container, "Separator")
Конкатенирует все строки в контейнере, используя разделитель. Если ПослеПоследнего == true, Concatenates all strings in the container, using a separator. If AfterLast == true,
то разделитель добавляется и после последнего элемента контейнера, иначе только между элементами. then the separator is added after the last element of the container as well, otherwise only between elements.
Если ТолькоНеПустые == true, то пустые строки пропускаются без добавления разделителя. If OnlyNotEmpty == true, then empty strings are skipped without adding a separator.
#### e_repl(ИсходнаяСтрока, "Искать", "Заменять") #### e_repl(OriginalString, "Search", "Replace")
Заменяет в исходной строке вхождения "Искать" на "Заменять". Replaces occurrences of "Search" in the original string with "Replace".
Шаблоны поиска и замены - строковые литералы времени компиляции. Search and replace patterns are compile-time string literals.
#### expr_replaced<ТипСимвола>{ИсходнаяСтрока, Искать, Заменять} #### expr_replaced<CharacterType>{OriginalString, Search, Replace}
Заменяет в исходной строке вхождения Искать на Заменять. Replaces occurrences of Search in the original string with Replace.
Шаблоны поиска и замены - могут быть любыми строковыми объектами в рантайме. Search and replace patterns can be any string objects at runtime.
#### empty_expr<ТипСимвола> #### empty_expr<CharacterType>
Выдает пустую строку. Сокращённая запись — eea, eeu, eew, eeuu. Применяется если формирование строки начинается с числа и строкового литерала: Returns an empty string. Abbreviated notation — eea, eeu, eew, eeuu. Used if the string formation starts with a number and a string literal:
```cpp ```cpp
str = eea + count + " times."; str = eea + count + " times.";
``` ```
так как оператор сложения определён только для сложения строкового выражения и числа. since the addition operator is only defined for adding a string expression and a number.
Также замечу, что существует `operator""_ss`, который превращает строковый литерал в объект `simple_str_nt`, который уже является строковым выражением: I also note that there is `operator""_ss`, which turns a string literal into a `simple_str_nt` object, which is already a string expression:
```cpp ```cpp
str = "Count = "_ss + count; str = "Count = "_ss + count;
... ...
str = count + " times."_ss; str = count + " times."_ss;
``` ```
#### Свои строковые выражения #### Your own string expressions
Вы можете сами создавать свои типы строковых выражений для оптимального формирования строк в нужных вам целях и алгоритмах. You can create your own string expression types to optimally form strings for your specific purposes and algorithms.
Для этого просто создайте тип с методами `length`, `place` и `typename symb_type`. To do this, simply create a type with `length`, `place` and `typename symb_type` methods.
Примеры создания и использования из реальных проектов: Examples of creation and use from real projects:
```cpp ```cpp
/* Сформировать строку в JSON формате, в 16 битных символах */ /* Form a string in JSON format, in 16-bit characters */
struct expr_json_str { struct expr_json_str {
    using symb_type = u16s;     using symb_type = u16s;
    ssu text;     ssu text;
@ -607,7 +605,7 @@ inline u16s* expr_json_str::place(u16s* ptr) const noexcept {
} }
``` ```
Использование: Usage:
```cpp ```cpp
........ ........
@ -617,9 +615,9 @@ vtText << uR"({"#type":"jxs:string","#value":")" + expr_json_str(name) + u"\"}";
....... .......
``` ```
Ещё пример Another example
```cpp ```cpp
/* Нужно сформировать бинарные данные в BASE64 формате, в 16 битных символах */ /* Need to form binary data in BASE64 format, in 16-bit characters */
struct expr_str_base64 { struct expr_str_base64 {
    using symb_type = u16s;     using symb_type = u16s;
    ssa text;     ssa text;
@ -660,7 +658,7 @@ inline u16s* expr_str_base64::place(u16s* ptr) const noexcept {
} }
``` ```
Использование: Usage:
```cpp ```cpp
...... ......
chunked_string_builder<u16s> vtText; chunked_string_builder<u16s> vtText;
@ -669,10 +667,10 @@ vtText << u"{\"#\",87126200-3e98-44e0-b931-ccb1d7edc497,{1,{#base64:" + expr_str
...... ......
``` ```
И ещё And more
```cpp ```cpp
/* Нужно преобразовать tm в строку даты/времени в 16-битных символах */ /* Need to convert tm to a date/time string in 16-bit characters */
struct expr_str_tm { struct expr_str_tm {
    using symb_type = u16s;     using symb_type = u16s;
    const tm& t;     const tm& t;
@ -685,11 +683,11 @@ struct expr_str_tm {
inline u16s* expr_str_tm::place(u16s* ptr) const noexcept { inline u16s* expr_str_tm::place(u16s* ptr) const noexcept {
    if constexpr (sizeof(wchar_t) == 2) {     if constexpr (sizeof(wchar_t) == 2) {
        // Под Windows можно сразу форматнуть строку в нужный буфер         // Under Windows, you can immediately format the string into the desired buffer
        std::swprintf((wchar_t*)ptr, 20, L"%04i-%02i-%02i %02i:%02i:%02i", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday,         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); t.tm_hour, t.tm_min, t.tm_sec);
    } else {     } else {
        // Сначала форматнём в промежуточный буфер, потом скопируем в результат         // First, format into an intermediate buffer, then copy to the result
        char buf[20];         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,         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); t.tm_min, t.tm_sec);
@ -701,7 +699,7 @@ inline u16s* expr_str_tm::place(u16s* ptr) const noexcept {
} }
``` ```
Использование Usage
```cpp ```cpp
...... ......
@ -717,31 +715,31 @@ bool makeBind(SqliteQuery& query, tVariant& param, unsigned paramNum) {
...... ......
``` ```
ВНИМАНИЕ: обычно поля в объектах строковых выражений являются ссылками на исходные данные. ATTENTION: usually the fields in string expression objects are references to the source data.
И ссылки эти почти всегда ведут на локальные или временные объекты. Поэтому крайне рискованно возвращать строковые выражения And these references almost always lead to local or temporary objects. Therefore, it is extremely risky to return string expressions
из функций — надо сто раз проверить, что в них не попали ссылки на локальные или временные переменные. from functions — you need to check a hundred times that they do not contain references to local or temporary variables.
Возьмите за правило — можно легко передавать строковые выражения в функции, и опасно возвращать их из функций. Make it a rule — you can easily pass string expressions to functions, and it is dangerous to return them from functions.
Лучше при возврате материализовать строковое выражение в строковый объект, содержащий итоговую строку. It is better to materialize a string expression into a string object containing the final string when returning.
При желании тип возвращаемой строки можно задать шаблонным параметром. If desired, the type of the returned string can be specified by a template parameter.
### Класс chunked_string_builder ### Class chunked_string_builder
Предназначен для конкатенации множества строк. Designed for concatenating multiple strings.
Когда вам нужно последовательно формировать длинный текст из множества небольших кусочков (например, формируете html ответ When you need to sequentially form a long text from many small pieces (for example, you are forming an html response
и т. п.) - последовательно складывать всё в один строковый объект крайне неоптимально — будет много переаллокаций и etc.) - sequentially adding everything to one string object is extremely suboptimal - there will be many reallocations and
перекопирования уже накопленных символов. В этом случае удобно использовать chunked_string_builder — всё, что он умеет, copying of already accumulated characters. In this case, it is convenient to use chunked_string_builder - all it can do is
это прибавлять строку к накопленным символам. Однако делает он это не в единый последовательный буфер памяти, а в отдельные add a string to the accumulated characters. However, it does this not in a single sequential memory buffer, but in separate
буфера, не меньше чем заданное выравнивание. При заполнении очередного буфера он просто создает ещё один буфер и продолжает buffers, no less than the specified alignment. When filling the next buffer, it simply creates another buffer and continues
складывать данные в него. to add data to it.
То есть допустим вы задали выравнивание 1024. That is, suppose you set the alignment to 1024.
Добавили несколько строк, заполнили буфер на 100 символов. И добавляете строку длинной 3000 символов. Added several strings, filled the buffer with 100 characters. And you add a string of 3000 characters long.
При этом 924 символа скопируются в первый буфер, заполнив его до конца. In this case, 924 characters will be copied to the first buffer, filling it to the end.
Для оставшихся 2076 создастся буфер размером 3072 символа, и они скопируются в него, в нём останется место для 996 символов. For the remaining 2076, a buffer of 3072 characters will be created, and they will be copied into it, leaving space for 996 characters in it.
Так последовательно каждый буфер заполняется до конца, и имеет размер кратный заданному выравниванию. Thus, each buffer is sequentially filled to the end and has a size that is a multiple of the specified alignment.
Таким образом избегаются переаллокации и перекопирование обработанных символов. This avoids reallocations and copying of processed characters.
После окончательного заполнения вы можете работать с накопленными данными — либо слить все буфера в одну последовательную After the final filling, you can work with the accumulated data - either merge all the buffers into one sequential
строку (размер для буфера которой вы теперь уже знаете), либо перебирать их по отдельности, например, посылая эти буфера string (the size for the buffer of which you now already know), or iterate over them separately, for example, sending these buffers
в сеть. Либо последовательно копируя данные в буфер заданного размера. to the network. Or sequentially copying data into a buffer of a given size.

749
docs/overview_ru.md Normal file
View File

@ -0,0 +1,749 @@
# Строки в С++
(, что с вами не так?)
[On English|По-английски](overview.md)
<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, char16_t,
char32_t, wchar_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\>
- `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>
- `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>
- `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=16>` для lsrting\<char, N, false>
- `lstringu<N=16>` для lsrting\<char16_t, N, false>
- `lstringw<N=16>` для lsrting\<wchar_t, N, false>
- `lstringuu<N=16>` для lsrting\<char32_t, N, false>
- `lstringsa<N=16>` для lsrting\<char, N, true>
- `lstringsu<N=16>` для lsrting\<char16_t, N, true>
- `lstringsw<N=16>` для lsrting\<wchar_t, N, true>
- `lstringsuu<N=16>` для 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.
Для строковых выражений определена шаблонная функция сложения:
```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 +`, формируя цепочку из нескольких строковых выражений,
и в итоге "материализовать" последний получившийся объект, который сначала посчитает размер всей общей памяти для
конечного результата, а затем разместит вложенные подвыражения в один буфер.
Все строковые типы библиотеки сами являются строковыми выражениями, то есть могут служить слагаемыми в конкатенациях
строковых выражений.
Также `operator+` определён для строковых выражений и строковых литералов, строковых выражений и чисел (числа конвертируются
в десятичное представление), а также вы можете сами добавить желаемые типы.
Пример:
```cpp
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_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 символов.
Так последовательно каждый буфер заполняется до конца, и имеет размер кратный заданному выравниванию.
Таким образом избегаются переаллокации и перекопирование обработанных символов.
После окончательного заполнения вы можете работать с накопленными данными — либо слить все буфера в одну последовательную
строку (размер для буфера которой вы теперь уже знаете), либо перебирать их по отдельности, например, посылая эти буфера
в сеть. Либо последовательно копируя данные в буфер заданного размера.

View File

@ -1,14 +1,16 @@
# Объекты simstr в отладчиках # simstr Objects in Debuggers
Для более удобного отображения объектов simstr в отладчиках msvc и gdb подготовлено два файла: [On Russian|По-русски](readme_ru.md)
simstr.natvis - для использования в отладчике MSVC, и simstr_pretty_print.py для работы с gdb.
# Объекты simstr в отладчиках Two files have been prepared for more convenient display of simstr objects in msvc and gdb debuggers:
Если вы работаете в MS Visual Studio simstr.natvis автоматически добавляется в pdb файл, simstr.natvis - for use in the MSVC debugger, and simstr_pretty_print.py for working with gdb.
и обеспечивает удобный просмотр строковых объектов simstr везде, где используется эта библиотека.
# Объекты simstr в Visual Studio Code # simstr Objects in Debuggers
## При работе в gdb If you are working in MS Visual Studio, simstr.natvis is automatically added to the pdb file,
В конфигурации отладчика необходимо добавить следующие строки: and provides convenient viewing of simstr string objects wherever this library is used.
# simstr Objects in Visual Studio Code
## When working in gdb
In the debugger configuration, you need to add the following lines:
``` ```
"setupCommands": [ "setupCommands": [
@ -25,16 +27,15 @@ simstr.natvis - для использования в отладчике MSVC, и
] ]
``` ```
После этого в отладчике будут удобно отображаться объекты simstr. After that, simstr objects will be conveniently displayed in the debugger.
Скрипт инспектирует переменные этих типов и выдает для них текстовое описание, отображающее их The script inspects variables of these types and provides a textual description for them, displaying their
содержимое в удобном виде. В первой строке отображается основная информация, видимая в окне content in a convenient form. The first line displays the basic information visible in the
инспектирования переменных. При наведении указателя мыши на переменную в окне исходного кода variable inspection window. When you hover the mouse over a variable in the source code window
или на значении в окне инспектирования переменных во всплывающем тултипе будет показана or over a value in the variable inspection window, the remaining information will be shown in the pop-up tooltip.
остальная информация.
Конфигурации отладчика располагаются в файле `.vscode/launch.json`. Debugger configurations are located in the `.vscode/launch.json` file.
Возможно, у вас также установлено расширение `CMake Tools`, которое позволяет выбирать целевой проект для запуска. You may also have the `CMake Tools` extension installed, which allows you to select the target project to run.
В этом случае настройки для подключения скрипта прописываются в `.vscode/settings.json`: In this case, the settings for connecting the script are written in `.vscode/settings.json`:
``` ```
"cmake.debugConfig": { "cmake.debugConfig": {
@ -55,7 +56,7 @@ simstr.natvis - для использования в отладчике MSVC, и
} }
``` ```
Если же в Visual Studio Code вы работаете с отладчиком MSVC, то есть в launch.json `"type": "cppvsdbg"` то настройка другая: If you are working with the MSVC debugger in Visual Studio Code, that is, in launch.json `"type": "cppvsdbg"`, then the setting is different:
``` ```
"configurations": [ "configurations": [
{ {

70
for_debug/readme_ru.md Normal file
View File

@ -0,0 +1,70 @@
# Объекты simstr в отладчиках
[On English|По-английски](readme.md)
Для более удобного отображения объектов simstr в отладчиках msvc и gdb подготовлено два файла:
simstr.natvis - для использования в отладчике MSVC, и simstr_pretty_print.py для работы с gdb.
# Объекты simstr в отладчиках
Если вы работаете в MS Visual Studio simstr.natvis автоматически добавляется в pdb файл,
и обеспечивает удобный просмотр строковых объектов simstr везде, где используется эта библиотека.
# Объекты simstr в Visual Studio Code
## При работе в gdb
В конфигурации отладчика необходимо добавить следующие строки:
```
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "Enable pretty-printing for simstr",
"text": "source ${workspaceFolder}/for_debug/simstr_pretty_print.py",
"ignoreFailures": true
}
]
```
После этого в отладчике будут удобно отображаться объекты simstr.
Скрипт инспектирует переменные этих типов и выдает для них текстовое описание, отображающее их
содержимое в удобном виде. В первой строке отображается основная информация, видимая в окне
инспектирования переменных. При наведении указателя мыши на переменную в окне исходного кода
или на значении в окне инспектирования переменных во всплывающем тултипе будет показана
остальная информация.
Конфигурации отладчика располагаются в файле `.vscode/launch.json`.
Возможно, у вас также установлено расширение `CMake Tools`, которое позволяет выбирать целевой проект для запуска.
В этом случае настройки для подключения скрипта прописываются в `.vscode/settings.json`:
```
"cmake.debugConfig": {
"MIMode": "gdb",
"environment": [],
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"text": "source ${workspaceFolder}/for_debug/simstr_pretty_print.py",
"description": "pretty print simstr",
"ignoreFailures": true
}
]
}
```
Если же в Visual Studio Code вы работаете с отладчиком MSVC, то есть в launch.json `"type": "cppvsdbg"` то настройка другая:
```
"configurations": [
{
.....
"type": "cppvsdbg",
....
"visualizerFile": "${workspaceFolder}/for_debug/simstr.natvis"
},
...
```

View File

@ -12,7 +12,8 @@
#include <utility> #include <utility>
/*! /*!
* @brief Пространство имён для объектов библиотеки * @ru @brief Пространство имён для объектов библиотеки
* @en @brief Library namespace
*/ */
namespace simstr { namespace simstr {
@ -145,15 +146,25 @@ public:
}; };
/*! /*!
* @brief Базовая концепция строкового объекта. * @ru @brief Базовая концепция строкового объекта.
* @tparam A - проверяемый тип * @ru @tparam A - проверяемый тип
* @tparam K - тип символов * @ru @tparam K - тип символов
* @details В библиотеке для разных целей могут использоваться различные типы объектов строк. * @ru @details В библиотеке для разных целей могут использоваться различные типы объектов строк.
* Мы считаем строковым объектом любой объект, поддерживающий методы: * Мы считаем строковым объектом любой объект, поддерживающий методы:
* - `is_empty()`: возвращает, пуста ли строка. * - `is_empty()`: возвращает, пуста ли строка.
* - `length()`: возвращает длину строки без нулевого терминатора. * - `length()`: возвращает длину строки без нулевого терминатора.
* - `symbols()`: возвращает указатель на строку символов. * - `symbols()`: возвращает указатель на строку символов.
* - `typename symb_type`: задаёт тип символов строки * - `typename symb_type`: задаёт тип символов строки
*
* @en @brief Base concept of string object.
* @en @tparam A - tested type
* @en @tparam K - type of symbols @ru K - тип символов
* @en @details The library can use different types of string objects for different purposes.
* We consider a string object to be any object that supports methods:
* - `is_empty()`: Returns whether the string is empty.
* - `length()`: returns the length of a string without a null terminator.
* - `symbols()`: returns a pointer to a string of symbols.
* - `typename symb_type`: sets the character type of the string
*/ */
template<typename A, typename K> template<typename A, typename K>
concept StrType = requires(const A& a) { concept StrType = requires(const A& a) {

154
readme.md
View File

@ -1,94 +1,96 @@
# simstr - библиотека строковых объектов и функций # simstr - String object and function library
[![CMake on multiple platforms](https://github.com/orefkov/simstr/actions/workflows/cmake-multi-platform.yml/badge.svg)](https://github.com/orefkov/simstr/actions/workflows/cmake-multi-platform.yml) [![CMake on multiple platforms](https://github.com/orefkov/simstr/actions/workflows/cmake-multi-platform.yml/badge.svg)](https://github.com/orefkov/simstr/actions/workflows/cmake-multi-platform.yml)
Версия 1.2.4. Version 1.2.4.
В этой библиотеке содержится реализация нескольких видов строковых объектов и различных алгоритмов для работы со строками. [On Russian|По-русски](readme_ru.md)
Цель библиотеки - сделать работу со строками в С++ такой же простой и лёгкой, как во множестве других языков, особенно This library contains the implementation of several types of string objects and various algorithms for working with strings.
скриптовых, но при этом сохранив оптимальность и производительность на уровне С и C++, и даже улучшив их.
Не секрет, что работа со строками в С++ зачастую доставляет боль. Класс `std::string` часто неудобен либо неэффективен. The goal of the library is to make working with strings in C++ as simple and easy as in many other languages, especially
Многих функций, обычно необходимых при работе со строками, просто нет, и их каждому приходится писать самому. scripting languages, while maintaining optimality and performance at the level of C and C++, and even improving them.
Эта библиотека не делалась как универсальный комбайн, который "может всё", я реализовывал то, что мне приходилось It's no secret that working with strings in C++ often causes pain. The `std::string` class is often inconvenient or inefficient.
использовать в работе, стараясь сделать это наиболее эффективным способом, и скромно надеюсь, что кое-что у меня получилось Many functions that are usually necessary when working with strings are simply not there, and everyone has to write them themselves.
и пригодится другим людям, либо напрямую, либо как источник идей.
Библиотека не претендует на роль "поменял хедер и всё заработало лучше". Многие методы я старался делать совместимыми This library was not made as a universal combine that "can do everything", I implemented what I had to
с `std::string` и `std::string_view`, но особо с этим не заморачивался. Переписывание старого кода на работу с simstr use at work, trying to do it in the most efficient way, and I modestly hope that I succeeded in something
потребует некоторых усилий, но уверяю, что они окупятся. А новый код писать с её применением легко и доставляет удовольствие :) and will be useful to other people, either directly or as a source of ideas.
Основное отличие simstr от std::string - для работы со строками используется не единый универсальный класс, а несколько The library does not pretend to be a "change the header and everything works better" solution. I tried to make many methods compatible
видов объектов, каждый из которых хорош для своих целей, и при этом хорошо взаимодействующих друг с другом. with `std::string` and `std::string_view`, but I didn't bother with it much. Rewriting old code to work with simstr
Если вы активно использовали std::string_view и понимали, в чём его преимущество и недостатки по сравнению с std::string, will require some effort, but I assure you that it will pay off. And writing new code with its use is easy and enjoyable :)
то подход simstr вам также будет понятен.
## Основные возможности библиотеки The main difference between simstr and std::string is that instead of a single universal class, several
- Строки `char`, `char16_t`, `char32_t`, `wchar_t`. types of objects are used to work with strings, each of which is good for its own purposes, and at the same time interacts well with each other.
- Прозрачное преобразование строк из одного типа символов в другой, с автоматической конвертацией между UTF-8, UTF-16, UTF-32, If you actively used std::string_view and understood its advantages and disadvantages compared to std::string,
используя [simdutf](https://github.com/simdutf/simdutf). then the simstr approach will also be clear to you.
- Расширяемая система "Строковых выражений". Позволяет эффективно реализовать преобразование и сложение (конкатенацию) строк, литералов,
чисел и возможно других объектов.
- Строковые функции:
- Получение подстрок.
- Поиск подстрок и символов - с начала или с конца строки.
- Различный тримминг строк - справа, слева, везде, по пробельным символам, по заданным символам.
- Замена подстрок.
- Замена набора символов на набор соответствующих подстрок.
- Слияние (join) контейнеров строк в единую строку, с заданием разделителей и опций - "пропускать пустые", "разделитель после последней".
- Разбиение (split) строк на части по заданному разделителю. Разбиение возможно сразу в контейнер со строками, либо вызовом функтора для
каждой подстроки, либо путем итерации с помощью итератора `Splitter`.
- Интеграция с функциями форматирования `format` и `sprintf` (с автоматическим увеличением буфера).
Форматирование возможно для строк `char`, `wchar_t` и строк, совместимых с `wchar_t` по размеру.
То есть под Windows это `char16_t`, под Linux - `char32_t`. Писать свою библиотеку форматирования не входило в мои замыслы.
- Парсинг целых чисел с возможностью "тонкой" настройки при компиляции - можно задавать опции проверки переполнения,
пропуск пробельных символов, конкретное основание счисления либо автовыбор по префиксам `0x`, `0`, `0b`, `0o`,
допустимость знака `+`. Парсинг реализован для всех видов строк и символов.
- Парсинг double пока реализован вызовом стандартной библиотеки и работает только для строк `char`, `wchar_t` и совместимых с
`wchar_t` по размеру типов.
- Содержится минимальная поддержка Unicode при преобразовании `upper`, `lower` и регистро-независимом сравнении строк.
Работает только для символов первой плоскости Unicode (до 0xFFFF), а при смене регистра не учитываются случаи, когда один code point
может преобразовываться в несколько, то есть преобразование регистра символов соответствует `std::towupper`, `std::towlower` для unicode локали, только быстрее и может работать с любым видом символов.
- Реализован `hash map` для ключей строкового типа, на базе `std::unordered_map`, с возможностью более эффективного хранения и
сравнения ключей по сравнению с ключами `std::string`. Поддерживается возможность регистро-независимого сравнения ключей (Ascii или
минимальный Unicode (см. предыдущий пункт)).
## Основные объекты библиотеки ## Main features of the library
- simple_str&lt;K> - самая простая строка (или кусок строки), иммутабельная, не владеющая, аналог `std::string_view`. - Strings `char`, `char16_t`, `char32_t`, `wchar_t`.
- simple_str_nt&lt;K> - то же самое, только заявляет, что заканчивается 0. Для работы со сторонними C-API. - Transparent conversion of strings from one character type to another, with automatic conversion between UTF-8, UTF-16, UTF-32,
- sstring&lt;K> - shared string, иммутабельная, владеющая, с разделяемым буфером символов, поддержка SSO. using [simdutf](https://github.com/simdutf/simdutf).
- lstring&lt;K, N> - local string, мутабельная, владеющая, с задаваемым размером SSO буфера. - Extensible "String Expression" system. Allows you to efficiently implement the conversion and addition (concatenation) of strings, literals,
numbers and possibly other objects.
- String functions:
- Getting substrings.
- Searching for substrings and characters - from the beginning or from the end of the string.
- Various string trimming - right, left, everywhere, by whitespace characters, by specified characters.
- Replacing substrings.
- Replacing a set of characters with a set of corresponding substrings.
- Merging (join) containers of strings into a single string, with specifying separators and options - "skip empty", "separator after last".
- Splitting strings into parts by a specified separator. Splitting is possible directly into a container with strings, or by calling a functor for
each substring, or by iterating using the `Splitter` iterator.
- Integration with `format` and `sprintf` formatting functions (with automatic buffer increase).
Formatting is possible for `char`, `wchar_t` strings and strings compatible with `wchar_t` in size.
That is, under Windows it is `char16_t`, under Linux - `char32_t`. Writing my own formatting library was not part of my plans.
- Parsing integers with the possibility of "fine" tuning during compilation - you can set options for checking overflow,
skipping whitespace characters, a specific radix or auto-selection by prefixes `0x`, `0`, `0b`, `0o`,
admissibility of the `+` sign. Parsing is implemented for all types of strings and characters.
- Parsing double is currently implemented by calling the standard library and only works for `char`, `wchar_t` strings and types compatible with
`wchar_t` in size.
- Minimal Unicode support is included when converting `upper`, `lower` and case-insensitive string comparison.
It only works for characters in the first plane of Unicode (up to 0xFFFF), and when changing case, it does not take into account cases where one code point
can be converted into several, that is, the case conversion of characters corresponds to `std::towupper`, `std::towlower` for the unicode locale, only faster and can work with any type of characters.
- Implemented `hash map` for string type keys, based on `std::unordered_map`, with the possibility of more efficient storage and
comparison of keys compared to `std::string` keys. Case-insensitive key comparison is supported (Ascii or
minimal Unicode (see previous paragraph)).
## Статьи ## Main objects of the library
- [Обзор и введение](docs/overview.md) - simple_str&lt;K> - the simplest string (or piece of string), immutable, not owning, analogue of `std::string_view`.
- [Обзорная статья на Хабре](https://habr.com/ru/articles/935590) - simple_str_nt&lt;K> - the same, only declares that it ends with 0. For working with third-party C-API.
- [Описание применяемой техники "Expression Templates"](https://habr.com/ru/articles/936468/) - sstring&lt;K> - shared string, immutable, owning, with shared character buffer, SSO support.
- lstring&lt;K, N> - local string, mutable, owning, with a specified size of the SSO buffer.
## Использование ## Articles
`simstr` состоит из трёх заголовочных файлов и двух исходников. Можно подключать как CMake проект через `add_subdirectory` (библиотека `simstr`), - [Overview and introduction](docs/overview.md)
можно просто включить файлы в свой проект. Для сборки также требуется [simdutf](https://github.com/simdutf/simdutf) (при использовании CMake - [Overview article on Habr](https://habr.com/ru/articles/935590)
скачивается автоматически). - [Description of the "Expression Templates" technique used](https://habr.com/ru/articles/936468/)
Для работы `simstr` требуется компилятор стандарта не ниже С++20 - используются концепты и std::format. ## Usage
Работа проверялась под Windows на MSVC-19 и Clang-19, под Linux - на GCC-13 и Clang-21. `simstr` consists of three header files and two source files. You can connect as a CMake project via `add_subdirectory` (the `simstr` library),
Также проверялась работа в WASM, сборка в Emscripten 4.0.6, Clang-21. you can simply include the files in your project. Building also requires [simdutf](https://github.com/simdutf/simdutf) (when using CMake
it is downloaded automatically).
`simstr` requires a compiler of standard no lower than C++20 to work - concepts and std::format are used.
The work was tested under Windows on MSVC-19 and Clang-19, under Linux - on GCC-13 and Clang-21.
The work in WASM was also tested, built in Emscripten 4.0.6, Clang-21.
## Бенчмарки ## Benchmarks
Бенчмарки производятся с использованием фреймворка [Google benchmark](https://github.com/google/benchmark). Benchmarks are performed using the [Google benchmark](https://github.com/google/benchmark) framework.
Постарался сделать замеры для наиболее типичных операций, встречающихся в обычной работе. Я проводил замеры на своём оборудовании, под I tried to make measurements for the most typical operations that occur in normal work. I took measurements on my equipment, under
Windows и Linux (в WSL), с использованием компиляторов MSVC, Clang, GCC. Сторонние результаты приветствуются. Windows and Linux (in WSL), using MSVC, Clang, GCC compilers. Third-party results are welcome.
Также проводил замеры в WASM, сборка в Emscripten. Обращаю внимание, что под WASM в Emscripten собирается 32-битная сборка, а значит, I also took measurements in WASM, built in Emscripten. I draw your attention to the fact that a 32-bit build is assembled under WASM in Emscripten, which means that
размеры буферов SSO в объектах меньше. the sizes of SSO buffers in objects are smaller.
- [Исходный код бенчмарков](bench/bench_str.cpp) - [Benchmark source code](bench/bench_str.cpp)
- [Результаты бенчмарков](https://snegopat.ru/simstr/results.html) - [Benchmark results](https://snegopat.ru/simstr/results.html)
## Примеры использования ## Usage examples
Пока отдельных примеров использования не подготовлено, можно посмотреть тексты [тестов](tests/test_str.cpp), While no separate usage examples have been prepared, you can look at the texts of [tests](tests/test_str.cpp),
[бенчмарков](bench/bench_str.cpp), и [утилиты подготовки html](bench/process_result.cpp) из результатов бенчмарков. [benchmarks](bench/bench_str.cpp), and [html preparation utilities](bench/process_result.cpp) from the benchmark results.
Также simstr используется в моём проекте [v8sqlite](https://github.com/orefkov/v8sqlite) Also, simstr is used in my [v8sqlite](https://github.com/orefkov/v8sqlite) project
## Сгенерированная документация ## Generated documentation
[Находится здесь](https://snegopat.ru/simstr/docs/) [Located here](https://snegopat.ru/simstr/docs/)

96
readme_ru.md Normal file
View File

@ -0,0 +1,96 @@
# simstr - библиотека строковых объектов и функций
[![CMake on multiple platforms](https://github.com/orefkov/simstr/actions/workflows/cmake-multi-platform.yml/badge.svg)](https://github.com/orefkov/simstr/actions/workflows/cmake-multi-platform.yml)
Версия 1.2.4.
[On English|По-английски](readme.md)
В этой библиотеке содержится реализация нескольких видов строковых объектов и различных алгоритмов для работы со строками.
Цель библиотеки - сделать работу со строками в С++ такой же простой и лёгкой, как во множестве других языков, особенно
скриптовых, но при этом сохранив оптимальность и производительность на уровне С и C++, и даже улучшив их.
Не секрет, что работа со строками в С++ зачастую доставляет боль. Класс `std::string` часто неудобен либо неэффективен.
Многих функций, обычно необходимых при работе со строками, просто нет, и их каждому приходится писать самому.
Эта библиотека не делалась как универсальный комбайн, который "может всё", я реализовывал то, что мне приходилось
использовать в работе, стараясь сделать это наиболее эффективным способом, и скромно надеюсь, что кое-что у меня получилось
и пригодится другим людям, либо напрямую, либо как источник идей.
Библиотека не претендует на роль "поменял хедер и всё заработало лучше". Многие методы я старался делать совместимыми
с `std::string` и `std::string_view`, но особо с этим не заморачивался. Переписывание старого кода на работу с simstr
потребует некоторых усилий, но уверяю, что они окупятся. А новый код писать с её применением легко и доставляет удовольствие :)
Основное отличие simstr от std::string - для работы со строками используется не единый универсальный класс, а несколько
видов объектов, каждый из которых хорош для своих целей, и при этом хорошо взаимодействующих друг с другом.
Если вы активно использовали std::string_view и понимали, в чём его преимущество и недостатки по сравнению с std::string,
то подход simstr вам также будет понятен.
## Основные возможности библиотеки
- Строки `char`, `char16_t`, `char32_t`, `wchar_t`.
- Прозрачное преобразование строк из одного типа символов в другой, с автоматической конвертацией между UTF-8, UTF-16, UTF-32,
используя [simdutf](https://github.com/simdutf/simdutf).
- Расширяемая система "Строковых выражений". Позволяет эффективно реализовать преобразование и сложение (конкатенацию) строк, литералов,
чисел и возможно других объектов.
- Строковые функции:
- Получение подстрок.
- Поиск подстрок и символов - с начала или с конца строки.
- Различный тримминг строк - справа, слева, везде, по пробельным символам, по заданным символам.
- Замена подстрок.
- Замена набора символов на набор соответствующих подстрок.
- Слияние (join) контейнеров строк в единую строку, с заданием разделителей и опций - "пропускать пустые", "разделитель после последней".
- Разбиение (split) строк на части по заданному разделителю. Разбиение возможно сразу в контейнер со строками, либо вызовом функтора для
каждой подстроки, либо путем итерации с помощью итератора `Splitter`.
- Интеграция с функциями форматирования `format` и `sprintf` (с автоматическим увеличением буфера).
Форматирование возможно для строк `char`, `wchar_t` и строк, совместимых с `wchar_t` по размеру.
То есть под Windows это `char16_t`, под Linux - `char32_t`. Писать свою библиотеку форматирования не входило в мои замыслы.
- Парсинг целых чисел с возможностью "тонкой" настройки при компиляции - можно задавать опции проверки переполнения,
пропуск пробельных символов, конкретное основание счисления либо автовыбор по префиксам `0x`, `0`, `0b`, `0o`,
допустимость знака `+`. Парсинг реализован для всех видов строк и символов.
- Парсинг double пока реализован вызовом стандартной библиотеки и работает только для строк `char`, `wchar_t` и совместимых с
`wchar_t` по размеру типов.
- Содержится минимальная поддержка Unicode при преобразовании `upper`, `lower` и регистро-независимом сравнении строк.
Работает только для символов первой плоскости Unicode (до 0xFFFF), а при смене регистра не учитываются случаи, когда один code point
может преобразовываться в несколько, то есть преобразование регистра символов соответствует `std::towupper`, `std::towlower` для unicode локали, только быстрее и может работать с любым видом символов.
- Реализован `hash map` для ключей строкового типа, на базе `std::unordered_map`, с возможностью более эффективного хранения и
сравнения ключей по сравнению с ключами `std::string`. Поддерживается возможность регистро-независимого сравнения ключей (Ascii или
минимальный Unicode (см. предыдущий пункт)).
## Основные объекты библиотеки
- simple_str&lt;K> - самая простая строка (или кусок строки), иммутабельная, не владеющая, аналог `std::string_view`.
- simple_str_nt&lt;K> - то же самое, только заявляет, что заканчивается 0. Для работы со сторонними C-API.
- sstring&lt;K> - shared string, иммутабельная, владеющая, с разделяемым буфером символов, поддержка SSO.
- lstring&lt;K, N> - local string, мутабельная, владеющая, с задаваемым размером SSO буфера.
## Статьи
- [Обзор и введение](docs/overview_ru.md)
- [Обзорная статья на Хабре](https://habr.com/ru/articles/935590)
- [Описание применяемой техники "Expression Templates"](https://habr.com/ru/articles/936468/)
## Использование
`simstr` состоит из трёх заголовочных файлов и двух исходников. Можно подключать как CMake проект через `add_subdirectory` (библиотека `simstr`),
можно просто включить файлы в свой проект. Для сборки также требуется [simdutf](https://github.com/simdutf/simdutf) (при использовании CMake
скачивается автоматически).
Для работы `simstr` требуется компилятор стандарта не ниже С++20 - используются концепты и std::format.
Работа проверялась под Windows на MSVC-19 и Clang-19, под Linux - на GCC-13 и Clang-21.
Также проверялась работа в WASM, сборка в Emscripten 4.0.6, Clang-21.
## Бенчмарки
Бенчмарки производятся с использованием фреймворка [Google benchmark](https://github.com/google/benchmark).
Постарался сделать замеры для наиболее типичных операций, встречающихся в обычной работе. Я проводил замеры на своём оборудовании, под
Windows и Linux (в WSL), с использованием компиляторов MSVC, Clang, GCC. Сторонние результаты приветствуются.
Также проводил замеры в WASM, сборка в Emscripten. Обращаю внимание, что под WASM в Emscripten собирается 32-битная сборка, а значит,
размеры буферов SSO в объектах меньше.
- [Исходный код бенчмарков](bench/bench_str.cpp)
- [Результаты бенчмарков](https://snegopat.ru/simstr/results.html)
## Примеры использования
Пока отдельных примеров использования не подготовлено, можно посмотреть тексты [тестов](tests/test_str.cpp),
[бенчмарков](bench/bench_str.cpp), и [утилиты подготовки html](bench/process_result.cpp) из результатов бенчмарков.
Также simstr используется в моём проекте [v8sqlite](https://github.com/orefkov/v8sqlite)
## Сгенерированная документация
[Находится здесь](https://snegopat.ru/simstr/docs/)