mirror of https://github.com/orefkov/simstr.git
Translation of the description and documentation into English has begun.
This commit is contained in:
parent
8d8ec95faa
commit
b76486ff2b
|
|
@ -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
|
||||||
|
|
|
||||||
642
docs/overview.md
642
docs/overview.md
|
|
@ -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 don’t 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 don’t 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 don’t 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 don’t 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.
|
||||||
|
|
|
||||||
|
|
@ -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 символов.
|
||||||
|
Так последовательно каждый буфер заполняется до конца, и имеет размер кратный заданному выравниванию.
|
||||||
|
Таким образом избегаются переаллокации и перекопирование обработанных символов.
|
||||||
|
|
||||||
|
После окончательного заполнения вы можете работать с накопленными данными — либо слить все буфера в одну последовательную
|
||||||
|
строку (размер для буфера которой вы теперь уже знаете), либо перебирать их по отдельности, например, посылая эти буфера
|
||||||
|
в сеть. Либо последовательно копируя данные в буфер заданного размера.
|
||||||
|
|
@ -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": [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
@ -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
154
readme.md
|
|
@ -1,94 +1,96 @@
|
||||||
# simstr - библиотека строковых объектов и функций
|
# simstr - String object and function library
|
||||||
[](https://github.com/orefkov/simstr/actions/workflows/cmake-multi-platform.yml)
|
[](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<K> - самая простая строка (или кусок строки), иммутабельная, не владеющая, аналог `std::string_view`.
|
- Strings `char`, `char16_t`, `char32_t`, `wchar_t`.
|
||||||
- simple_str_nt<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<K> - shared string, иммутабельная, владеющая, с разделяемым буфером символов, поддержка SSO.
|
using [simdutf](https://github.com/simdutf/simdutf).
|
||||||
- lstring<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<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<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<K> - shared string, immutable, owning, with shared character buffer, SSO support.
|
||||||
|
- lstring<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/)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
# simstr - библиотека строковых объектов и функций
|
||||||
|
[](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<K> - самая простая строка (или кусок строки), иммутабельная, не владеющая, аналог `std::string_view`.
|
||||||
|
- simple_str_nt<K> - то же самое, только заявляет, что заканчивается 0. Для работы со сторонними C-API.
|
||||||
|
- sstring<K> - shared string, иммутабельная, владеющая, с разделяемым буфером символов, поддержка SSO.
|
||||||
|
- lstring<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/)
|
||||||
Loading…
Reference in New Issue