group-telegram.com/cxx95/97
Last Update:
#madskillz
Нестандартные представления строк
В "стандартном" C++ есть три основных представления для строк. Не будем учитывать "составные" классы (как std::stringstream
), у которых нет уникальных концепций.
=====
const char*
- просто указатель на начало строки где-то в памяти. Обычно если итерироваться по указателю, то когда-то достигнем нулевой байт \0
(нуль-терминатор), который указывает на конец строки. Все строковые алгоритмы Си завязаны на признак \0
как на конец строки.
=====
std::string
- класс строки, владеющий памятью для нее в куче. Запись std::string s = "abcd";
значит, что где-то в куче занята память под байты abcd\0
. Известно, std::string
гарантированно нуль-терминирован (начиная с C++11).
std::string_view
- класс строки, не владеющий памятью. Представляет собой пару const char* s
(начало строки) и size_t len
(длину строки).
Не обязательно верно то, что *(s + len) == '\0'
. std::string_view
указывает не на всю строку, а только на какую-то ее часть.
=====
Класс std::string
поведением похож на контейнер std::vector<char>
. Можно посмотреть на какие-нибудь неклассические контейнеры, чтобы создать новые строковые классы, которых нет в стандартном C++.
SmallString
- класс строки, владеющий памятью для нее, с поведением как у SmallVector<char>
. Реализован в LLVM.
Запись
std::string s1;Дает два объекта
SmallString<256> s2;
s1
и s2
, у которых одинаковый набор методов, но s2
хранится на стеке, если размер строки не превышает 256 символов (планируется, что так будет в 99.9% случаев). Если размер все-таки превысили, то строку начинают хранить в куче.=====
В бизнес-логике со строками есть проблема. Иногда в коде надо делать много составных строк. Например, для создания строки - "версии" программы нужно сложить несколько строк-частей:
Major + "." + Minor + "." + VersionPatchВ этом случае происходит создание 3 (!) лишних "временных" строк с аллокациями памяти, то есть делается строка
Major + "."
, потом строка (Major + ".") + Minor
и так далее. Более того, итоговая строка (4-я по счету) тоже по сути лишняя, если мы хотели сразу записать итог в какой-нибудь файл, а не хранить результат сложения.В кодовой базе LLVM есть решение, которое сложно для понимания, но мы его разберем:
Twine
- класс "сумма строк". Документация по Twine, но больше информации в исходнике.Трудности начинаются на уровне названия класса, как у не-носителя английского
Вообще, трудности сначала были со словом
string
. До того, как я начал программировать, у меня это ассоциировалось со стрингами, которые носил Борат. У этого слова куча значений, пусть в нашем случае это будет шнур
.Теперь посмотрим на слово
twine
. У него тоже вагон значений, пусть в нашем случае это будет бечёвка
, пнятненько?</конец бесполезного абзаца>
Этот класс опасный: он полагается на стремное правило Reference Lifetime Extension, а также на не менее стремное правило, что объекты, созданные для использования в full-expression, не удаляются до конца выполнения этого full-expression (сформулировал как смог).
Функция должна принимать
Twine
по константной ссылке:void foo(const Twine& T);А подавать туда Twine нужно не отходя от кассы, чтобы сработало правило RLE:
foo(Twine(Major) + "." + Minor + "." + VersionPatch);Благодаря правилу про full-expression, все составные части строки "живы" на стеке, пока не выполнится вызов
foo
.Twine
внутри себя выглядит как бинарное дерево. У него два "ребенка":Child LHS;Каждый ребенок это указатель на какой-нибудь строковой объект:
Child RHS;
const char
, или std::string
, или std::string_view
, или другой Twine
("поддерево"). Также для удобства поддерживаются числа union ChildПРОДОЛЖЕНИЕ В ПЕРВОМ КОММЕНТАРИИ (у телеграма ограничение по размеру постов
{
const Twine *twine;
const char *cString;
const std::string *stdString;
/* ... */
int decI;
/* ... */
};