From b76486ff2b0457440d4bca912b3fe979403b011a Mon Sep 17 00:00:00 2001 From: Aleksandr Orefkov Date: Sun, 16 Nov 2025 11:17:02 +0300 Subject: [PATCH] Translation of the description and documentation into English has begun. --- docs/Doxyfile | 5 +- docs/overview.md | 646 +++++++++++++++++---------------- docs/overview_ru.md | 749 +++++++++++++++++++++++++++++++++++++++ for_debug/readme.md | 39 +- for_debug/readme_ru.md | 70 ++++ include/simstr/strexpr.h | 21 +- readme.md | 154 ++++---- readme_ru.md | 96 +++++ 8 files changed, 1354 insertions(+), 426 deletions(-) create mode 100644 docs/overview_ru.md create mode 100644 for_debug/readme_ru.md create mode 100644 readme_ru.md diff --git a/docs/Doxyfile b/docs/Doxyfile index 23ae796..150ed96 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = "simstr" # could be handy for archiving the generated documentation or if some version # 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 # 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 # @} 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 # only. Doxygen will then generate output that is more tailored for C. For diff --git a/docs/overview.md b/docs/overview.md index a75182d..c13887d 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -1,30 +1,31 @@ -# Строки в С++ -(, что с вами не так?) +# Strings in C++ +(, what's wrong with you?) + +[On Russian|По-русски](overview_ru.md) -В ретроспективе 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" -(«Их отсутствие привело к тому, что все заново изобретали велосипед, и к ненужному разнообразию в самых фундаментальных классах»). +In a 1991 retrospective on the history of C++, its creator Bjarne Stroustrup called the lack of a standard string type +(and some other standard types) in C++ 1.0 the worst mistake he made in its development: +"Their absence led to everyone reinventing the wheel and to an unnecessary diversity in the most fundamental classes" -## Что было и есть -Во вступительной части я хочу немного описать, каково ныне состояние со строками в С++, как мы к нему докатились и почему оно таково. -А также описать недостатки текущих реализаций, чтобы были понятны решения, которые я использую в своей библиотеке строк. +## 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. -Собственно, изначально как такового стандартного типа для строк в С++ не было. -Для работы со строками использовался подход из C – строка есть указатель на массив байтов, оканчивающихся нулём. -Недостатки таких строк — невозможно в строке использовать байт `0`, т. е. не подходит для бинарных данных, -непонятна стратегия управления/владения ресурсами, ну и основной недостаток — длину строки приходится вычислять каждый раз, -перебирая все её символы. +Actually, initially there was no standard type for strings in C++. +The approach from C was used to work with strings – a string is a pointer to an array of bytes ending in zero. +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, и как -многое из STL, крайне неоднозначно воспринимался программистами. +The first attempts to standardize strings as a class began only in C++98 - std::string appeared as part of STL, and like +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 struct simple_string {     const char* data; @@ -32,92 +33,92 @@ }; ``` -При наличии такой строки, уже множество алгоритмов значительно оптимизируются. -Например, при сравнении двух строк на равенство мы можем даже не начинать сравнивать их символы, если длины строк не равны. -Более того, этих данных абсолютно достаточно для всех методов, которые не модифицируют строку. -Также заметим, что такой объект на современных 64-битных архитектурах прекрасно передается в функции по значению — -оба его поля укладываются в регистры (ну, кроме windows), что облегчает работу оптимизатору компилятора. +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. +Also note that such an object on modern 64-bit architectures is perfectly passed to functions by value – +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. -### Ресурсы -И следующий вопрос, возникающий со строками — это владение ресурсами. -Практически каждый крупный фреймворк решал эту задачу самостоятельно, изобретая свои велосипеды. -У нас есть `std::string`, в QT у нас `QString`, в MFC - `CString`, в ATL - `CAtlString`, свои строки есть в Folly, -в общем, “тысячи их”, любой игровой движок начинают с того, чтобы написать свои строки. +### 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. +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. -Многие из этих реализаций в аспекте управления ресурсами для улучшения производительности использовали подход -**COW** – “Copy On Write”. При этом объект строки ссылался на некий разделяемый между несколькими объектами буфер с символами -строки и счётчиком ссылок на этот буфер, что позволяло быстро создавать копию строки, а реально копировать символы только -при её модификации. +Many of these implementations in the aspect of resource management used the approach to improve performance +**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. -### Мутабельность / иммутабельность -Из-за этого подход **COW** умер к С++11: при каждой операции, могущей модифицировать символы строки приходилось проверять, -не ссылаемся ли мы на разделяемый буфер и если да, то копировать символы в другой буфер. -В многопоточной же среде потом ещё и проверять, не надо ли теперь освобождать старый буфер, и естественно всё это обмазавшись -локами или атомиками, что тоже не бесплатно. -Поэтому, начиная с С++11 `std::string` не использует **COW**, и каждое копирование объекта строки приводит и к копированию -всех символов строки в другой буфер. +### Mutability / immutability +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. +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** – -“Small String Optimization”, когда объект строки содержит внутри себя небольшой буфер и символы коротких строк -располагаются прямо в нём. -Но это уже зависит от реализации: в одних библиотеках помещают в объект строки до 15 байт, в некоторых до 23. -Однако эта оптимизация тоже палка о двух концах, и может в различных реализациях усложнить перемещение строки - если она хранит -указатель на свой внутренний буфер, его придётся корректировать. +Naturally, each new buffer requires memory allocation, which they are trying to slightly optimize through **SSO** – +“Small String Optimization”, when the string object contains a small buffer inside itself and the characters of short strings +are located directly in it. +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 - const char* text1 = "Hello, World";         // ничего не стоит - std::string_view text2 = "Hello, World";    // Ничего не стоит, вычисляет длину строки при компиляции - std::string text3 = "Hello, World";         // В рантайме каждый раз копирует символы строки + const char* text1 = "Hello, World";         // costs nothing + std::string_view text2 = "Hello, World";    // Costs nothing, calculates the length of the string at compile time + 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 ) -Но если нам дальше по коду не нужно никак модифицировать строку, мы зря платим за аллокацию, копирование символов, -а также за деструктор строки. То есть хотелось бы иметь как минимум два варианта строк — мутабельные и иммутабельные, -чтобы явно дать понять компилятору, что мы не собираемся модифицировать строку. -Или банальный пример — мы парсим какой-то входящий буфер данных, нам нужно проверить, равен ли некий кусок буфера строке -”hello” на «чистом С++», т. е. без всяких memcmp и strcmp. До появления string_view приходилось делать примерно так: +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" 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 bool is_part_buffer_equal_hello(const char* data, int start, int end) {     return std::string(data + start, end - start) == "hello"; } ``` -Тут получается, сначала копируются символы из буфера data в буфер временной строки, возможно с аллокацией памяти, и лишь -потом временная строка сравнивается с ”hello”, а потом ещё и деструктор и раскрутка стека на случай исключения. +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 +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 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 +However, the generated machine code is significantly transformed, reaching the level of manual C-code – there it is simply compared +that end – start == 5 and then a piece of the initial buffer is compared via memcmp with the string "hello" +(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. +You can verify this at 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&`. +It would seem, well, `string_view` appeared in C++17, please, use it in the parameters of your functions instead of `const std::string&`, +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 +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 +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 +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 #include void some_outer_function(const std::string&); @@ -127,9 +128,9 @@     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 #include void some_outer_function(const std::string&); @@ -145,180 +146,180 @@ } ``` -К сожалению, пока ни один компилятор не оптимизирует первый простой код до уровня второго более оптимального кода, а писать -такой код каждый раз руками довольно неудобно. То есть опять приходится платить за то, чем не пользуешься. -Да и в этом случае вполне может возникнуть оверхед по памяти — операции добавления строки обычно во всех реализациях увеличивают -размер буфера строки не меньше, чем в два раза, считая, что скоро к строке могут снова что-нибудь добавить. -Поэтому если строку не планируется более модифицировать, но время её жизни ещё не подошло к концу (например, это поле -какого-либо класса), нужно ещё не забыть сделать на ней `shrink_to_fit`. +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 +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`. -- Строки подразумеваются мутабельными, что приводит к обязательному копированию всех символов строки при инициализации и - копировании объектов строк. -- Соответственно, не имеем возможности быстрого копирования строк, даже если не планируем потом менять копию. -- Конкатенация нескольких строк — задача, могущая выполнятся неоптимально, приводить к оверхеду по памяти, написать оптимальный код сложно. -- Есть костыль для иммутабельных строк в виде `std::string_view`, однако он не решает вопросы владения строкой, поэтому по сути - годится только как тип для передачи параметров в функции, не меняющие строки, с оговоркой, что не может использоваться в функциях, - вызывающих C-API, так как не даёт гарантий нуль-терминированности. -- Ну и к `std::string` есть вопросы, что несмотря на то, что это класс для строк, собственно для работы со строками в нём крайне куцый - функционал по сравнению с тем, к чему привыкли в других языках — к примеру нет замены подстрок по шаблону (в других языках это обычно - replace, но в С++ эта функция делает совершенно другое), trim, split, join, upper, lower и т. п. - Эти функции приходится каждый раз писать самому, и не факт, что у всех это получится оптимально. +- "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. +- 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 + calling C-API, since it does not guarantee null-termination. +- 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, 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 годах, когда у нас уже появилась семантика перемещения, но ещё -не было std::string_view. Однако сейчас минимальная версия стандарта для работы библиотеки: **C++20** – используются концепты и \. +I started developing the library itself little by little back in 2011-2012, when we already had move semantics, but not yet +there was std::string_view. However, now the minimum standard version for the library to work is: **C++20** – concepts and \ 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. -Несколько общих моментов: -- Все классы для работы со строками шаблонизированы типом символов, но подразумевается, что символы могут быть char, char16_t, +Several general points: +- 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. -- Все строки имеют явную длину. -- Классы владельцы строк хранят их с завершающим нулем в конце, который не входит в длину строки. -- В самой строке могут содержаться нулевые символы, все алгоритмы работают только через длину строки, не обращая на них внимания. -- Классы владельцы строк могут инициализироваться строками другого типа символов, выполняя конвертацию между UTF-8, UTF-16, UTF-32. -- Для смены регистра символов и сравнения строк без учёта регистра используются встроенные таблицы для первой плоскости юникода - (до 0xFFFF). Строки считаются представленными в кодировке UTF-8, UTF-16, UTF-32 соответственно. - Однако не делается нормализация строк и не обрабатываются ситуации, когда смена регистра символа приводит к изменению их количества. - То есть преобразование регистра символов соответствует `std::towupper`, `std::towlower` для unicode локали, - только быстрее и может работать с любым видом символов. - Если вам нужна строгая работа с юникодом, используйте другие средства, например ICU. +- 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. +- 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 + (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. + 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. + 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) -Класс просто представляет собой указатель на начало константной строки и её длину, по сути то же самое, что `std::string_view`. -Предназначен для работы с иммутабельными строками, не владеющий ими, то есть вы должны сами озаботиться тем, что реальная строка, -представленная через `simple_str` – жива во время его использования. +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, +represented through `simple_str` – is alive during its use. -Реализует все строковые методы, не модифицирующие строку. +Implements all string methods that do not modify the string. -Алиасы: -- `ssa` для simple_str\ -- `ssu` для simple_str\ -- `ssw` для simple_str\ -- `ssuu` для simple_str\ +Aliases: +- `ssa` for simple_str\ +- `ssu` for simple_str\ +- `ssw` for simple_str\ +- `ssuu` for simple_str\ -Применяется в основном для передачи строк как параметр функций, не модифицирующих переданную строку, вместо `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) -По устройству и назначению совпадает с `simple_str`, но дает гарантии нуль-терминированности строки. -То есть если функции надо переданный параметр без изменений передать дальше как C-строку в какое то API, она должна использовать для -параметра тип `simple_str_nt`. -Все классы владеющих строк (simstr::sstring, simstr::lstring) могут быть преобразованы в `simple_str_nt`, так как хранят строки с завершающим нулём. -Это позволяет писать функции с единым типом параметра, принимающим на вход любой тип владеющих строковых объектов. +In terms of structure and purpose, it coincides with `simple_str`, but guarantees null-termination of the string. +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` type for the parameter. +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. -Алиасы: -- `stra` для simple_str_nt\ -- `stru` для simple_str_nt\ -- `strw` для simple_str_nt\ -- `struu` для simple_str_nt\ +Aliases: +- `stra` for simple_str_nt\ +- `stru` for simple_str_nt\ +- `strw` for simple_str_nt\ +- `struu` for simple_str_nt\ -Может инициализироваться строковыми литералами: +Can be initialized with string literals: ```cpp 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 stringa result = "Count: "_ss + count; ``` -#### Класс sstring (shared string). +#### Sstring class (shared string). (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. -Владеет строкой, управляет памятью для символов строки. -Хранит со строками завершающий нуль, и может быть источником для `simple_str_nt`, для передачи в C-API. -Так же, как и `simple_str`, реализует все методы, не модифицирующие строку. +Owns the string, manages the memory for the characters of the string. +Stores a trailing zero with the strings, and can be a source for `simple_str_nt`, for passing to C-API. +Like `simple_str`, it implements all methods that do not modify the string. -Алиасы: -- `stringa` для sstring\ -- `stringu` для sstring\ -- `stringw` для sstring\ -- `stringuu` для sstring\ +Aliases: +- `stringa` for sstring\ +- `stringu` for sstring\ +- `stringw` for sstring\ +- `stringuu` for sstring\ -То, что хранимая строка иммутабельна, позволяет применить ряд оптимизаций: -- Для строк, не подходящих для SSO, использует общий разделяемый буфер с атомарным счётчиком ссылок. - Позволяет быстро копировать строку без необходимости блокировок доступа к содержимому буфера. -- Нет необходимости хранить размер буфера (capacity) — всё равно мы ничего не дописываем в буфер. -- Позволяет просто ссылаться на литералы программы, не копируя их символы в какой-либо буфер: +The fact that the stored string is immutable allows you to apply a number of optimizations: +- 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. +- 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 - stringa str = "Hello!"; // Ничего не стоит, не копирует байты строки - stringa ltr = stra{"Hello!"}; // А вот тут копирует байты строки в ltr + stringa str = "Hello!"; // Costs nothing, does not copy the bytes of the string + 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 бит: -- `stringa` – класс 24 байта, SSO до 23 символов. -- `stringu` – класс 32 байта, SSO до 15 символов. -- `stringuu` – класс 32 байта, SSO до 7 символов. +For 64 bits: +- `stringa` – class 24 bytes, SSO up to 23 characters. +- `stringu` – class 32 bytes, SSO up to 15 characters. +- `stringuu` – class 32 bytes, SSO up to 7 characters. -Для 32 бит: -- `stringa` – класс 16 байт, SSO до 15 символов. -- `stringu` – класс 24 байта, SSO до 11 символов. -- `stringuu` – класс 24 байта, SSO до 5 символов. +For 32 bits: +- `stringa` – class 16 bytes, SSO up to 15 characters. +- `stringu` – class 24 bytes, SSO up to 11 characters. +- `stringuu` – class 24 bytes, SSO up to 5 characters. -#### Класс lstring (local string) +#### Class lstring (local string) (simstr::lstring) -Класс, хранящий строку и позволяющий её модифицировать. -Владеет строкой, управляет памятью для символов строки. -Хранит со строками завершающий нуль, и может быть источником для `simple_str_nt`, для передачи в C-API. -Как и все остальные классы, реализует все методы, не модифицирующие строку. +A class that stores a string and allows it to be modified. +Owns the string, manages the memory for the characters of the string. +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` в параметре шаблона задаётся размер внутреннего буфера для хранения символов. -Строки длиной до N символов хранятся внутри объекта, а при превышении этого количества — аллоцируется динамический буфер, -в который сохраняются символы. При копировании объекта все символы также всегда копируются. +The size of the internal buffer for storing characters is specified as `N` in the template parameter. +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 и символы не помещаются в локальный буфер, то динамический буфер создается с дополнительным местом, -так чтобы совпадать по структуре с буфером `sstring`. Тогда при перемещении `lstring` в `sstring` – переместится только указатель -на буфер, без излишнего копирования символов. +If `forShare` == true and the characters do not fit into the local buffer, then a dynamic buffer is created with additional space, +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. -Алиасы: -- `lstringa` для lsrting\ -- `lstringu` для lsrting\ -- `lstringw` для lsrting\ -- `lstringuu` для lsrting\ -- `lstringsa` для lsrting\ -- `lstringsu` для lsrting\ -- `lstringsw` для lsrting\ -- `lstringsuu` для lsrting\ +Aliases: +- `lstringa` for lsrting\ +- `lstringu` for lsrting\ +- `lstringw` for lsrting\ +- `lstringuu` for lsrting\ +- `lstringsa` for lsrting\ +- `lstringsu` for lsrting\ +- `lstringsw` for lsrting\ +- `lstringsuu` for lsrting\ -Небольшой пример использования с пояснениями: +A small example of use with explanations: ```cpp #ifdef _WIN32 const char path_separator = '\\'; @@ -329,13 +330,13 @@ auto get_current_dir() { #ifdef _WIN32 -     /* заполняем буфер wchar_t строки lstringw из GetCurrentDirectoryW с возможным - увеличением буфера и конвертируем в ut8 char. В конструкторе используется то, что появилось - только в С++23 как `resize_and_overwrite`, а у нас было изначально :) */ +     /* fills the buffer of the wchar_t string lstringw from GetCurrentDirectoryW with possible + increasing the buffer and converting to ut8 char. The constructor uses what appeared + only in C++23 as `resize_and_overwrite`, and we had it originally :) */     lstringa path{lstringw{ [](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 { char* data; size_t length; @@ -347,11 +348,11 @@     wchar_t buffer[MAX_PATH + 1], *buf = buffer;     DWORD size = sizeof(buffer) / sizeof(wchar_t), lengthOfpath;     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);         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             lenOfpath = ret;             break; @@ -371,14 +372,14 @@     lstringa path{ [](char* p, size_t s) {         const char* res = getcwd(p, s + 1);         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 0ul;     }}; #endif - // Удостоверимся, что строка будет заканчиваться разделителем директорий + // Let's make sure that the string will end with a directory separator     if (!path.length() || path.at(-1) != path_separator) {         path += e_c(1, path_separator);     } @@ -388,37 +389,37 @@ stringa build_full_path(ssa fileName) {     return get_current_dir() + fileName + ".txt";     /* -     Здесь сначала на стеке создастся временный объект lstringa для вызова get_current_dir. -     Функция get_current_dir заполнит его названием текущего каталога. -     В 99.9% случаев для этого хватит локального буфера на стеке. -     После рассчитывается общая длина для результата: длина current_dir + длина fileName + 4. -     Определяется буфер для строки конечного результата - если длина меньше 24 — строка будет размещена прямо в stringa, -     иначе аллоцируется буфер для результирующей строки сразу нужного размера. -     Затем в буфер результирующей строки последовательно копируются символы из current_dir, file_name, ".txt"; -     Ну и благодаря RVO - место для самого результата (stringa) - отводится в вызывающей функции, -     то есть никакого дополнительного копирования при возврате не будет. +     Here, a temporary lstringa object will first be created on the stack to call get_current_dir. +     The get_current_dir function will fill it with the name of the current directory. +     In 99.9% of cases, the local buffer on the stack will be enough for this. +     After that, the total length for the result is calculated: the length of current_dir + the length of fileName + 4. +     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. +     Then the characters from current_dir, file_name, ".txt" are sequentially copied to the buffer of the resulting string; +     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), - или одна, если результирующая строка длиннее 23 символов, при этом эта аллокация будет сразу нужного размера. +     Thus, there will be a maximum of only two memory allocations (if current_dir does not fit into MAX_PATH), + 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`. -Функция `length` – возвращает длину строки, функция `place` – помещает символы строки в переданный ей буфер. +A *string expression* is any object of arbitrary type that has `length` and `place` functions. +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) может инициализироваться строковым выражением — она запрашивает у него длину, -выделяет место для хранения символов, и передает это место строковому выражению, вызывая его функцию place. +Any owning string (simstr::sstring, simstr::lstring) can be initialized with a string expression — it requests its length, +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 template 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 template B> struct strexprjoin { @@ -441,86 +442,83 @@     constexpr symb_type* place(symb_type* p) const noexcept { return b.place(a.place(p)); } }; ``` -Таким образом, операция сложения строковых выражений создает объект, также являющийся строковым выражением, -к которому также может быть применена следующая операция сложения, и который рекурсивно хранит ссылки на слагаемые части, -каждая из которых знает свой размер и умеет размещать себя в буфере результата. И так далее, к каждому получаемому -строковому выражению можно снова применить `operator +`, формируя цепочку из нескольких строковых выражений, -и в итоге "материализовать" последний получившийся объект, который сначала посчитает размер всей общей памяти для -конечного результата, а затем разместит вложенные подвыражения в один буфер. +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 +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 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<ТипСимвола, КоличествоСимволов, Символ = ' '>{} -Выдает строку длиной КоличествоСимволов, заполненную заданным символом. Количество символов и символ - константы времени -компиляции. Для некоторых случаев есть сокращенная запись: - - e_spca(КоличествоСимволов) - строка char пробелов - e_spcw(КоличествоСимволов) - строка w_char пробелов +#### expr_spaces{} +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: -#### expr_pad<ТипСимвола>{КоличествоСимволов, Символ = ' '} -Выдает строку длиной КоличествоСимволов, заполненную заданным символом. -Количество символов и символ могут задаваться в рантайме. Сокращенная запись: + e_spca(NumberOfCharacters) - string of char spaces + e_spcw(NumberOfCharacters) - string of w_char spaces - e_c(КоличествоСимволов, Символ) +#### expr_pad{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(NumberOfCharacters, Symbol) #### 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) -Если Condition == true, результат будет равен StrExpr1, иначе пустая строка. +If Condition == true, the result will be StrExpr1, otherwise an empty string. -#### expr_num<ТипСимвола>(ЦелоеЧисло) -Конвертирует число в десятичное представление. Редко используется, так как для строковых выражений и чисел -переопределен оператор "+", и число можно просто написать как `text + number`; +#### expr_num(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`; -#### expr_real<ТипСимвола>(ВещественноеЧисло) -конвертирует число в десятичное представление. Редко используется, так как для строковых выражений и чисел -переопределен оператор "+", и число можно просто написать как `text + number`; +#### expr_real(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`; -#### e_join(контейнер, "Разделитель") -Конкатенирует все строки в контейнере, используя разделитель. Если ПослеПоследнего == true, -то разделитель добавляется и после последнего элемента контейнера, иначе только между элементами. -Если ТолькоНеПустые == true, то пустые строки пропускаются без добавления разделителя. +#### e_join(container, "Separator") +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. +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{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<ТипСимвола> -Выдает пустую строку. Сокращённая запись — eea, eeu, eew, eeuu. Применяется если формирование строки начинается с числа и строкового литерала: +#### empty_expr +Returns an empty string. Abbreviated notation — eea, eeu, eew, eeuu. Used if the string formation starts with a number and a string literal: ```cpp str = eea + count + " times."; ``` -так как оператор сложения определён только для сложения строкового выражения и числа. -Также замечу, что существует `operator""_ss`, который превращает строковый литерал в объект `simple_str_nt`, который уже является строковым выражением: +since the addition operator is only defined for adding a string expression and a number. +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 str = "Count = "_ss + count; ... str = count + " times."_ss; ``` -#### Свои строковые выражения -Вы можете сами создавать свои типы строковых выражений для оптимального формирования строк в нужных вам целях и алгоритмах. -Для этого просто создайте тип с методами `length`, `place` и `typename symb_type`. -Примеры создания и использования из реальных проектов: +#### Your own string expressions +You can create your own string expression types to optimally form strings for your specific purposes and algorithms. +To do this, simply create a type with `length`, `place` and `typename symb_type` methods. +Examples of creation and use from real projects: ```cpp -/* Сформировать строку в JSON формате, в 16 битных символах */ +/* Form a string in JSON format, in 16-bit characters */ struct expr_json_str {     using symb_type = u16s;     ssu text; @@ -607,7 +605,7 @@ inline u16s* expr_json_str::place(u16s* ptr) const noexcept { } ``` -Использование: +Usage: ```cpp ........ @@ -617,9 +615,9 @@ vtText << uR"({"#type":"jxs:string","#value":")" + expr_json_str(name) + u"\"}"; ....... ``` -Ещё пример +Another example ```cpp -/* Нужно сформировать бинарные данные в BASE64 формате, в 16 битных символах */ +/* Need to form binary data in BASE64 format, in 16-bit characters */ struct expr_str_base64 {     using symb_type = u16s;     ssa text; @@ -660,7 +658,7 @@ inline u16s* expr_str_base64::place(u16s* ptr) const noexcept { } ``` -Использование: +Usage: ```cpp ...... chunked_string_builder vtText; @@ -669,10 +667,10 @@ vtText << u"{\"#\",87126200-3e98-44e0-b931-ccb1d7edc497,{1,{#base64:" + expr_str ...... ``` -И ещё +And more ```cpp -/* Нужно преобразовать tm в строку даты/времени в 16-битных символах */ +/* Need to convert tm to a date/time string in 16-bit characters */ struct expr_str_tm {     using symb_type = u16s;     const tm& t; @@ -685,11 +683,11 @@ struct expr_str_tm { inline u16s* expr_str_tm::place(u16s* ptr) const noexcept {     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, t.tm_hour, t.tm_min, t.tm_sec);     } else { -        // Сначала форматнём в промежуточный буфер, потом скопируем в результат +        // First, format into an intermediate buffer, then copy to the result         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); @@ -701,7 +699,7 @@ inline u16s* expr_str_tm::place(u16s* ptr) const noexcept { } ``` -Использование +Usage ```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 -Предназначен для конкатенации множества строк. -Когда вам нужно последовательно формировать длинный текст из множества небольших кусочков (например, формируете html ответ -и т. п.) - последовательно складывать всё в один строковый объект крайне неоптимально — будет много переаллокаций и -перекопирования уже накопленных символов. В этом случае удобно использовать chunked_string_builder — всё, что он умеет, -это прибавлять строку к накопленным символам. Однако делает он это не в единый последовательный буфер памяти, а в отдельные -буфера, не меньше чем заданное выравнивание. При заполнении очередного буфера он просто создает ещё один буфер и продолжает -складывать данные в него. +### Class chunked_string_builder -То есть допустим вы задали выравнивание 1024. -Добавили несколько строк, заполнили буфер на 100 символов. И добавляете строку длинной 3000 символов. -При этом 924 символа скопируются в первый буфер, заполнив его до конца. -Для оставшихся 2076 создастся буфер размером 3072 символа, и они скопируются в него, в нём останется место для 996 символов. -Так последовательно каждый буфер заполняется до конца, и имеет размер кратный заданному выравниванию. -Таким образом избегаются переаллокации и перекопирование обработанных символов. +Designed for concatenating multiple strings. +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 +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. -После окончательного заполнения вы можете работать с накопленными данными — либо слить все буфера в одну последовательную -строку (размер для буфера которой вы теперь уже знаете), либо перебирать их по отдельности, например, посылая эти буфера -в сеть. Либо последовательно копируя данные в буфер заданного размера. +That is, suppose you set the alignment to 1024. +Added several strings, filled the buffer with 100 characters. And you add a string of 3000 characters long. +In this case, 924 characters will be copied to the first buffer, filling it to the end. +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. diff --git a/docs/overview_ru.md b/docs/overview_ru.md new file mode 100644 index 0000000..d804244 --- /dev/null +++ b/docs/overview_ru.md @@ -0,0 +1,749 @@ +# Строки в С++ +(, что с вами не так?) + +[On English|По-английски](overview.md) + + +В ретроспективе 1991 года по истории C++ его создатель Бьярне Страуструп назвал отсутствие стандартного строкового типа +(и некоторых других стандартных типов) в C++ 1.0 худшей ошибкой, которую он допустил при его разработке: +"the absence of those led to everybody re-inventing the wheel and to an unnecessary diversity in the most fundamental classes" +(«Их отсутствие привело к тому, что все заново изобретали велосипед, и к ненужному разнообразию в самых фундаментальных классах»). + + +## Что было и есть +Во вступительной части я хочу немного описать, каково ныне состояние со строками в С++, как мы к нему докатились и почему оно таково. +А также описать недостатки текущих реализаций, чтобы были понятны решения, которые я использую в своей библиотеке строк. + +Собственно, изначально как такового стандартного типа для строк в С++ не было. +Для работы со строками использовался подход из C – строка есть указатель на массив байтов, оканчивающихся нулём. +Недостатки таких строк — невозможно в строке использовать байт `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 + 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 + 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** – используются концепты и \. + +Сначала я расскажу о классах библиотеки для самих строк, а потом о том, как в ней оптимально решается задача конкатенации строк. + +Несколько общих моментов: +- Все классы для работы со строками шаблонизированы типом символов, но подразумевается, что символы могут быть 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\ +- `ssu` для simple_str\ +- `ssw` для simple_str\ +- `ssuu` для simple_str\ + +Применяется в основном для передачи строк как параметр функций, не модифицирующих переданную строку, вместо `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\ +- `stru` для simple_str_nt\ +- `strw` для simple_str_nt\ +- `struu` для simple_str_nt\ + +Может инициализироваться строковыми литералами: +```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\ +- `stringu` для sstring\ +- `stringw` для sstring\ +- `stringuu` для sstring\ + + +То, что хранимая строка иммутабельна, позволяет применить ряд оптимизаций: +- Для строк, не подходящих для 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 (local string) +(simstr::lstring) + +Класс, хранящий строку и позволяющий её модифицировать. +Владеет строкой, управляет памятью для символов строки. +Хранит со строками завершающий нуль, и может быть источником для `simple_str_nt`, для передачи в C-API. +Как и все остальные классы, реализует все методы, не модифицирующие строку. + +В качестве `N` в параметре шаблона задаётся размер внутреннего буфера для хранения символов. +Строки длиной до N символов хранятся внутри объекта, а при превышении этого количества — аллоцируется динамический буфер, +в который сохраняются символы. При копировании объекта все символы также всегда копируются. + +Если `forShare` == true и символы не помещаются в локальный буфер, то динамический буфер создается с дополнительным местом, +так чтобы совпадать по структуре с буфером `sstring`. Тогда при перемещении `lstring` в `sstring` – переместится только указатель +на буфер, без излишнего копирования символов. + +Этот класс удобен для работы со строками как локальная переменная на стеке. +Обычно мы предполагаем примерный размер строк, с котороми будем работать, и можем создать локальную строку с буфером на стеке, +и работать с ней. При этом не опасаясь переполнения буфера, так как в этом случае строка переключится на динамический буфер. + +Алиасы: +- `lstringa` для lsrting\ +- `lstringu` для lsrting\ +- `lstringw` для lsrting\ +- `lstringuu` для lsrting\ +- `lstringsa` для lsrting\ +- `lstringsu` для lsrting\ +- `lstringsw` для lsrting\ +- `lstringsuu` для lsrting\ + + +Небольшой пример использования с пояснениями: +```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 из GetCurrentDirectoryW с возможным + увеличением буфера и конвертируем в ut8 char. В конструкторе используется то, что появилось + только в С++23 как `resize_and_overwrite`, а у нас было изначально :) */ + +     lstringa path{lstringw{ [](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 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 для вызова 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 B> + inline auto operator + (const A& a, const B& b) { +     return strexprjoin{a, b}; + } +``` + +`strexprjoin` – шаблонный тип, который сам является строковым выражением. +В себе он хранит ссылки на два переданных ему строковых выражения. +При запросе длины он выдает сумму длин двух строковых выражений, а при размещении символов — сначала размещает +в переданном буфере первое выражение, затем второе. +```cpp + template 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(контейнер, "Разделитель") +Конкатенирует все строки в контейнере, используя разделитель. Если ПослеПоследнего == 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::copy(ptr, r, lenOfTail); +        ptr += lenOfTail; +    } +    return ptr; +} +``` + +Использование: + +```cpp +........ +chunked_string_builder 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 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 символов. +Так последовательно каждый буфер заполняется до конца, и имеет размер кратный заданному выравниванию. +Таким образом избегаются переаллокации и перекопирование обработанных символов. + +После окончательного заполнения вы можете работать с накопленными данными — либо слить все буфера в одну последовательную +строку (размер для буфера которой вы теперь уже знаете), либо перебирать их по отдельности, например, посылая эти буфера +в сеть. Либо последовательно копируя данные в буфер заданного размера. diff --git a/for_debug/readme.md b/for_debug/readme.md index a25de36..b754951 100644 --- a/for_debug/readme.md +++ b/for_debug/readme.md @@ -1,14 +1,16 @@ -# Объекты simstr в отладчиках -Для более удобного отображения объектов simstr в отладчиках msvc и gdb подготовлено два файла: -simstr.natvis - для использования в отладчике MSVC, и simstr_pretty_print.py для работы с gdb. +# simstr Objects in Debuggers +[On Russian|По-русски](readme_ru.md) -# Объекты simstr в отладчиках -Если вы работаете в MS Visual Studio simstr.natvis автоматически добавляется в pdb файл, -и обеспечивает удобный просмотр строковых объектов simstr везде, где используется эта библиотека. +Two files have been prepared for more convenient display of simstr objects in msvc and gdb debuggers: +simstr.natvis - for use in the MSVC debugger, and simstr_pretty_print.py for working with gdb. -# Объекты simstr в Visual Studio Code -## При работе в gdb -В конфигурации отладчика необходимо добавить следующие строки: +# simstr Objects in Debuggers +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": [ @@ -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`. -Возможно, у вас также установлено расширение `CMake Tools`, которое позволяет выбирать целевой проект для запуска. -В этом случае настройки для подключения скрипта прописываются в `.vscode/settings.json`: +Debugger configurations are located in the `.vscode/launch.json` file. +You may also have the `CMake Tools` extension installed, which allows you to select the target project to run. +In this case, the settings for connecting the script are written in `.vscode/settings.json`: ``` "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": [ { diff --git a/for_debug/readme_ru.md b/for_debug/readme_ru.md new file mode 100644 index 0000000..2960da3 --- /dev/null +++ b/for_debug/readme_ru.md @@ -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" + }, + ... +``` diff --git a/include/simstr/strexpr.h b/include/simstr/strexpr.h index 9892a77..678a4fc 100644 --- a/include/simstr/strexpr.h +++ b/include/simstr/strexpr.h @@ -12,7 +12,8 @@ #include /*! - * @brief Пространство имён для объектов библиотеки + * @ru @brief Пространство имён для объектов библиотеки + * @en @brief Library namespace */ namespace simstr { @@ -145,15 +146,25 @@ public: }; /*! - * @brief Базовая концепция строкового объекта. - * @tparam A - проверяемый тип - * @tparam K - тип символов - * @details В библиотеке для разных целей могут использоваться различные типы объектов строк. + * @ru @brief Базовая концепция строкового объекта. + * @ru @tparam A - проверяемый тип + * @ru @tparam K - тип символов + * @ru @details В библиотеке для разных целей могут использоваться различные типы объектов строк. * Мы считаем строковым объектом любой объект, поддерживающий методы: * - `is_empty()`: возвращает, пуста ли строка. * - `length()`: возвращает длину строки без нулевого терминатора. * - `symbols()`: возвращает указатель на строку символов. * - `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 concept StrType = requires(const A& a) { diff --git a/readme.md b/readme.md index ee5ceef..f56beef 100644 --- a/readme.md +++ b/readme.md @@ -1,94 +1,96 @@ -# simstr - библиотека строковых объектов и функций +# simstr - String object and function library [![CMake on multiple platforms](https://github.com/orefkov/simstr/actions/workflows/cmake-multi-platform.yml/badge.svg)](https://github.com/orefkov/simstr/actions/workflows/cmake-multi-platform.yml) -Версия 1.2.4. +Version 1.2.4. -В этой библиотеке содержится реализация нескольких видов строковых объектов и различных алгоритмов для работы со строками. +[On Russian|По-русски](readme_ru.md) -Цель библиотеки - сделать работу со строками в С++ такой же простой и лёгкой, как во множестве других языков, особенно -скриптовых, но при этом сохранив оптимальность и производительность на уровне С и C++, и даже улучшив их. +This library contains the implementation of several types of string objects and various algorithms for working with strings. -Не секрет, что работа со строками в С++ зачастую доставляет боль. Класс `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. -Библиотека не претендует на роль "поменял хедер и всё заработало лучше". Многие методы я старался делать совместимыми -с `std::string` и `std::string_view`, но особо с этим не заморачивался. Переписывание старого кода на работу с simstr -потребует некоторых усилий, но уверяю, что они окупятся. А новый код писать с её применением легко и доставляет удовольствие :) +This library was not made as a universal combine that "can do everything", I implemented what I had to +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 - для работы со строками используется не единый универсальный класс, а несколько -видов объектов, каждый из которых хорош для своих целей, и при этом хорошо взаимодействующих друг с другом. -Если вы активно использовали std::string_view и понимали, в чём его преимущество и недостатки по сравнению с std::string, -то подход simstr вам также будет понятен. +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 +will require some effort, but I assure you that it will pay off. And writing new code with its use is easy and enjoyable :) -## Основные возможности библиотеки -- Строки `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 (см. предыдущий пункт)). +The main difference between simstr and std::string is that instead of a single universal class, several +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. +If you actively used std::string_view and understood its advantages and disadvantages compared to std::string, +then the simstr approach will also be clear to you. -## Основные объекты библиотеки -- simple_str<K> - самая простая строка (или кусок строки), иммутабельная, не владеющая, аналог `std::string_view`. -- simple_str_nt<K> - то же самое, только заявляет, что заканчивается 0. Для работы со сторонними C-API. -- sstring<K> - shared string, иммутабельная, владеющая, с разделяемым буфером символов, поддержка SSO. -- lstring<K, N> - local string, мутабельная, владеющая, с задаваемым размером SSO буфера. +## Main features of the library +- Strings `char`, `char16_t`, `char32_t`, `wchar_t`. +- Transparent conversion of strings from one character type to another, with automatic conversion between UTF-8, UTF-16, UTF-32, + using [simdutf](https://github.com/simdutf/simdutf). +- 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)). -## Статьи -- [Обзор и введение](docs/overview.md) -- [Обзорная статья на Хабре](https://habr.com/ru/articles/935590) -- [Описание применяемой техники "Expression Templates"](https://habr.com/ru/articles/936468/) +## Main objects of the library +- simple_str<K> - the simplest string (or piece of string), immutable, not owning, analogue of `std::string_view`. +- simple_str_nt<K> - the same, only declares that it ends with 0. For working with third-party C-API. +- 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. -## Использование -`simstr` состоит из трёх заголовочных файлов и двух исходников. Можно подключать как CMake проект через `add_subdirectory` (библиотека `simstr`), -можно просто включить файлы в свой проект. Для сборки также требуется [simdutf](https://github.com/simdutf/simdutf) (при использовании CMake -скачивается автоматически). +## Articles +- [Overview and introduction](docs/overview.md) +- [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. -Работа проверялась под Windows на MSVC-19 и Clang-19, под Linux - на GCC-13 и Clang-21. -Также проверялась работа в WASM, сборка в Emscripten 4.0.6, Clang-21. +## Usage +`simstr` consists of three header files and two source files. You can connect as a CMake project via `add_subdirectory` (the `simstr` library), +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. -## Бенчмарки -Бенчмарки производятся с использованием фреймворка [Google benchmark](https://github.com/google/benchmark). -Постарался сделать замеры для наиболее типичных операций, встречающихся в обычной работе. Я проводил замеры на своём оборудовании, под -Windows и Linux (в WSL), с использованием компиляторов MSVC, Clang, GCC. Сторонние результаты приветствуются. -Также проводил замеры в WASM, сборка в Emscripten. Обращаю внимание, что под WASM в Emscripten собирается 32-битная сборка, а значит, -размеры буферов SSO в объектах меньше. +## Benchmarks +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 and Linux (in WSL), using MSVC, Clang, GCC compilers. Third-party results are welcome. +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 +the sizes of SSO buffers in objects are smaller. -- [Исходный код бенчмарков](bench/bench_str.cpp) -- [Результаты бенчмарков](https://snegopat.ru/simstr/results.html) +- [Benchmark source code](bench/bench_str.cpp) +- [Benchmark results](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) +## Usage examples +While no separate usage examples have been prepared, you can look at the texts of [tests](tests/test_str.cpp), +[benchmarks](bench/bench_str.cpp), and [html preparation utilities](bench/process_result.cpp) from the benchmark results. +Also, simstr is used in my [v8sqlite](https://github.com/orefkov/v8sqlite) project -## Сгенерированная документация -[Находится здесь](https://snegopat.ru/simstr/docs/) +## Generated documentation +[Located here](https://snegopat.ru/simstr/docs/) diff --git a/readme_ru.md b/readme_ru.md new file mode 100644 index 0000000..b4c92a7 --- /dev/null +++ b/readme_ru.md @@ -0,0 +1,96 @@ +# simstr - библиотека строковых объектов и функций +[![CMake on multiple platforms](https://github.com/orefkov/simstr/actions/workflows/cmake-multi-platform.yml/badge.svg)](https://github.com/orefkov/simstr/actions/workflows/cmake-multi-platform.yml) + +Версия 1.2.4. + +[On English|По-английски](readme.md) + +В этой библиотеке содержится реализация нескольких видов строковых объектов и различных алгоритмов для работы со строками. + +Цель библиотеки - сделать работу со строками в С++ такой же простой и лёгкой, как во множестве других языков, особенно +скриптовых, но при этом сохранив оптимальность и производительность на уровне С и C++, и даже улучшив их. + +Не секрет, что работа со строками в С++ зачастую доставляет боль. Класс `std::string` часто неудобен либо неэффективен. +Многих функций, обычно необходимых при работе со строками, просто нет, и их каждому приходится писать самому. + +Эта библиотека не делалась как универсальный комбайн, который "может всё", я реализовывал то, что мне приходилось +использовать в работе, стараясь сделать это наиболее эффективным способом, и скромно надеюсь, что кое-что у меня получилось +и пригодится другим людям, либо напрямую, либо как источник идей. + +Библиотека не претендует на роль "поменял хедер и всё заработало лучше". Многие методы я старался делать совместимыми +с `std::string` и `std::string_view`, но особо с этим не заморачивался. Переписывание старого кода на работу с simstr +потребует некоторых усилий, но уверяю, что они окупятся. А новый код писать с её применением легко и доставляет удовольствие :) + +Основное отличие simstr от std::string - для работы со строками используется не единый универсальный класс, а несколько +видов объектов, каждый из которых хорош для своих целей, и при этом хорошо взаимодействующих друг с другом. +Если вы активно использовали std::string_view и понимали, в чём его преимущество и недостатки по сравнению с std::string, +то подход simstr вам также будет понятен. + +## Основные возможности библиотеки +- Строки `char`, `char16_t`, `char32_t`, `wchar_t`. +- Прозрачное преобразование строк из одного типа символов в другой, с автоматической конвертацией между UTF-8, UTF-16, UTF-32, + используя [simdutf](https://github.com/simdutf/simdutf). +- Расширяемая система "Строковых выражений". Позволяет эффективно реализовать преобразование и сложение (конкатенацию) строк, литералов, + чисел и возможно других объектов. +- Строковые функции: + - Получение подстрок. + - Поиск подстрок и символов - с начала или с конца строки. + - Различный тримминг строк - справа, слева, везде, по пробельным символам, по заданным символам. + - Замена подстрок. + - Замена набора символов на набор соответствующих подстрок. + - Слияние (join) контейнеров строк в единую строку, с заданием разделителей и опций - "пропускать пустые", "разделитель после последней". + - Разбиение (split) строк на части по заданному разделителю. Разбиение возможно сразу в контейнер со строками, либо вызовом функтора для + каждой подстроки, либо путем итерации с помощью итератора `Splitter`. +- Интеграция с функциями форматирования `format` и `sprintf` (с автоматическим увеличением буфера). + Форматирование возможно для строк `char`, `wchar_t` и строк, совместимых с `wchar_t` по размеру. + То есть под Windows это `char16_t`, под Linux - `char32_t`. Писать свою библиотеку форматирования не входило в мои замыслы. +- Парсинг целых чисел с возможностью "тонкой" настройки при компиляции - можно задавать опции проверки переполнения, + пропуск пробельных символов, конкретное основание счисления либо автовыбор по префиксам `0x`, `0`, `0b`, `0o`, + допустимость знака `+`. Парсинг реализован для всех видов строк и символов. +- Парсинг double пока реализован вызовом стандартной библиотеки и работает только для строк `char`, `wchar_t` и совместимых с + `wchar_t` по размеру типов. +- Содержится минимальная поддержка Unicode при преобразовании `upper`, `lower` и регистро-независимом сравнении строк. + Работает только для символов первой плоскости Unicode (до 0xFFFF), а при смене регистра не учитываются случаи, когда один code point + может преобразовываться в несколько, то есть преобразование регистра символов соответствует `std::towupper`, `std::towlower` для unicode локали, только быстрее и может работать с любым видом символов. +- Реализован `hash map` для ключей строкового типа, на базе `std::unordered_map`, с возможностью более эффективного хранения и + сравнения ключей по сравнению с ключами `std::string`. Поддерживается возможность регистро-независимого сравнения ключей (Ascii или + минимальный Unicode (см. предыдущий пункт)). + +## Основные объекты библиотеки +- simple_str<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/)