Как стать автором
Обновить

Комментарии 50

Могло ли взаимодействие с другими библиотеками повлиять на отказ от собственных реализаций?

Скорее переход на с++11 и избавление от легаси, пейпер по cow-строкам не зря отклонили в стандарт, не смотря на лобби со стороны Qt и GCC. Слишком много проблем и мало профита в итоге.

cow это тоже самое, что пара std::string+string_view.

Проблемы есть и там и там, думать и понимать надо и там и там. А отклонили скорее всего, я предполагаю, изза легаси, а не по причине проблем cow. Какой смысл переделывать string на cow, если проще ввести string_view.

Я к тому, что cow ни хуже ни лучше. Это тоже самое, но с другой стороны. А проблемы и косяки и там и там. Тут вопрос удобства применения в конкретном случае. Вы же утверждаете, как я понял, что cow это плохо, а пара string+string_view хорошо. И я для себя хотел бы увидеть подтверждения этому тезису.

это не тоже самое, начнем хотя бы с механизма алокации памяти. Я не сказал, что cow-строки плохой механизм, как и у любого решения у него есть зона использования - которой была строковые хеши и иммутабельные строки, попытки расширить cow-строки для широкого использования и замены string привели к проблемам, которые не смогли решить, поэтому и отказались. В движке был и другой тип строк UStringUnique который являлся аналогом std::string.

Это не то же самое с точки зрения внутренней реализации. Но то же самое, с точки зрения абстрактных строк. COW логически содержит в себе одновременно и string и string_vew как концепции.

 привели к проблемам, которые не смогли решить

Т.е. в юнити не смогли решить. Верно? Вы же пишите так, как будто сама концепция "больна". Поправьте если это не так.

std::string + std::string_view: std::string_view не копирует и не владеет данными, а только предоставляет доступ к части строки (или строкоподобной структуре). Когда создается копия std::string, данные копируются полностью, и каждая строка имеет свой буфер. std::string_view же лишь хранит указатель на данные и длину, избегая дополнительных затрат на копирование, но при этом остается привязанным к сроку жизни исходной строки. Про концепцию COW строк рассказывать?

Давайте я расскажу.
Пока COW-строка одна владеет ресурсом, она не отличается от std::string;
Когда на один ресурс ссылается несколько COW-строк они являются std::string_view. Когда происходит сам COW процесс, идет ветвление. Строка получившая уникальность становится по сути std::string, остальные остаются std::string_view над прошлой строкой.

Как я уже сказал, концептуально они (COW и пара string+string_view) одно и тоже. Отличие в реализации.

Не совсем так, string_view это удобная обертка над const char*, не более.
Cow строки при копировании строки они не создают дублирующий буфер данных, а просто увеличивают счетчик ссылок на общий буфер, пока одна из строк не изменится. При изменении создается полная копия буфера, чтобы изменения не затронули другие строки, ссылающиеся на оригинальные данные. Вы не сможете добиться этого поведения на std::string

Я вам говорю не про std::string, а про пару string+string_view. И без деталей реализации.

даже со связкой string+string_view не получится сделать аналог COW строк

Вы опять про реализацию, когда говорите слово "аналог". А я вам про концепцию. А именно не создавать лишний раз копию.
В паре string+string_view - это делается через использование string_view. В COW это встроенно by-design.

Я бы не сказал, что string_view обёртка над const char*. Всё-таки const char* подразумевает C-строки, а они нуль-терминированные. string_view никаких гарантий насчёт этого не даёт. То есть const char* в string_view можно превратить, а вот наоборот - не факт.

Поскольку строка str находится в состоянии совместного использования, принцип COW заставляет операцию str[0] создать копию общего буфера, чтобы перейти в состояние владения

Почему вы считаете, что операция чтения должна создать копию буфера? )

Потому что эта операция может использоваться не только для чтения:

str[0] = 'x'; // Совершенно корректный код!

Код был так написан, видимо предполагалось что взятие элемента происходит для его изменения, что в 99% так и было. Про эти грабли все знали и просто не пускали такой код на ревью. Моя команда занималась оптимизацией симсов и немного компилятором шейдеров, а починка махрового легаси в задачи не входила :) Четвертый репорт об этой проблеме лег в список сотни аналогичных с пометкой "Wont fix, low prior".

Но это очень странная конструкция:

1. Берем указатель на строку
2. Меняем строку
3. Работаем с указателем на строку

Это точно проблема архитектуры, а не программиста?

Но в примере она используется для чтения (это разные перегруженные операторы). И вопрос остается открытым. )

ага, и в наловленных багах тоже было чтение, которое не должно было менять строку, но приводило к крашу (не всегда). Баг жил в коде около полугода и никак себя не проявлял, пока ниже не сделали вывод в лог и словили мусор. Т.е. две строки, одна в начале функции, вторая внутри скопа использовались в качестве ключей для поиска по мапе, так писать было стандартной практикой, потому что это дешево и не делало фактических копий.

Извините за назойливость, но я не вижу проблем в архитектуре COW. Так же я не вижу бага.

1. Заводим строку 1
2. Берем указатель
3. Заводим внутри скопа вторую
4. Если меняем вторую, то это ее указатель станет другим а не первой
5. Читаем из первой
6. Выводим указатель первой, все хорошо, указатель не менялся

Где тут потенциальный баг изза архитектуры COW? Я вот этот момент хочу понять.

P.S.: Если мы получим указатель на строку из std::string, потом поменяем std::string, то будет ровно такая же ошибка, как в примере, что я писал вам в комментарии выше.

А зачем вы так делаете? Почему нету const char operator[]?

Тут проблема архитектуры реализации COW в юинити, а не самого COW.

Это не я так делаю, это реализация COW-строк из Unity < 4.6. За причинами лучше к авторам движка, видимо были причины. Про баг знали, но не фиксили

А зачем вы так делаете? Почему нету const char operator[]?

Потому что в классе не могут одновременно существовать два метода, перегруженные только по возвращаемому типу:

 <source>:53:20: error: functions that differ only in their return type cannot be overloaded
        char const operator[]( const USize i )
        ~~~~       ^
 <source>:47:15: note: previous definition is here
        char& operator[]( const USize i )

Так а зачем делать char& operator[]?

Это как раз тот метод, который не надо делать в COW строках.

Изначально вы писали:

Но в примере она используется для чтения (это разные перегруженные операторы).

Вы согласны, что погорячились с тем комментарием?

Да, если оператора модификации строки вообще не делать - всё будет работать. Правда, тут возникает вопрос что делать если модифицировать строку всё-таки надо.

Я согласен с тем, что неполно сформулировал мысль. И, конечно, изза этого смысл комментария становится неверным, да.

Мой основной поинт, что в COW-строках нет проблем, а есть проблемы в конкретно этой реализации.

Правда, тут возникает вопрос что делать если модифицировать строку всё-таки надо.

Делать через функу, а не оператор. Т.к. сам по себе в общем виде символ - это не char. У нас же есть к примеру, UTF-8.

Там должен был быть перегруженый const char&, но его почему-то не было, почему отдельный вопрос и не к пользователям движка. Возможно из-за кривого биндинга в моно, который не мог работать с таким перегрузками. Репортов на это поведение было вагон и тележка

const char& operator[]( const USize i ) const
{
   return (*m_buffer)[i];
}


Переписать все на с++23 с дедюсит зис.

Хотя я тут подумал, с++23 не нужен. Просто добавить конст к перегрузке которая конст чар и возвращает.

А разве C++ умеет выбирать перегрузку оператора в зависимости от его использования как lvalue/rvalue?

Нет, не умеет. Он умеет смотреть только на константность самой строки, а она в этом примере неконстантна.

Уверены?

const char first_char = str[0];

Ну вот написали вы const char, что дальше-то? Как оно повлияло на выбранную перегрузку метода? Никак. Ну нет в С++ обратного вывода типов, это вам не Хаскель и не Раст.

Я не написал const char. Я процитировал код из статьи.

Прочитайте еще раз всю ветку.

Я спросил почему чтение вызывает COW?
Вы привели пример с записью.
Я спросил причем тут запись, если вопрос про чтение и привел код из статьи.
Вы вообще куда то в сторону ушли.


P.S.: Если вы имеете ввиду конкретную реализацию из статьи, то мне понятна ваша позиция. Я вопрос задавал, имея ввиду, зачем вообще так сделано.

Это вы куда-то в сторону ушли.

Важно тут то, что str[0] = 'x'; - это совершенно корректный код. Да, если объявить его некорректным - некоторые проблемы уйдут, но он корректный, это данность.

Оператор индексирования не знает, вызвали его для чтения или для записи, поэтому вынужден исходить из того, что возможна запись. Именно поэтому чтение вызывает копирование.

Если бы вы сразу написали, что запись через индексирование выглядит как плохая идея для COW-строк - я бы согласился. Но вы почему-то вместо этого начали рассказывать, что запись от чтения якобы можно отличить перегрузкой, что чушь.

Но вы почему-то вместо этого начали рассказывать, что запись от чтения можно отличить перегрузкой, что чушь.

Вы мне приписали свою интерпретацию. Вот это точно чушь.

Объясняю, когда я говорил о перегрузке, я говорил что это разные подходы, разные перегруженные операторы в разных реализациях. Вы же обсуждали конкретную реализацию COW из статьи. Тогда как я имел ввиду , почему реализация сделана так, что чтение вызывает COW. Так что вам бы уточняющие вопросы научиться задавать, а не додумывать.

Поскольку строка str находится в состоянии совместного использования, принцип COW заставляет операцию str[0] создать копию общего буфера, чтобы перейти в состояние владения

Автор пишет, что принцип COW заставляет операцию чтения создать буфер.

Почему вы считаете, что операция чтения должна создать копию буфера? )

Я спрашиваю почему операция чтения должна создавать копию? Почему принцип COW заставляет операцию чтения создавать копию? Принцип, понимаете? Не реализация в статье, а принцип. Так написал автор статьи.

И вот ваш комментарий.

Потому что эта операция может использоваться не только для чтения:

Причем тут это? Как это относится к тому что я спрашиваю? Вы ушли не туда. Это просто медицинский факт. Вы цепляетесь за дальнейшую фразу про перегрузку, тогда как именно ваш комментарий уводит разговор в "нитуда". Я надеюсь так вам станет яснее, почему вы не правы.

Только вот автор всю статью писал исключительно про конкретную реализацию COW в Unity, а не про абстрактный принцип.

принцип COW заставляет операцию

В цитате он буквально пишет про принцип COW. А не про реализацию COW в юинити. И мой вопрос об этом. Внимательно прочитайте.

Ну вообще можно, начиная с ц++17 есть перегрузка по & и &&. Можно сделать копию только для рвалью, для лвалью оставить без копии. Т.е что то типа

char& operator [](size_t) &&;
char operator [](size_t) & const;
std::move(cow)[0] = 'x';

Но проще конечно просто отдельный метод(ы) иметь мутабельный вместо оператора. Смысл в том, что даже в те времена когда этот класс строки писали можно было сделать правильно, а описываемые баги это НЕ проблема cow, а проблема конкретно этой реализации, хотя в культе тож самое насколько я помню. В реальности же менять часть строки приходится настолько редко, что отдельные методы и правильно и эргономично, а конкретно с коу перегрузка не константного оператора индексации который возвращает ссылку это ошибка дизайна.

Но да оператор не воробей зарелизишь не исправить без поломки совместимости.

Это ломает семантику rvalue/lvalue ссылок. Применение std::move к переменной означает намерение эту самую переменную выкинуть, а тут оно означает прямо противоположное.

Не говоря уже о том, что с таким подходом будут спецэффекты в выражениях вида char c = foo()[0].

Если делать по уму, то надо вообще забыть про хитрые перегрузки, и все изменения выражать явным вызовом метода, к примеру mut:

char c = str[0]; // чтение
str[0] = c; // ошибка
str.mut()[0] = c; // запись

Это решается тем, что оператор индексирования должен возвращать не ссылку, а служебный объект с перегруженными операторами конвертации в char, присваивания и взятия ссылки.

И внутри этого объекта можно сделать уже создание копии при присваивании.

Объект легковесный, содержит в себе только указатель на оригинальный объект строки и запрошенный индекс, в 99% случаев компилятор должен инлайнить всё это.

А зачем работа со строками на каждый фрейм нужна? Я как-то ожидал, что строки -- для удобства программирования, но в рантайме от них должен остаться только запах и указатели, не постоянное генерирование...

Например имена ресурсов генерятся рантайм, как это было в симсах, логи, разные идентификаторы, рантайм имена аи груп, динамические свойства. Должно не значит будет;)

Возможно, это что-то из области UnityScript и его интерпретатора.

Логи может быть. Профайлер. Вывод информации в визуальную часть, то же строки. Это предположение.

Был навигатор под древнее железо без ГПУ, вся рендер часть на интах (эмуляция фиксированной точки). Пару лет переходили на мобилки и С++11, так при попытке вернуться на древние навигаторы оказалось, что код замедлился в 10 раз и оптимизировать слишком дорого, проще отказаться от проекта.

C_str cstr = str.c_str();

очевидно, что конструирование/присвоение с указателя должно копировать данные, а не верить в чудеса

и с COW этот момент вообще никак не связан

Во-первых, у стандартного std::string, под который тут пытаются мимикрировать, c_str() ничего не копирует. И отличие std::string, где этой проблемы нет, от UString, где эта проблема есть - именно в COW.

Во-вторых, если под указатель выделить память - кто её освобождать-то будет?

В-третьих, на кой чёрт вообще нужны строки с COW, если c_str в них делает полную копию? Это прямо противоположно тому, чего пытаются добиться используя COW.

я про то, что если классу строки в конструктор дают char*, то он с этих данных должен снять копию, потому что ты ничего не знаешь про время жизни этого указателя

Так он, вообще-то, снимает. А причём тут вообще c_str?

блин, перечитал код еще раз и понял, что я протупил. сорян.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации