
CryEngine2 использовал класс собственный CString для реализации работы со строками и немного использовал строки из стандартной строковой библиотеки Windows. Насколько я помню, последняя версия CryEngine всё ещё использует те же самые CString, она кардинально поменялась внутри, но как дань истории название класса менять не стали, зато сильно расширили функционал. Я не на 100% уверен, применялся ли CString только в редакторе или в рантайме игры тоже, вы можете сами это посмотреть в исходниках, которые все еще доступны на гитхабе. Это один подход к работе со строками, довольно распространенный в мире игростроя - когда мы все нужное пишем сами, не оглядываясь... хотя, тут больше уместно слово поглядывая, на существующие реализации и утаскивая в проект все самое лучшее.
Есть и другой подход... Я работал в команде над некоторым проектом, который должен был выйти на консолях, и в какой-то момент на проект пришел эффективный тимлид, который хорошо умел в красивые презентации, и продавил использование std::string из sdk. Все очень опытные программисты, синьоры и руководство важно кивали на совещании, и согласились всё перевести на std::string… не такие уж они оказались опытные, как выяснилось. В итоге мы заменили большую часть CString на std::string. Не сказал бы, что это сильно повлияло на время компиляции - плюс-минус минута к проекту, который собирается двадцать минут, особой погоды не делают, но это также превратило наш довольно понятный базовый код в запутанный кошмар. Возможно, для переносимости это было лучше, но ни наш проект, ни CryEngine2 Editor так и не были портированы ни на Linux, ни на какую-либо другую платформу.
Прошло десять лет, я вижу ровно туже ситуацию на текущем проекте - новый тимлид решил перевести местный MySuperPupeString на std::string, уже предчувствуя "нижней чуйкой" последствия - запасаюсь попкорном и беру отпуск на следующий месяц после принятия решения. Но не это интересно, а то - какие вообще строки могут быть в вашем с++ коде.
char*

Дикий волк с минимальным набором инстинктов — в виде указателя и терминатора '\0', которых ему более чем достаточно для выживания в диких линуксовых степях, андроидных лесах и вообще везде, где есть хоть какое-то подобие процессора. Вначале был BCPL, где типов не было вовсе. Это был первобытный мир, где все данные были одинаковыми зверями — или просто кусками памяти без паспортов и социального статуса. В 1969 году Кен Томпсон создал язык B для первой версии UNIX на PDP-7. Он тоже был бестипным — «C без типов», как его вспомнят позже. А уже в C Ритчи заложил гениальную идею: объявление переменной выглядит так же, как её использование в выражении. Если пишешь char str, то в коде используешь str для получения символа. На тот момент это была не абстракция (её назовут так позже), а прямое отражение того, как работает железо, и особенно память. Поэтому char* это вовсе не “строка”, а всего лишь указатель на байт данных в памяти. В этом смысле волк не думает о стаде овец как о массиве — он просто видит первую овцу и знает, что дальше наверняка пасутся ещё. Чтобы понять, где кончается еда… то есть строка, нужно лишь договориться о сигнале. В BCPL использовали длину в первом байте, и Pascal позже подхватил эту идею. Но в C выбрали концепцию '\0' специального нулевого байта на конце. Это было проще для железа: не нужно хранить лишнюю длину, просто иди по памяти, пока не встретишь ноль. Всё как в дикой природе - волк с собой счётчик овец не носит. Так же и strcpy(), strlen(): бегут по памяти, пока не ткнутся в '\0'.
Особенности содержания — низкая читабельность: ptr++, str**, strcpy(a, b), но зато набор команд минимален и эффективен, как охотничий инстинкт. Синтаксический сахар в виде строковых литералов "hello" и квадратных скобочек str[i] тоже появился не сразу, но всегда можно было обойтись суровой конструкцией (str + i). Нога откусывается максимально быстро: достаточно выйти за границы буфера, забыть терминатор или вставить его «не там», и почти всегда имеем segfault. Этот волк знает толк в извращениях, но именно на его магии построены целые горы директив препроцессора, макросов стрингификации вроде #define STR(x) #x и даже немалые куски автогенераторов кода.
char[N]

Когда Деннис Ритчи создал язык C, у него не было роскоши сложных абстракций, да и роскошь тогда вообще не водилась. Компьютеры PDP-11, на которых собирали первые версии UNIX, имели жалкие килобайты памяти, а malloc появился не сразу. Программисты работали с тем, что было - автоматические переменные на стеке и статические данные в сегменте data считались нормальным рационом. Массив символов char buf[N] был естественным способом хранить строку. Объявили массив, он тут же поселился на стеке, никаких аллокаций, никаких освобождений, никакой драматургии. Это была та самая черепаха, простое и древнее существо, появившееся миллионы лет назад и особо не желающее эволюционировать. Зачем эволюция, если всё и так работает.
В культуре UNIX, где программы были маленькими утилитами которые делают одну вещь хорошо, фиксированные буферы встречались повсюду. Чтение строк из stdin, парсинг конфигурации, обработка временных данных, везде стояли char buffer[BUFFER_SIZE] с магическими числами вроде 80, 256, 1024. Эти числа подбирались опытным путём, то есть так чтобы обычно хватало, но чтобы стек не взорвался.
Главная магия фиксированных массивов в том что они живут на стеке. Объявление char buf[N] резервирует байты прямо там и гарантирует что черепаха всегда дома. Она никуда не путешествует в далёкие земли кучи, где живут шумные и непредсказуемые существа, и где память освобождается тогда когда захочет. Черепаха просто сидит в своем уголке стека, делает свою работу и исчезает когда функция возвращается. Но даже у черепах есть ограничения - стек конечен, типичный размер от одного до восьми мегабайт на поток, и черепаха может жить только в небольшой коробке. Если коробка нужна слишком большая, придется идти в дикую степь динамических аллокаций, где живут другие звери, гораздо более нервные и прожорливые.
Этот сюжет напоминает что в программировании не всегда нужны мудрёные инструменты. Иногда простой массив на стеке решает задачу лучше чем умная динамическая строка с десятками глянцевых методов. Черепаха никогда не обгонит гепарда, но точно доползет до финиша предсказуемо и надёжно. В этом ее мудрость и ценность, простота которая работает уже пятьдесят лет и, как она сама считает, будет работать ещё столько же. Современные программисты иногда смотрят на такие решения с лёгкой улыбкой, полагая что мир давно шагнул вперёд и пора использовать продвинутые контейнеры, умные строки и автотипы. Но рано или поздно каждый сталкивается с ситуацией когда нужно что то маленькое и простое, как кусочек памяти, который живёт ровно столько сколько живёт функция и не оставляет за собой хвосты. И тогда появляется тот самый char buf[N] который выглядит как привет из семидесятых и при этом всё ещё идеально подходит для задачи.
char literal

Самой первой формой строк которую придумали в языке C был литерал (Строковая константа, строковый литерал, или просто литерал), просто последовательность символов в двойных кавычках. Написал "hello" в коде, компилятор аккуратно положил эти байты в исполняемый файл, а программа получила указатель на них. Это была рыбка которая поселилась в стеклянном аквариуме вместе с рождением языка, красивая, плавающая перед глазами, но трогать ее руками строго запрещено.
Литералы по своей природе являются константами, они компилируются в executable и помещаются в секцию .rodata, которую операционная система помечает как read only после загрузки программы. Попытка записать туда что-то вызывает немедленный segfault, рыбка моментально напоминает, что стекло пуленепробиваемое и ломать аквариум не надо.
Одной из ранних оптимизаций компиляторов стал string literal pooling. Если в коде несколько раз встречался одинаковый литерал, компилятор мог создать одну единственную копию в .rodata, а все указатели направить на нее. Написал "error" в десяти местах, а в бинарнике может оказаться всего один экземпляр. Все рыбки с надписью "error" на самом деле были той же самой рыбкой, которую разные участки программы рассматривали под разными углами. Стандарт это не запрещал, но и не гарантировал, и рассчитывать на то что два "hello" указывают на один адрес было занятием для особенно доверчивых романтиков.
Современные компиляторы делают с литералами куда более интересные штуки. Короткие строки могут вообще не попасть в .rodata, компилятор превращает их в набор инструкций и строка "ab" становится числом которое загружается в регистр одним движением. Строки средних размеров могут быть собраны прямо на стеке серией mov инструкций. Только действительно длинные строки живут в .rodata как полноценные аквариумные обитатели и копируются через memcpy.
Литералы живут вечно с точки зрения программы. Они создаются когда процесс загружается в память и исчезают только когда процесс завершается. Нельзя освободить память занимаемую литералом, нельзя переместить ее, нельзя изменить и адрес остается фиксированным всю жизнь процесса. Рыбка в аквариуме родилась в момент запуска и будет плавать там до самого конца. Рыбка сама о себе заботится - ее не нужно кормить, чистить, пересаживать или размножать.
В 2025 году спустя пять десятилетий после появления C, литералы остаются фундаментальной частью языка. Они не эволюционировали как другие строковые типы, они остались такими же прямолинейными как были. Рыбка в аквариуме не меняется поколениями, но делает свое дело идеально. Плавает за стеклом, радует глаз, ничего не требует и живет вечно.
std::string (C++03/11)

Когда появился C++ и std::string в восьмидесятых, это выглядело как попытка одомашнить дикого волка. Добавили RAII, методы, автоматическое управление памятью и получилось нечто вроде собаки. Надёжной, удобной, с добрыми глазами, но прожорливой и не такой быстрой в руках опытного охотника. А в руках новичка она легко превращалась в австралийского кролика который плодится без остановки и захламляет всю память до горизонта.
Бьярн Страуструп создавая C++ взял C как основу и прикрутил к нему классы, но со строками получилось неловко. Они не вписывались в концепцию языка и char* остался диким, как был. Программистам он откусывал ноги на полном ходу, устраивал buffer overflow, забытые нулевые байты и прочие радости жизни, присущие дедушке С. В самом языке строки не были оформлены никак, философия C++ предполагала, что язык даёт просто инструменты, а абстракции строят уже сторонние библиотеки. И это было милой ошибкой, ведь базовые сущности всё же должны определяться самим языком, иначе начинается зоопарк.
Зоопарк и начался. Каждая компания писала свою породу String и продвигала её где могла. У Microsoft был CString, у Borland AnsiString, у Qt QString и каждый уверял что именно его собака сидит правильнее других. Ну и конечно совместимость между этими "существами" была примерно как между пуделем и афганской борзой, т.е. в принципе можно - но зачем? И проблем стало столько, что Страуструп в ретроспективе истории C++ публично признал отсутствие стандартного строкового типа проблемой, но делать ничего не стал. Тогда сообщество в лице Александра Степанова и Мэн Ли принесли STL, как коллекцию контейнеров и алгоритмов, включая и basic_string как базу для стандартной строки. Это была революция, строка наконец стала контейнером вроде vector, только с особой семантикой, а не просто кучей данных внутри.
Однако скрещивание домашних и диких пород уже было не остановить и каждый фреймворк считал своим долгом перещеголять std::string хоть в чем-то, даже если успехи были сомнительные. Тем временем настоящие сишники до сих пор смотрят на std::string как на избалованную декоративную собачку, которой не место в суровой дикой природе где выживают только сильнейшие или может простейшие?
Читабельность у std::string прекрасная, пока вы не решите открыть реализацию в стандартной библиотеке. Даже простое сложение строк превращается в вызов переопределённого оператора плюс, а если хочется слегка пострелять себе в ногу, то хватает классических проблем с инвалидацией итераторов, внезапной реаллокацией при изменении данных или весёлой и порой непредсказуемой работой SSO, маленьким хвостом, который иногда виляет всей собакой.
std::string_view

Радость одомашнивания и приручения волков быстро испортилась одной мелкой неприятностью. Собаки начали пухнуть от переедания. К середине 2000-х годов std::string стал стандартом, но у собаки появилась серьёзная проблема. Она слишком любила кушать, а точнее копировать. Каждый раз когда строку передавали в функцию через const std::string& существовал риск, что внутри функции кто нибудь хитрый создаст новую строку из литерала для сравнения и обязательно устроит временную аллокацию. Собаки в очередной раз переедали, и страдала производительность
bool compare(const std::string& s1, const std::string& s2) {
return s1 == s2;
}
std::string str = "hello";
compare(str, "world"); // Создаёт временный std::string, аллокация
До появления стандартного string_view сообщество вовсю пыталось вывести породу худых собакенов. Так появилась boost::string_ref что было ранним пропозалом для невладеющих данными ссылок на строки. Как водится разные заводчики тут же начали скрещивать своих зверушек и выставлять напоказ. Например absl::string_view в библиотеке Abseil. В Qt завели собственный string_ref и еще отдельно CoW строки. Все решали одну и ту же проблему как бы не копировать когда нужен только read only доступ. Это были первые дикие кошки, которых никто толком не приручил, и каждая библиотека вырастила свою породу. В стандарт C++17 кошку наконец пустили официально в виде std::string_view. Она по большей части повторяла бустовскую реализацию. Кошка памятью не владеет вообще, она только смотрит на неё с высоты своей пушистой независимости
Преимущества кошки вполне очевидны - она не тащит еду домой, а ест там где нашла. И может есть любую рыбу, на которую покажут пальцем
std::string str = "this is my input string";
std::string_view sv(&str[11], 2); // "my" — без копирования
void print(std::string_view sv) {
std::cout << sv;
}
print("literal"); // OK
print(std::string("str")); // OK
print(some_char_ptr); // OK
string_view кошка пришла в плюсатый дом не затем чтобы выгнать собаку. Она просто показывает что иногда владение не нужно и достаточно просто посмотреть. Просто посмотреть это теперь вообще философия C++17 - меньше копирования, больше умных view типов и больше производительности через zero cost абстракции и мелкие хаки. Синтаксис вполне читабельный и учебники по современному C++ тоже с радостью гладят эту кошку. Ведь с ней сложнее выстрелить себе в ногу - она не владеет памятью, "каклокаций" не делает и вообще ведет себя прилично. Но у кошек есть свои выкрутасы, появились специфические проблемы с висячими ссылками если исходная строка ушла в мир иной. На string_view теперь пишут высокопроизводительный код хитро организуя время жизни полноценных строк. Но view от этого не стал продлевать жизнь данным., это все та же кошка, она независима и может уйти в любой момент оставив вас в одиночестве среди чей-то памяти
std::pmr::string

К началу 2010-х std::string была хорошей собакой для большинства задач, но у неё имелась одна маленькая трагедия. Она совершенно не умела выбирать откуда есть. Попытки вывести породу худых собак уже привели к появлению кошек, как было рассказано выше, но требовался другой зверь. Нужен был тип, который мог бы одинаково ловко работать с разными аллокаторами, не впадая в истерику. Беда в том, что в std::string аллокатор был compile time параметром. Стоило нам взять другой аллокатор для обычной строки, и получался новый тип данных, который смотрел на старый код как на дальнего родственника и не признавал его. Это выглядело как собака, приученная есть только из одной миски всю жизнь. А чтобы переключить её на другую миску, например на custom pool allocator в игре, приходилось заводить целую новую породу. Для embedded систем и игр такая зооферма еще как то терпима, но для широкого использования такая экзотика уже не подходила, потому что число типов строк росло быстрее чем поголовье кроликов весной.
В 2017 году комитет C++ посмотрел на этот питомник и решил, что пора навести порядок. Они взяли proposal расширенных аллокаторов из буста и ввели Polymorphic Memory Resources в стандарт, подарив нам полиморфизм для аллокаторов через виртуальные функции. Пришлось создать новый тип std::pmr::memory_resource как абстрактный базовый класс, но это было уже серьёзным шагом вперед по сравнению с прежним бардаком из несовместимых типов.
class memory_resource {
public:
void* allocate(size_t bytes, size_t alignment);
void deallocate(void* ptr, size_t bytes, size_t alignment);
bool is_equal(const memory_resource& other) const;
private:
virtual void* do_allocate(size_t, size_t) = 0;
virtual void do_deallocate(void*, size_t, size_t) = 0;
virtual bool do_is_equal(const memory_resource&) const = 0;
};
namespace pmr {
using string = std::basic_string<char, std::char_traits<char>,
polymorphic_allocator<char>>;
}
Теперь породистая собака знает свою родословную и может есть из любой миски memory resource которую ей выдадут. Правда, она оказалась чуточку медленнее обычной и требует подозрительно много церемоний перед кормежкой. Это немного сказалось на популярности. К 2025 году PMR используют в основном в специализированных областях вроде игр, embedded и HFT, а для обычных приложений старый добрый std::string все ещё достаточно хорош. Читабельность у pmr::string примерно такая же, просто к ней прилагается небольшой зоопарк производных аллокаторов и шаблонов, зато теперь миски можно менять на лету и собака не возмущается, если переживет замену.
QString (Qt)

С развитием индустрии софтостроения начали появляться странные и ярко выраженные проблемы использования разных софтверных экосистем, каждая из которых продвигала только свою личную и рассово верную реализацию строк. Чтобы решить очередную проблему очередного тринадцатого стандарта в 1991 году норвежская Trolltech решилась на подвиг и начала разработку кроссплатформенного фреймворка Qt. Задача была почти героическая, нужно было заставить один и тот же код работать на Windows, Unix X11 и Mac. При этом каждая система имела свое мнение по поводу кодирования символов. Windows использовал UTF16 через wide char API. Mac OS тоже предпочитал UTF16, но уже через свои родные Unicode APIs. Unix и Linux демонстрировали полный творческий хаос от ASCII и Latin1 до экзотических локалей, которые вспоминали только в полночь. Тогда std::string еще не существовал и даже C++98 выглядел как мираж, поэтому появление очередного четырнадцатого стандарта универсальной строки было вопросом времени и терпения.
Когда делали Qt 1.0, то создатели Хаавард Норд и Эйрик Чампе Энг решили выбрать UTF16 как внутреннее представление QString. С технической точки зрения UTF16 казался самым лучшим компромиссом, так что большинство символов занимали всего 2 байта, включая китайские и японские иероглифы, и только редкие emoji и символы исторических письменностей требовали 4 байта. Так что попугай выучил самый модный язык на тот момент. В придачу его обучили невероятному количеству слов toLower, split, startsWith, contains, replace и многим другим. Так попугай получился болтливым и с ответом на каждый случай жизни, даже методы для нормализации пробелов вроде toHtmlEscaped() и позволяли безопасно отправлять его в браузер.
В 1998 году вышел C++98 со своим std::string и тогда началась эпоха мучительных конверсий туда-сюда - многие уже успели приручить попугая QString и построить для него целые леса скворечников. Попытки подружить его со std::string приводили к созданию временного QByteArray через toUtf8, что выглядело как работа переводчика, который переводит перевод другого перевода. Попугай продолжал гордо говорить на своем UTF16, когда весь остальной мир стремительно переходил на UTF8, и это стало настоящей болью для миллионов разработчиков. Они писали на Qt, но вынуждены были иметь дело со std::string, boost и кучей других либ в качестве зависимостей, превращая весь процесс в местный филиал ада.
В 2025 году QString живет в золотой клетке своей экосистемы, хоть и половина десктопного софта для Linux написана на Qt. В эту клетку входят KDE, VLC, OBS Studio, Telegram Desktop и многие другие. Но надо отметить, что попугай получился красивый, блестящий и прекрасно обученный, но выпустить его на волю означает катастрофу. Непонятно только кого ждет гибель, попугая или мира вокруг. Многие разработчики считают это наследием 1995 года, которое по инерции тянется в 2025, ведь Qt выбрал кроссплатформенность любой ценой, а цена оказалась экзотической строкой, которая упорно не дружит с остальным C++ миром.
Читабельность у кода отличная, его легко узнает любой С++ программист даже если он Qt никогда не видел, хотя таких уже почти не осталось. Конструкции вроде str.toLower или str.split вообще издалека можно спутать с Python. Вся магия как обычно в Qt спрятана глубоко в PImpl и внутри их мета системы, которая по слухам умеет всё. Но каждый раз когда я пишу очередное приложение на Qt, я снова сталкиваюсь с внешними std::string-like библиотеками и вспоминаю ад конверсий как страшный сон.
NSString

NSString разрабатывался в начале 1990-х и общее представление Unicode только появлялось на свет, но работа над универсальной кодировкой символов началась в середине восьмидесятых с участием крупных технологических компаний включая Apple и NeXT. Вышедший в октябре 1991 года Unicode 1.0 был шестнадцатибитной кодировкой и базировался на этой концепции с типом unichar как основной единицей и это стало проблемой позже, обезьяну учили языку который ещё развивался, и когда язык изменился — она осталась с архаичным диалектом. Хотя концептуально NSString базируется на UTF-16, внутренняя реализация на самом деле зависима от содержимого строки.
NSString задумывался как неизменяемый объект с самого начала, на тот момент решение было вдохновлено функциональным программированием и практиками Smalltalk. Такая неизменяемая строка безопасна для передачи между потоками, для использования как ключ в словарях, для кеширования. Если нужна была изменяемая строка, использовался NSMutableString, другая порода обезьяны из того же зоопарка, что создавало интересную дихотомию — две строки с разным поведением но общим предком, это еще более дикая обезьяна, которую можно было дрессировать и менять её поведение.
Одной из самых магических особенностей iMonkey был биндинг данных в CFStringRef. NSString можно было кастовать в CFStringRef и обратно без каких-либо конверсий — это был один и тот же объект в памяти. Это было как если бы обезьяна могла превращаться в собаку из параллельной вселенной C без единого усилия, но круто было пока не начинались вопросы о владении памятью, существование объекта в двух вселенных было очень шатким — нужно было точно понимать кто владеет объектом и в какой момент происходит передача владения данными.
Когда Apple приобрёл NeXT, то весь NeXTSTEP стек включая NSString стали основой для Mac OS X, а позже iOS. Экзотическая обезьяна из закрытого зоопарка NeXT переехала в ещё более закрытый зоопарк Apple, и вплоть до 2014 года NSString был вездесущим. Каждое приложение на Mac и iPhone использовало его для UI, для работы с файлами, для сетевых запросов, для хранения данных, а поголовье обезьян выросло и заполонило весь зоопарк, вытеснив других животных. Но выпустить её за пределы Apple экосистемы было невозможно - синтаксис Objective-C, зависимость от Cocoa frameworks, подсчета ссылок и других ограничений - всё это работало только внутри огороженного сада Apple. NSString был продуктом своего времени, став элегантным решением для эпохи, когда динамизм Smalltalk казался будущим мира разработки, а UTF-16 был разумным выбором, которого хватит всем.
std::wstring

В начале 1990-х миру софта пришлось думать не только о англоговорящих людях, но и о тех, кто английский учить совсем не хотел, предпочитая видеть родные глифы, иероглифы и точечки над буквами. ASCII с его жалкими 128 символами был безнадёжно мал — ни китайский, ни японский, ни арабский, ни даже латиница в полной красе не помещались. Unicode появился как великое решение, и в первой версии 1991 года он был шестнадцатибитной кодировкой — все символы мира должны были уместиться в 65536 позиций. Как говорили мудрые люди того времени, 640 Кб должно было хватить всем.
Когда C++ стандартизировался в 1998 году, комитет включил в стандарт wchar_t для широких символов и std::wstring как строку из них. Идея была гениальна в своей наивности: пусть каждый символ будет шире, чем char, и Unicode поместится без проблем. В этот момент эволюция решила поэкспериментировать и создала утконоса — существо, которое на бумаге выглядело как хорошая идея, а на практике оказалось биологическим парадоксом. Проблема была в том, что стандарт не зафиксировал размер wchar_t, а лишь обещал, что он достаточно велик, чтобы вместить любой символ Unicode. На практике это означало собственную интерпретацию на каждой платформе.
Windows пошла своим путём и выбрала 16 бит, потому что их API базировался на UTF-16, но это были свои, уникальные 16 бит. Linux и Unix решили сделать 32 бита, чтобы быть более универсальными, а MacOS вообще начала отходить от wchar_t в сторону собственной реализации. Получился утконос, который в Австралии откладывал яйца размером 16 бит, в Европе рождал живых детёнышей по 32 бита, а в отдельных зоопарках оставлял кладки по 24 бита. Один и тот же код компилировался на разных платформах и работал совершенно по-разному. sizeof(wchar_t) возвращал 2 на Windows, 4 на Linux и сем-восем на Mac, превращая любые попытки бинарной совместимости в утопическую мечту.
Попытки конвертировать между std::string и std::wstring на Windows превращались в многочасовое путешествие по документации, изучение std::wstring_convert и магию codecvt фасетов для конверсий между кодировками. Код выглядел довольно просто, но пользоваться им можно было только в очень узких рамках, словно дрессированным утконосом, который умеет плавать, но только в правильной луже.
// UTF-8 строка
std::string utf8 = u8"Привет, мир! ?";
// Конвертация UTF-8 -> UTF-16 (std::wstring)
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
std::wstring wide = converter.from_bytes(utf8);
std::wcout << L"UTF-16: " << wide << std::endl;
// Конвертация UTF-16 -> UTF-8
std::string utf8_again = converter.to_bytes(wide);
std::cout << "UTF-8: " << utf8_again << std::endl;
Главная причина существования std::wstring была проста - Windows API заняла эту нишу раньше всех и относительно легко диктовала условия разработки софта. Все функции WinAPI для работы с файлами, окнами, реестром, сетью принимали WCHAR* то есть UTF-16 строки и до появления std::filesystem в C++17 единственным способом открыть файл с нелатинским именем на Windows было конвертировать путь в std::wstring и использовать нестандартные расширения компилятора. Второй причиной было легаси, когда тонны старого кода были написаны в эпоху сабелезубых волков, а wchar_t казался будущим. Понтяно, что компании не хотели переписывать миллионы строк кода только из-за рефакторинга ради рефакторинга, поэтому std::wstring тащился из версии в версию стандарта как пережиток прошлого.
Утконос вымер бы миллионы лет назад если бы не изолированная экосистема Windstralia, где у него не было конкурентов. Все хотят его забыть и заменить на современные решения, но Windows API не даёт полностью от него отказаться и легаси код на wchar_t продолжает жить в корпоративных кодовых базах как ископаемые останки мезозойской эры программирования. Утконос странный, непонятный, никто не может объяснить зачем природа его создала, но он продолжает существовать в своей изолированной экосистеме, как напоминание о ранних годах индустрии.
FrameString

Игровые движки всегда сталкивались с фундаментальной проблемой производительности и ограниченными ресурсами. Игра работающая на 60 кадрах в секунду имела бюджет всего 16.6 миллисекунд на кадр, и в этот микроскопический промежуток времени нужно было обработать физику, AI, анимации, рендеринг и тысячи других задач. Если вы посмотрите на malloc в стандартном std::string, то оказывается что он занимает сотни циклов процессора - нужно было поискать подходящий блок в куче, возможно вызвать системный аллокатор и когда у вас сотни и тысячи мест, где ведется работа со строками, то это становится проблемой.
Debug сообщения с позициями игроков, UI лейблы со счётчиками здоровья и патронов, имена particle effects, форматированные логи событий, обращение к свойствам объектов по имени — всё это строки живущие ровно один кадр. В начале цикла апдейта они создаются, используются, и к концу render они больше не были нужны, еще чаще они используются вообще в одном блоке кода или функции. Но каждая из них вызывает malloc/free, превращая управление памятью в бутылочное горлышко производительности.
Чтобы избавиться от этих накладных расходов, разработчики идут на компромисс и меняют память на скорость. Индустрия обратилась к старой идее из мира системного программирования - линейным аллокаторам. Концепция проста: выделяем один большой блок памяти на старте, и раздаем из него кусочки просто сдвигая указатель. Аллокация превращается в две инструкции - проверку что места хватает и инкремент смещения. Никаких поисков по free lists, никаких вызовов операционной системы и минимальные накладные расходы.
class ArenaAllocator {
char buffer[10 * 1024 * 1024]; // 10 MB
size_t offset = 0;
public:
char* alloc(size_t n) {
char* ptr = buffer + offset;
offset += n;
return ptr; // Мгновенно!
}
void reset() { offset = 0; } // Сброс за O(1)
};
Впоследствии многие движки обернули такие аллокаторы в удобные высокоуровневые типы, например String класс который принимал framemem_ptr был одним из них. Снаружи он выглядел как обычная строка с методами append, format, operator+, но под капотом все аллокации шли внутри фрейма.
String debugMsg(framemem_ptr);
debugMsg = "Player position: ";
debugMsg += to_string(x);
debugMsg += ", ";
debugMsg += to_string(y);
// В конце кадра вся память автоматически освобождается
Но красота эфемерности несет смертельную опасность, ведь бабочка-однодневка живёт только один день, и если попытаться сохранить её на завтра, то в руках останется труп. Главная ошибка с frame memory strings была попытка сохранить указатель на следующий кадр. Компилятор не может это предотвратить, и статический анализ тоже не увидит, если не написать нужные правила. Несмотря на опасности, такие строки стали стандартом в высокопроизводительных движках потому что скорость важнее всего, и если водка мешает учебе, то выбор очевиден аллокация их занимает несколько циклов процессора, когда стандартный malloc занимал несколько сотен циклов - выбор явно не в пользу std::string и обычных строк.
String(framemem_ptr) это бабочка-однодневка потому что её жизнь мимолётна и прекрасна: она рождается в начале кадра, живёт несколько миллисекунд и умирает когда кадр заканчивается. Её существование эфемерно но в этой эфемерности есть элегантность, потому что не нужно думать о деструкторах, не нужно вызывать free, не нужно бояться утечек памяти. Опасность в том что нельзя поймать бабочку и сохранить на завтра.
FString

В середине 1990-х годов Тим Суини, основатель Epic Games, был очарован революцией которую произвёл Quake, эпики взяли эти идеи как основу но сделали в итоге не просто игру, а полноценный движок для создания игр. В 1998 году вышел Unreal, первый шутер на Unreal Engine первого поколения. Это был золотой стандарт для своего времени: динамическое освещение, продвинутый редактор уровней, скриптовая система UnrealScript. Но технически Unreal Engine был оптимизирован для одной задачи - шутер от первого лица. Вся архитектура, весь networking, весь рендеринг были заточены под коридоры и арены жанра. И строки в движке были утилитарными инструментами для этой цели - имена уровней, названия оружия, debug сообщения, network packets. Так родился предок FString - простая кастомная строка которая не зависела от стандартной библиотеки C++. Как обычно игровые движки контролируют всё сами (от аллокаторов до методов строки) только так можно гарантировать предсказуемое поведение на всех платформах. Это был щенок золотистого ретривера, которого взяли не из питомника стандартной библиотеки, а вырастили сами с нуля.
С каждым поколением Unreal Engine строковая система усложнялась. В Unreal Engine 3 появилась не одна, а три породы строк: FString для изменяемого текста, FName для эффективных сравнений через хеш-таблицу, и FText для локализованного UI текста. FString нравится всем - большая, дружелюбная собака которая делала всё что попросишь, конкатенировать строки, форматирование, split строки по разделителю, ParseIntoArray, Find, Contains, StartsWith, EndsWith и еще куча всего. Но ретривер был большим, значительно больше чем std::string из-за дополнительных полей для интеграции с движком. Каждая строка несла с собой metadata для reflection системы, информацию об аллокаторе, возможно thread-safety флаги - это было платой за универсальность, и при первой встрече может случайно сбить с ног своим энтузиазмом. Он приносит не только то что попросили но и всё остальное - мяч, палку, газету, свою миску, поводок. FString тащит за собой всю инфраструктуру Unreal Engine и это одновременно его сила и слабость. Если вы внутри движка это преимущество и всё работает вместе бесшовно, если вы пытаетесь интегрироваться со сторонними либами - это становится проблемой: нельзя просто взять FString и использовать в другом проекте, он не выживет без своей семьи.
Золотистый ретривер живёт в большом доме Epic Games уже тридцать лет. Он знает каждый угол, каждого члена семьи, каждую команду. Новые щенки могут быть современнее и эффективнее, но у ретривера есть то чего у них нет — десятилетия опыта работы в AAA играх, миллионы строк проверенного кода, армия разработчиков которые знают все его причуды. Ретривер, который помог создать Gears of War, Fortnite и другие хиты точно заслужил своё место у камина даже если весит слишком много.
StringAtom

В 1960 году Джон МакКарти создал LISP и одной из фундаментальных концепций языка была работа с символами, как атомарными идентификаторами, которые существовали как уникальные сущности. В версии LISP 1.5 1962 года описывалась функция intern которая либо возвращала существующий символ с заданным именем, либо создавала новый если такого ещё не было, и это был моментом рождения string interning как концепции.
Символы в LISP были неизменяемы и уникальны по своей природе. Если написать (foo) дважды в коде, оба раза получите ссылку на один и тот же символ в памяти. Сравнение символов в этом случае становится очень быстрым, мы просто сравнением указатели, потому что одинаковые символы физически были одним объектом. Это была галапагосская черепаха программирования - создавалась медленно через поиск в таблице символов, но потом жила вечно и была уникальна, невозможно было создать дубликат.
МакКарти внес фундаментальную идею о неизменяемых объектах-идентификаторах: в программах идентификаторы сравниваются тысячи раз но создаются редко. Имена переменных, функций, классов - всё это известно на этапе написания кода и почти не меняется в рантайме, тогда зачем сравнивать их посимвольно каждый раз?
Параллельно с развитием LISP, создатели компиляторов независимо пришли к той же идее, что компилятор должен отслеживать все идентификаторы в программе и Symbol table стала фундаментальной структурой данных описанной в классическом Dragon Book Ахо, Сети и Ульмана.
К 1990-м идея string interning мигрировала и в высокоуровневые языки, так Java сделал его частью языка - все compile-time строки автоматически интернировались, и программист мог явно вызвать String.intern() для runtime строк. Scheme, Smalltalk, Julia продолжили традицию LISP с символами как типом. С ростом сложности игровых движков проблема строковых идентификаторов пришла и туда, разные системы требовали быстрого поиска компонентов по имени, например шейдеры искали параметры по строковым именам, анимации тоже переключались между стейтами по именам. Решением стал StringAtom или StringID, как обертки над интернированной строкой, когда при создании StringAtom("Health") происходил поиск в глобальной hash-таблице и если "Health" уже была там - возвращался существующий ID или указатель, а если нет, то создавалась новая запись и строка копировалась в вечное хранилище. Дальше все операции со StringAtom были просто сравнением пары чисел.
StringAtom healthTag("Health");
StringAtom manaTag("Mana");
// Сравнение мгновенно - просто ID или pointer equality
if (component.tag == healthTag) {
// ...
}
Самой опасной особенностью StringAtom стало его время жизни - фактически вечное. Однажды созданный атом никогда не удалялся, глобальная таблица интернированных строк только растет. Это было сознательное решение - отслеживание того когда атом больше не используется будет в десятки раз дороже, чем просто держать его в памяти всегда. Это работает отлично если создается разумное количество уникальных атомов. Теги в игре — сотни уникальных имён, параметры шейдеров - тысячи, уникальные строки еще несколько тысяч. Всё это умещалось в килобайты памяти и живет до конца программы - популяция галапагосских черепах стабильна - они долго живут, медленно размножаются, и экосистема справляется.
Но если программист по ошибке создает StringAtom из пользовательского ввода или уникальных имен в цикле, популяция быстро раздувается до гигабайтов и погибают все, даже при том что черепахи бессмертны, и если их популяция растёт бесконтрольно — остров переполняется и экосистема коллапсирует.
xstring/cow

В середине 80-х память была дорогим ресурсом и хорошо если компьютеры имели мегабайты памяти, если дорогим ресурсом становится память то логично искать способы оптимизировать повседневные операции с ней, и одна из идей казалась очевидной - раздельное хранение уникальных строк и указателей на них. Концепция Copy-On-Write пришла из операционных систем где процессы разделяли страницы памяти до момента модификации, когда ОС не копировал все адресное пространство родительского процесса сразу, а только маркировал страницы как shared и копировал их по требованию когда кто-то пытался писать. Это экономило огромное количество ресурсов и памяти, так почему бы не применить ту же идею к строкам?
Так родилась концепция xstring или COW string - строки где несколько переменных указывают на одни данные как на кроликов, живущих в общей норе. Один большой буфер с данными строк, и множество владельцев разделяющих его использование. Счетчик ссылок отслеживает сколько кроликов живёт в норе, а когда создаем копию строки - то просто инкрементируем счётчик вместо аллокации и копирования всего буфера. Кролики экономные - зачем каждому рыть свой тоннель если можно жить вместе?
К началу 2000-х многоядерные процессоры выявили проблемы COW строк. Подсчет ссылок требует атомарных операций и каждое копирование строки - atomic increment, каждое уничтожение — atomic decrement. С учетом того, что атомарные операции на порядки дороже обычных инкрементов это приводит к некоторым проблемам при работе с такими строками. Особенно плохо становится в многопоточном коде, где строки часто передаются между потоками - тут уже атомарный подсчет ссылок становится горячей точкой и проблемой , а не решением. Кролики путаются когда их слишком много бегает по норе одновременно — сталкиваются в узких тоннелях и мешают друг другу.
Читабельность обычная - те же методы что у std::string. Использование стреляет через неожиданные паттерны, когда модификация триггерит дорогое копирование всех данных, ложное или ошибочное копирование данных. Кролик мирный пока в норе достаточно места и тихо, но как только начинаешь его двигать — быстро прыгает в сторону роя новое жильё.
StringID

Тогда же в начале 2000-х игровые движки столкнулись с другой фундаментальной проблемой - идентификации уникальных данных, взрывной рост числа используемых ресурсов - текстуры, модели, звуки, анимации - привел к тому, что существующие системы хранения на обычных строках не справлялись. Тут еще развитие скриптование и моддинг добавили свою бочку дегтя в ложку меда, насыпав неуникальных разделяемых строк (когда два мода имели одинаковые строки, но шарить их было нельзя), логики, имен, систем. Везде были строковые сравнения, и везде они были узким местом производительности.
StringAtom частично решал эту проблему через интернирование, но требовал поиска по таблице и память для хранения всех уникальных строк навсегда, а std::string сравнивался медленно через strcmp, поэтому нужно было что-то радикально другое - способ превратить строку в число на этапе компиляции, но иметь возможность сделать тоже самое и в рантайме, оставив только число. В 2007 году в появилась статья (https://cowboyprogramming.com/2007/01/04/practical-hash-ids/), которая формализовала подход, который до сих пор используется в большинстве AAA студиях - идея была проста до гениальности: захешировать строку через CRC32 и получить uint32_t, использовать это число как уникальный ID. Строка "PlayerHealth" превращалась в 0x8F4A23B1 и компилятор вообще не включал оригинальную строку в исполняемый файл, гепард начинал бегать, и бегать очень быстро.
constexpr uint32_t operator""_sid(const char* str, size_t len) {
return FNV1a(str);
}
constexpr uint32_t damageEvent = "DamageEvent"_sid;
Теперь строки это числа, но программист продолжает видеть в коде текст, понимает что происходит, а компилятор генерирует только число. И это был идеальный баланс - код остается понятным людям, но оптимален для машины. Одной из киллер фич StringID была возможность использовать строковые идентификаторы в switch выражениях. Обычно C++ не позволяет switch по строкам потому что они не могут быть представлены в виде числа, но хеши строк это compile-time константы, поэтому такой код будет работать.
switch(messageType) {
case "PlayerDied"_sid:
handlePlayerDeath();
break;
case "EnemySpawned"_sid:
handleEnemySpawn();
break;
}
StringID нашёл свою нишу везде где нужны были быстрые строковые идентификаторы известные во время компиляции, например в Event IDs, именах ресурсов, компонентах и переменных шейдеров, системы рефлексии используют StringID вместо полных строк. Везде где строка была идентификатором а не данными, гепард оказывается эффективнее любого другого животного в саванне, но гепард умеет только быстро бегать и ничего больше.
StringID занимает своё место в экосистеме строк как специализированная форма для идентификаторов, не для обработки текста, не для пользовательского ввода или хранения данных. Только для быстрой работы с известными именами, которые сравниваются часто и должны быть максимально простыми.
std::string().append()
Мы никуда не денемся от std::string как стандарта. Он встроен во всё вокруг, и полностью игнорировать его просто невозможно - большАя часть экосистемы C++ так или иначе ожидает именно std::string. В любой крупной кодовой базе он всё равно будет просачиваться в интерфейсы, и бороться с этим бессмысленно. Да, он не идеален, но его сила в том, что он обеспечивает совместимость, предсказуемость поведения и минимальную когнитивную нагрузку для всех, кто читает и пишет код.
Но и зацикливаться только на std::string, как будто это единственно возможное представление строки тоже вредно. Внутри движка, на горячих колстеках, чувствительных к выделениям памяти, можно и нужно использовать другие решения: small-string оптимизации, арен-аллокаторы, string_view, собственные строковые буферы или вообще фиксированные массивы. Если знать, где именно нужны альтернативы, можно получить огромный выигрыш в производительности и управляемости кода — и при этом не ломать интеграцию со стандартными компонентами.
Отдельное спасибо моему другу Саше Васильеву за предоставленные рисунки и художественную обработку.

Если вам интересно услышать про реализацию xstring в игровых движках приходите на вебинар https://pvs-studio.ru/ru/webinar/25/ там будут не менее увлекательные доклады от Андрея Карпова и Дениса Ярошевского.
И заходите на мой курс Нескучное программирование, попробуем вернуть немного магии в разработку на плюсах. Промокод как обычно HABR50.

Комментарии (86)

OlegZH
20.11.2025 17:33А как обстоит дело с AnsiString (VCL) и CString (MFC)?

bodyawm
20.11.2025 17:33String в Delphi реализован поверх динамических массивов, а те в свою очередь работают за счет рефкаунтов. Строки условно не мутабельные и могут реаллоцироваться при изменении.
Однако есть ещё ShortString - это array[0..255] ofChar и PAnsiChar/PWideChar - это обычные нуль-терминированные строки

NeoCode
20.11.2025 17:33Про игровые строки не знаю, а из обычных QString самая лучшая. Реально много продуманных и полезных методов, не то что в убогой std::string.

Mark194
20.11.2025 17:33Поддерживаю, особенно в последних версиях Qt много полезных методов, делающих всё проще

YogMuskrat
20.11.2025 17:33Главное преимущество QString, на мой взгляд, это хорошая поддержка юникода прямо из коробки. Но зато занимает в два раза больше памяти, чем std::string.

NeoCode
20.11.2025 17:33На месте разработчиков Qt я бы добавил еще одну такую же мощную строку для Utf8 (поддержать оба внутренних представления в одной QString наверное сложнее, хотя потенциально тоже возможно). А QByteArray , который иногда используется как байтовая строка, лучше бы переименовать в QBlob.

Jijiki
20.11.2025 17:33std::u32string. в С uint32_t и char* symbol, хранить строку можно в rope буфере - дереве, потомучто помимо пространства строки есть еще пространство 2д, строка в 2д будет ограничена краями, и задана размером глифа помимо наполнения самого квадрата глифом, а это значит если сцена реализована через дерево, строки, которые находятся в текстБоксе лучше хранить в дереве тоже
а если это текстура(неизменяемый текст), то это не строка а текстура предпосчитанная из строки движком текста
почему так, потомучто рекурсивный обход дерева при рендере частично выраждается в корутину с нюансами, тоесть рендерим квадратики
template<typename T> void renderTreePASSUI(const SceneUINode<T>* root, Shader *shader,float dt,glm::mat4 &projection) { if (root == nullptr) return; // Обработка левого поддерева renderTreePASSUI(root->left, shader,dt,projection); // Рендер текущего объекта const T& obj = root->object; if (obj.VAO && *obj.VAO) glBindVertexArray(*obj.VAO); else{ outputError("Error on RENDER!"); return; } if (obj.textureID && *obj.textureID) glBindTexture(GL_TEXTURE_2D, *obj.textureID); glm::mat4 model = glm::mat4(1.f); if (obj.type == OBJUITYPES::STATICUI && obj.pos) { model = glm::translate(glm::mat4(1.f), *obj.pos); model = glm::scale(model, obj.ptr->size); // размер 100x100 пикселей model = glm::mat4(1.0f) * model; shader->setMat4("uModel", model); shader->setVec4("color", obj.ptr->color); glDrawElements(GL_TRIANGLE_STRIP, 6, GL_UNSIGNED_INT, 0); glBindVertexArray(0); } // Обработка правого поддерева renderTreePASSUI(root->right, shader,dt,projection); }Скрытый текст

квадратики включая указатель в центре, потомучто это уже система УИ рендер квадратиков, как видим векторов тут нету, вектор просто хранит обьекты, лично я считаю это нюансы нелинейного обхода данных, даже при условии что рендерится от начала до конца
тоесть по моему мнению в высоконагруженных приложениях нельзя просто так взять и
for(int i=0; i<size;i++) renderRectsэто будет грузить почему-то мне кажется

Kobagugi
20.11.2025 17:33QString прекрасен пока ты живешь внутри экосистемы Qt, но как только тебе нужно вызвать функцию из сторонней библиотеки, которая ожидает const char* или std::string, начинается ад конверсий через toUtf8().constData() и эта "лучшая" строка становится источником постоянной боли)

NeoCode
20.11.2025 17:33Это проблемы не Qt, а самого C++ как языка и его библиотеки. С++ далек от совершенства, стандартаня библиотека далека от соврешенства, Qt как может пытается это исправить. Но разумеется, на несовершенном фундаменте совершенное здание не построить:)

unC0Rr
20.11.2025 17:33Ммм... Как насчёт QString::toStdString()? Ничем не хуже ада конверсий между const char*, std::string, std::string_view.

Rezzet
20.11.2025 17:33Хуже всего когда какая-то библиотека навязывает тебе свои строки, в частности это делает Qt, вот че ни делай, а от них не уйдешь ну и код постоянно пронизан конверсиями, потому что другие библиотеки или хотят std::string или свои(что уже большая редкость). Но QString какими бы они классными не были это мракобесие и кошмар любого кода на Qt. Сядьте в комитете договоритесь раз и используйте одни строки. А раньше еще очень любили данные в строки прятать, благо сейчас все чаще вижу std::vector<std::uint8_t>
UDP. Ладно я понимаю что нельзя одни строки придумать для всего с++, есть железки где это неуместно, значит надо какие-то уровни фичей делать в стандарте, минимальный, только с базовыми фичами, и дальше там 1, 2, 3 уровень, да то же не сильно хорошее решение, ну а какой вариант? Либо единый стандарт и с++ работает на калькуляторах, но все страдают, либо абстракции высокого уровня с расплатой в виде перфа и большинство радуются, а кому надо работают с базовыми версиями языка.
UDP2: если Qt когда-то перейдет на std::string я напьюсь в дрова по этому поводу и буду танцевать на столе(рабочем). Но этого не будет.

MountainGoat
20.11.2025 17:33Конечно не перейдёт, у std::string нет такого функционала. Единственное чего получится добиться – это что вместо QString вам придётся писать QString<std::string> и всё станет в разы медленнее.

Kobagugi
20.11.2025 17:33О да, боль каждого, кто пытался подружить Qt с условным Boost или любой другой современной C++ библиотекой
Танец с бубном вокруг QString::toStdString() и QString::fromStdString() это обязательная программа

Rezzet
20.11.2025 17:33С бубном там танцевать особо не надо, все работает достаточно прозаично, скорее надо следовать четкому соблюдению ритуала. И весь код пронизано вот этой никому не нужной хренью, можно сделать операторы преобразования, но сыкатно, они могут так сработать непредсказуемо и по перфу и по типам повыводить что-то крайне странное. Я в итоге сдался и для половины сделал, как минимум для либы логов и сериализации в json стало проще.

feelamee
20.11.2025 17:33да то же не сильно хорошее решение, ну а какой вариант?
вариант это вообще не позволять владеющим строкам просачиваться в интерфейс. Если вам внутри не нужно хранить полученную строку, то это просто эффективнее. Если нужно хранить, то принимая владеющий тип в функцию вы раскрываете деталь реализации.
std::string_view хороший вариант для стабильного апи. Он может работать почти с любой строкой (разве что есть строки которые не аллоцированы как один кусок памяти).Ещё более спартанский вариант это char* + size_t. Это для сишного апи.
Но на деле это мало кто будет о таких мелочах задумываться. Влепят аппрув и поехали.. Вот и живем так что весь код пропитан QString::toStdWStribg / fromStdWString

Rezzet
20.11.2025 17:33О это вы конечно загнули, тут благо в целом народ понял совсем недавно чем ссылка от объекта отличается и если в функцию приходит const std::string&, то не надо это прихранивать у себя без явного копирования и вообще относиться к этой вещи как "не твое", а вы уже string_view хотите, кому-то придет в голову их прихранивать у себя, ну а че вот же я копию у себя сделал и счастливый. Опять придется ждать пока коллективное бессознательное обучится новой магии вне Хогвартса.
Он может работать почти с любой строкой (разве что есть строки которые не аллоцированы как один кусок памяти).
А как std::string_view будет у вас работать с QString, если у последнего внутри QChar и он внутри UTF-16 содержит? sizeof(QChar) == 2

yatanai
20.11.2025 17:33Меня больше напрягает того что никто не может определится что такое строка или символ впринципе. В итоге если хочешь обойти строку то должен учитывать что utf-8 строка будет тупо байты сыпать, и ты ещё должен их сборать и понять какой символ перед тобой. Дурка
В итоге строка это тупо динамический массив, по сути (но со специфическим char типом), чем иногда пользуются различные библиотеки (часто такое в хэшировании видел, что они байты хэша в std::string хранят, а не в vector каком-нибудь)

Rezzet
20.11.2025 17:33В итоге строка это тупо динамический массив, по сути (но со специфическим char типом), чем иногда пользуются различные библиотеки (часто такое в хэшировании видел, что они байты хэша в std::string хранят, а не в vector каком-нибудь)
так раньше делали, потому что в vector какого типа? целочисленные типы не имели фиксированного размера, имел только char, а значит надо делать vector<char>, а зачем его делать если string он и есть. Сейчас все чаще и прикладные программисты и разработчики библиотек начинают использовать std::vector<std::uint8_t> или может быть даже std::vector<std::byte>

middle
20.11.2025 17:33Не раскрыта тема TString в Яндексе.

dalerank Автор
20.11.2025 17:33А это что за зверь то такой?

middle
20.11.2025 17:33В Яндексе есть Аркадия -- монорепа, которая в том числе содержит стандартную библиотеку Яндекса, которую использовал сначала Поиск, а потом и всё остальные проекты. Эта библиотека содержала тип Stroka (напомню, что Яндекс.Поиск появился раньше, чем std::string), который был был позже переименован в TString. В году 2019 TString хотели заменить на std::string, но чем всё кончилось, я не знаю.
Кстати, был тип Wtroka (аналог std::wstring), который произносился как "штрока".
unreal_undead2
20.11.2025 17:33Да многие большие проекты своими строками обзаводятся. В последнее время с llvm::StringRef часто встречаюсь, ещё из открытого навскидку blink::String, в одном из закрытых проектов долго жили свои строки со стандартным интерфейсом, но в какой то момент просто сказали using our_string = std::string (или wstring, в зависимости от платформы).

rsashka
20.11.2025 17:33Класс llvm::StringRef никак не аналог std::string. Это скорее аналог std::string_view так как он не владеет данными, а содержит только ссылку на них

unreal_undead2
20.11.2025 17:33Согласен, но в коде llvm чаще используется именно StringRef чтобы не перевыделять/копировать память.

rsashka
20.11.2025 17:33Не понял, почему "но". Ведь данные в AST статичные и перевыделять память по новой действительно не нужно.

unreal_undead2
20.11.2025 17:33Я к тому, что если по смыслу в функции нужно что-то сделать строковыми данными - чаще всего она принимает StringRef и пользуется его интерфейсом. Да, он не занимается вопросом аллокации памяти - но это только часть функционала строки.
Ведь данные в AST
llvm - это далеко не только компилятор ;) Но и при банальном парсинге входных текстовых файлов в коде обычно StringRef.

rsashka
20.11.2025 17:33AST, это только пример статичной структуры, узлы которой не требуют перевыделения памяти.
И структура банального парсинга входных текстовых файлов будет тоже статичной, для анализа узлов в которой не требуется перевыделять память (после чтения файла данные уже находятся в ней), поэтому использовать StringRef или std::string_view совершенно логично.

kemlinbot
20.11.2025 17:33Круто, даже не задумывался, что зоопарк такой большой.
Когда начинал программировать и узнал, что литералы живут до конца программы - хотелось их не использовать вовсе, казалось, что столько памяти пропадает зря.

PatakinVVV
20.11.2025 17:33Самое забавное, что половину этого зверинца порождает не столько C++, сколько разные требования по времени жизни и владению памятью

rivo
20.11.2025 17:33А еще каждый свою кодировку продвигал и тип wchar. Все кто на utf8 перешли, не страдали особо и совместимость со старым кодом получили.

cdriper
20.11.2025 17:33Читать просто невозможно. ИИ нагенерировал главу для учебника по биологии с каким-то бесконечным количеством грубейших ошибок. В области C и C++, разумеется.
Всё как в дикой природе - волк с собой счётчик овец не носит.
Какой волк, какой счетчик овец? Это техническая статья или постмодернистские галлюцинации?
Главная магия фиксированных массивов в том что они живут на стеке.
Во-первых, нет никакой магии.
Во-вторых, фиксированные массивы не живут на стеке. Они вообще не живут. Их расположение определяется местом, где они определены и это далеко не всегда стек.вы можете сами это посмотреть в исходниках, которые все еще доступны на гитхабе
Типичный LLM ход.
Весь этот бред разбирать не вижу никакого смысла. Это просто мусор.

HiItsYuri
20.11.2025 17:33Какой волк, какой счетчик овец? Это техническая статья или постмодернистские галлюцинации?
Счётчик количества символов в строке. Простейшая аллегория же.

Antohin
20.11.2025 17:33А это смотря в каком настроении и с какой целью читать статью. Я вот базами занимаюсь. Базово знаком с std::string, зачем вот мне оно надо - глубже в эту вашу паучью банку лезть? Если бы не забавные аналогии, вряд ли бы осилил хотя бы до середины. А так прочитал всю в общеобразовательных целях.

cdriper
20.11.2025 17:33в общеобразовательных целях
в образовательных целях прочитать сгенерированную LLM статью с кучей ляпов и фактических ошибок?

klonklion
20.11.2025 17:33Среди всех прочитанных сегодня мной статей это самая неллм статья. Впрочем, всё субъективно.

Zalechi
20.11.2025 17:33Статья отличная. Мне кажется это конспект автора для какой-то лекции, где он возможно прогнал его на каком-то этапе через ллм.
В любом случае, я испытал те же самые чувства.

AskePit
20.11.2025 17:33у автора хорошие статьи, и эта статья хорошая - со смачным удовольствием влепил плюс. отнюдь не мусор, не ваодите всех в заблуждение

Kobagugi
20.11.2025 17:33Читаю и прямо флешбеки из начала 2000 ловлю
Войны между адептами MFC CString, борландовского AnsiString и std::string...
А потом приходил Qt со своим QString и говорил "подержите мое пиво"
Каждая библиотека, каждый фреймворк считал своим долгом родить собственную реализацию строки, и каждая была несовместима с соседней. Ад конверсий toStdString().c_str() закалил не одно поколение плюсовиков

domix32
20.11.2025 17:33Отдельное спасибо моему другу Саше Васильеву за предоставленные рисунки и художественную обработку.
Это ж ИИ обработка, не?

dalerank Автор
20.11.2025 17:33Это подбор фоток под темы абзацев + несколько фильтров в фотошопе + ручные правки. Я пробовал нагенерить это промтами, но в итоге выбрал лимиты и сдался. Оказалось проще отдать человеку :)

Zalechi
20.11.2025 17:33Статья шикарная вышла. Там по мелочи ляпы — вообще ни о чем.
Ллм использовать как помощника это нормально. Другое дело люди массово злоупотребляют.

MasterMentor
20.11.2025 17:331/3 статьи читается с интересом, затем - перебор с объёмом текста и сложно держать внимание на концепциях.
Вывод, характеризующий "ИТ": в 2025-м году от Х.Р. хомяки не могут остановиться - улучшают HTML и борются со строками.
И прогноз на будущее:
Бэк ту гуд.
Отрадно наблюдать, как в 2025-м году в пабликах "Ассемблер" (по-русски) идёт горячее обсуждение программирования под ZX Spectrum ~1988г. Полагаю, после периода стихийно-массовго программизма в Руси придётся начинать с этого чекпоинта. :)
dalerank Автор
20.11.2025 17:33Тут более техничная статья про xstring https://habr.com/ru/articles/873016/ и поменьше объемом

WASD1
20.11.2025 17:33bool compare(const std::string& s1, const std::string& s2) { return s1 == s2; } std::string str = "hello"; compare(str, "world"); // Создаёт временный std::string, аллокацияА почему при передаче по константной ссылке создаётся новая версия строки s1?

dalerank Автор
20.11.2025 17:33А дело не в s1

WASD1
20.11.2025 17:33а зачем вообще - я реально не понимаю проблему (мотивировочную часть).
Есть ссылки. Они куда-то передаются - собственно это должно работать с zero-copy.
rhs всегда объявляется const (т.е. даже слишком много реализаций оверлодить не надо).
Проблема с чем? С литералом "world"?
Ну да только в С++20 додумалисть сделать ==( const std::string &, const char *).
Jijiki
20.11.2025 17:33поидее тогда если так то
bool cmp11(const std::string_view& s1, const std::string_view& s2) { return s1 == s2; } int main() { std::string s1("hello");//явное владение std::string s2("world");//явное владение s1.clear();s2.clear(); std::cout<<cmp11(s1,s2)<<std::endl; //отправка указателей на владельцев поидее return 0; }точно так же и с выводом можно сделать поидее, явно аллоцировать владение строки(но не стараться хранить сразу стринг_вью там даже если покажется, что работает в кадрах будет видно, что где-то будет мусор), чтобы оно жило в своём кадре и отправлять только интерфейс на строку, но при условии, что владение существует

MiracleUsr
20.11.2025 17:33С литералом world, но ещё до сравнения. Функция принимает в качестве аргумента только ссылку на std::string - соответственно для передачи в функцию из этого литерала будет сконструирован экземпляр std::string, что и вызовет аллокацию и копирование строки.
Если принимать аргументом не std::string, а std::string_view, то все станет гораздо лучше и универсальнее.

WASD1
20.11.2025 17:33Ну да.
Достаточно же не в 2020 году, а в 2003 / 2011 году написать:bool operator==( const std::string & lhs, const char * rhs);И не пришлось бы переходить на новый тип данных.
Но видимо комитет по стандарту лёгких путей не ищет.
wander
20.11.2025 17:33Вы совсем уже с ума сошли со своим хейтом? Такие операторы были с самого начала стандартизации C++.


Jijiki
20.11.2025 17:33а как же Юникод, это же std::u32string и хэш uint32_t в char* с размером глифа, который отразится в аабб, разве это зоопарк или мы обсуждаем только ASCII? в аски просто стринг, или char строка и инт - кодпойнт, с аабб, по обратной связи вы возможно спросите как управлять такой строкой и текстом.
так ответ и кроется в реализации, если мы рисуем строки и они не изменны никогда, это заведомо текстура, если рисуем текстовый редактор, то пользователь манипулирует строкой через механизм 2д как бы мы этого не хотели, границы окна, границы текстБокса, границы глифа, так это еще учитывая то что у вас никогда не будет строки прямой, там будет сущность буффера даже в С++, изза того, что проще писать хэш во время открытия файла, соотв если это не текстовый редактор, надо реализовать механизм ввода символов через Юникод же, а это текстуры глифов с позициями, и там еще будут нюансы с линейностью, тоесть всё таки это через вектор да, ну тоесть vector-string-size? нет конечно, мы получаем метаданные глифа до отрисовки, в нужной локали, и реализовываем буффер вывода строки
если это 8битные вещи, там уже есть преднастроенные глифы, битмапы же тоже
странно тут как не крути буффер - его реализация будет одна, и она не будет string напрямую, это будут кусочки памяти char* от кодпойнта же

Kotofay
20.11.2025 17:33Это было проще для железа: не нужно хранить лишнюю длину, просто иди по памяти, пока не встретишь ноль.
Т.е. терминирующий '/0' место не занимает, ага.
Тупое решение было, а не для экономии памяти. И обернулось оно множеством проблем в будущем.Предпреждая комментарии, что это один байт -- на PDP-11, где язык С был применён массово, минимально в памяти адресовалось не меньше слова (16 бит, отсюда и размер char -- по умолчанию 16 бит в большинстве случаев).
Т.о. никаким экономичным способом хранения строки тут и не пахло.
WASD1
20.11.2025 17:33Это было проще для железа: не нужно хранить лишнюю длину, просто иди по памяти, пока не встретишь ноль.
Это наследие PDP-11 (возможно всей архитектуры PDP).
- Там была аппаратная работа с 0-terminated строками.
- Ну её и внесли в стандарт С (локально казалось хорошим решением).
- Оттуда в С++ (локально казалось хорошим решением).
Kotofay
20.11.2025 17:33Да, именно так.
LOOP: MOV @R3+, @R5+
JNZ LOOP
Ну, красота, же!
while( *dst++ = *src++ );

mk2
20.11.2025 17:33Это экономно с точки зрения хранения самого указателя. Указатель занимает в памяти/на стеке один размер указателя (а не два), передаётся как аргумент в одном регистре (а не в двух), и так далее.

Kotofay
20.11.2025 17:33Т.е. сделать --ptrStr и получить размер строки никак?
Зачем два указателя то?

Zalechi
20.11.2025 17:33Прочитав статью я сделал такой вывод:
«Странная картина вырисовывается: Комитет просто не может родить стройную систему, потому что вечно не успевает за хотелками железа и маркетинга. Бизнесу надо портировать на XBOX "вчера"? Окей, берем костыль, а не пишем архитектуру. Никто не будет ждать "правильного" стандарта. Вот и получается, что мы вынуждены скрещивать носорогов с бегемотами, просто чтобы продакшн не встал. Ощущение, что язык формируют не инженеры, а дедлайны».
Прогнал мой вердикт через ллм, и она меня поправила:
«Вообще интересная картина вырисовывается.
Кажется, что Комитет тут даже не при чем — он просто не может создать "серебряную пулю". Статья отлично показывает, что единую стройную систему построить невозможно физически: бизнесу нужно "вчера и на XBOX", эмбеддерам нужно без аллокаций на стеке, а геймдеву — чтобы парсилось за 0 тактов.
Получается, этот зоопарк — неизбежная цена за то, что C++ пытается усидеть на всех стульях сразу. Мы скрещиваем носорогов с бегемотами не потому, что не умеем кодить, а потому что универсальный инструмент проиграет специализированному в каждой конкретной нише.
Хотя впечатление бардака, конечно, не отпускает».

dalerank Автор
20.11.2025 17:33Все так, единственная поправочка, бизнес уже продал игру с костылем владельцу xbox, выплатил зп разрабам и рекламирует новую игру, где здесь место и время для поиска идеальной строки в степях дедлайнов решительно непонятно. А если серьезно, то есть устоявшийся набор решений eastl::fixed_string, fstring, xstring и попытки заиспользовать чтото другое быстро отсекутся на ревью думающим лидом. Бардак конечно, но бардак управляемый...

Zalechi
20.11.2025 17:33Да я кстати не делал акцент на дедлайнах, скорее на «реактивности» — все здесь и сейчас, баги исправлять потом будем.
Вот короче оригинал-оригинал:
вот статья на хабре: https://habr.com/ru/articles/968536/
нормально будет так ответить:
»Вообще странная конечно картина по выводам нарисовывается:
Комитет не может организовать стройную систему из-за быстро измененяющегося рынка железа и бизнеса с маркетингом, которые вечно пытаются сэкономить денег. Надо портировать срочно на XBOX? Окей, зачем нанимать новую команду, писать весь стек с нуля? — Не-а, в диком бизнесе так не работает. Нам нужно всё здесь и сейчас. Вот и начинают скрещивать носорогов с бегемотами по сути.
Не знаю. У меня лично такое впечатление сложилось.»?
WASD1
20.11.2025 17:33Так это ровно как и должно быть для не-киллер фичи языка.
Если у вас есть несколько значимых критериев - обычно на практике означает что у вас есть целый набор парето-оптимальных состояний.
И для того, чтобы выбрать "лучшее" из этого набора парето-оптимальных состояний вам надо знать какой из критериев важнее. Обычно для этого надо уметь угадывать будущее, что сложно.
гипотетический пример:virtue(std::string) = { 5/* идеоматичность */, 1/*производительность*/}; virtue(std::string_view) = { 2/* идеоматичность */, 4/*производительность*/}Заметьте - вы не можете сказать "что лучше" {5, 1} или {2, 4} пока вам не скажут что для вас важнее "идеоматичность" или "производительность".
А когда стало известно, какой критерий важнее, - выясняется что правильный тип данных нужен вчера!
Zalechi
20.11.2025 17:33Заметьте - вы не можете сказать "что лучше" {5, 1} или {2, 4} пока вам не скажут что для вас важнее "идеоматичность" или "производительность".А когда стало известно, какой критерий важнее, - выясняется что правильный тип данных нужен вчера!
Ну я пытаюсь сказать, что бизнес все время лихорадит

vvzvlad
20.11.2025 17:33Огонь статья. Сколько же вы придумали животных для нее?

dalerank Автор
20.11.2025 17:33Вопрос в другом, о скольких животных мы не знаем или забыли? Я например вот не знал про TString и Штроку, а могли выйти хорошие экземпляры, и к сожалению забыл написать про AnsiString/MFCString, хотя потратил на работу с ними лет пять жизни точно

Playa
20.11.2025 17:33Что? Строка со счётчиком ссылок в стандартном C++? Знакомьтесь: std::runtime_error.

cdriper
20.11.2025 17:33Может стоит все-таки признать, что содержание и корректность важнее, чем картинка с дикобразом и аллегории из серии "кролики скачут друг на дружке в норе и месят глину"?
Я не эксперт по биологии, но в C++ разбираюсь чуть лучше автора сего графоманского опуса. Это типичный материал в духе "стремительным домкратом", написанный человеком, который "плавает", что в юникоде, что в языке C++, что в игровых движках, что в истории вычислительной техники.
Перечислю только некоторые грубые ошибки и неточности, которые сразу бросились в глаза.
Главная магия фиксированных массивов в том что они живут на стеке. Объявление char buf[N] резервирует байты прямо там и гарантирует что черепаха всегда дома.
Наглая ложь. Кроме стека, массив в Си может быть глобальным или находиться в куче.
Если поле типа char buf[N] объявлено полем в структуре, то вообще невозможно сказать, где этот массив будет находиться.Литералы (...) помещаются в секцию .rodata, которую операционная система помечает как read only после загрузки программы. Попытка записать туда что-то вызывает немедленный segfault
Секция .rodata это секция в файлах формата ELF, который не имеет вообще никакого отношения к языку C++ и его стандарту. Равно как и к стандарту языка Си.
А еще ELF файл может быть загружен операционной системой, которая не поддерживает защиту памяти (например, из-за аппаратных ограничений). Поэтому немедленного segfault может и не быть.std::string (C++03/11)
Нет никакого C++03/11. C++03 это абсолютно минорный апдейт стандарта C++98.
Стандарт C++11 это революция в языке, "совершенно новый язык".Поэтому C++03 и C++11 в одну кучу будет смешивать только тот, кто вообще ничего не знает про эти стандарты.
И так же не знает, что std::string появился в C++98.
В природе в принципе не существует стандарта C++ в котором нет этого типа, поэтому приписки типа "C++03/11" это бессмыслица.
Добавили RAII, методы, автоматическое управление памятью
Это просто набор случайных слов.
Добавили классы с конструкторами, деструкторами и методами. Автоматические управление ресурсами (в том числе памятью) через вызов конструктора и деструктора стали называть RAII.хватает классических проблем с (...) весёлой и порой непредсказуемой работой SSO
А можно поподробнее? Где там веселье и непредсказуемость? По слухам какая-то магия? Кролики съели кошачий корм, а обиделись на них за это рыбки?
Каждый раз когда строку передавали в функцию через const std::string& существовал риск, что внутри функции кто нибудь хитрый создаст новую строку из литерала для сравнения
В примере, который это иллюстрирует, никто новую строку ВНУТРИ функции не создает. Строка создается снаружи. Просто для того, чтобы соответствовать аргументам функции compare().
И я в упор не вижу описанного риска, ибо сравнение внутри функции будет выглядеть как s == "abc".
Понимания мотивации создания string_view у автора нет.
Просто посмотреть это теперь вообще философия C++17 - меньше копирования, больше умных view типов и больше производительности через zero cost абстракции и мелкие хаки.
Я хорошо знаю стандарты и "просто посмотреть" это не философия C++17. Это просто кто-то разогнался на собачках, кошечках, кроликах и незаметно для себя дошел до оценки философии стандарта языка, который толком и не знает.
На string_view теперь пишут высокопроизводительный код хитро организуя время жизни полноценных строк.
Точно такой же высокопроизводительный код писали и будут писать на основе std::string.
И нет тут никакой хитрости. Если ошибешься с временем жизни, то получишь "немедленный segfault".
В 1998 году вышел C++98 со своим std::string
Таааак! Так вон же выше написано, что тип появился в C++03/11! "К середине 2000-х годов std::string стал стандартом".
Попытки подружить QString со std::string приводили к созданию временного QByteArray через toUtf8
Конвертация QString в string опирается на локаль операционной системы, которая, мягко говоря, далеко не всегда UTF-8.
И вообще, зачем впихивать невпихуемое и юникод строку утрамбовывать в тип, который не предназначен для хранения такого рода данных?
Конструкции вроде str.toLower или str.split вообще издалека можно спутать с Python
Та ладно! Их можно спутать с любым языком, где есть класс строка с методами. А именно Java, C#, JavaScript, Swift, Kotlin, Go, Scala... Что ж мы так рано остановились?
Вся магия как обычно в Qt спрятана глубоко в PImpl и внутри их мета системы, которая по слухам умеет всё.
Большинство классов в Qt построено на PImpl. И что? К чему это?
И мета система к вещам типа QString::split() вообще не имеет никакого отношения. Такая вот магия. По слухам.
Windows пошла своим путём и выбрала 16 бит, потому что их API базировался на UTF-16, но это были свои, уникальные 16 бит.
Ничего уникального не было. Это был и есть стандарт UTF-16 LE.
а MacOS вообще начала отходить от wchar_t в сторону собственной реализации
MacOS никуда не отходила. В качестве юникода был выбран стандартный UTF-16.
В компиляторах на платформе wchar_t был UTF-32.
Плюсы на маках всегда были гражданами второго сорта, поэтому Apple тут просто спустила все на тормозах, вместо того, чтобы привести в соответствие.Попытки конвертировать между std::string и std::wstring на Windows превращались в многочасовое путешествие по документации
Как я уже писал выше, потому что это неоднозначное и очень опасное преобразование само по себе. Как значение double сохранить в char. Ну или как пенис слона засунуть в вагину кролика, если использовать более близкие к статье аналогии.
И даже в эпоху, когда не было LLM, даже программисты, никогда в жизни не видевшие Win32 API, в течении минуты находили MultiByteToWideChar и WideCharToMultiByte.
до появления std::filesystem в C++17 единственным способом открыть файл с нелатинским именем на Windows было конвертировать путь в std::wstring и использовать нестандартные расширения компилятора
Ложь. До появления "просто посмотреть" стандарта (в котором, наверное, и файлы открываются исключительно в режиме "только чтение", чтобы только посмотреть) существовал тысяча и один способ открыть файл с юникод именем под Windows.
Например, используя функцию CreateFileW().
Или через boost filesystem.
Или через QFile.
Или...
Если вы посмотрите на malloc в стандартном std::string (...)
То вы его там будите очень долго искать. И не найдете.
Потому что по стандарту std::string обязан использовать new char[].
Вся архитектура, весь networking, весь рендеринг были заточены под коридоры и арены жанра. И строки в движке были утилитарными инструментами для этой цели - имена уровней, названия оружия, debug сообщения, network packets.
Строки это строки. Они никак не были "заточены" под коридоры и название оружия. Ходили слухи, что там была какая-то магия и их даже можно было использовать для имен собачек, кошечек и утконосов.
МакКарти внес фундаментальную идею о неизменяемых объектах-идентификаторах: в программах идентификаторы сравниваются тысячи раз но создаются редко. Имена переменных, функций, классов - всё это известно на этапе написания кода и почти не меняется в рантайме, тогда зачем сравнивать их посимвольно каждый раз?
Да, да, классы в LISP 60-х годов. У которых почти иногда никогда редко не меняется имя в рантайме.
Или это мы в C++ программе постоянно посимвольно сравниваем имен классов и переменных? И почти иногда всегда меняем в рантайме... Магия!

cdriper
20.11.2025 17:33до появления std::filesystem в C++17 единственным способом открыть файл с нелатинским именем на Windows
И это я еще на написал, про возможность в лоб использовать имя, закодированное в текущей локали.

dalerank Автор
20.11.2025 17:33Хабр - открытая площадка, где каждый может написать материал в том стиле, который предпочитает нужным. Надеюсь вам не составит большого труда сделать статью об этом, бо время для комментария с подробным анализом нашлось же... Тегните меня там - покритикую ваш опус в духе стремительного домкрата

wander
20.11.2025 17:33Конвертация QString в string опирается на локаль операционной системы, которая, мягко говоря, далеко не всегда UTF-8.
Так было до Qt5. Теперь toStdString() всегда приводит к toUtf8() под капотом.

bodyawm
Ну круто же) Спасибо, интересно было почитать историю становления строк.
> CRC32
Как насчет коллизий? Вот допустим у строк A и B коллизия по именам (её шанс мал, но не равен нулю), как не вляпаться? На этапе сборки бандла проверять?
dalerank Автор
В том диапазоне текста, что набирается при использовании тегов, имен текстур звуков и моделей в игровых движках - я коллизий за все время работы встречал может пару раз. И заканчивалось это изменением имени текстуры, либо переход на 64битный хеш, ибо по времени сравнения u32/u64 разницы нет
Notevil
А можно ли понять, что случилась коллизия не поймав баг при отладке?
bodyawm
При сборке бандла можно)
Notevil
а как? там какой-то особый механизм для этого есть? Или сторонняя утилита для проверки?
bodyawm
Самый "тупой" способ - это посчитать хэш для всех строк в бандлах и затем для каждой текстовой строки найти потенциальную хэш-коллизию.
dalerank Автор
если есть подозрение на коллизию, делаем дабажный ID который хранит еще и строку из которой был сделан и запускаем с ним. Ловим колиизию и думаем как фиксить.
AlexSky
А зачем хеши? Почему просто на этапе компиляции не собрать все уникальные строки в таблицу и пользоваться указателями? На этапе компиляции, конечно, хеширование имеет смысл, но тут можно и коллизии отловить.
Со ссылками вместо хешей мы, к тому же, получаем доступ к содержимому строки не теряя в скорости сравнения.
bodyawm
Ну так строки с именами ассетов могут быть как часть других ассетов. Например материалы ссылающиеся на текстуры.
AlexSky
А, строка может прийти извне, понял. Почему-то подумал только про сравнение строковых литералов.
dalerank Автор
Строки в самом движке это 1% использования, в основном строки приходят из ассетов и ссылки на них держать не получится. На этом кадре ассет есть, на следующем уже нет. Так что либо копия, либо ее сурогатт