Возможность реализации COW std :: string в C ++ 11

Сегодня я прошел мимо этого SO-вопроса: Законность реализации COW std :: string в C ++ 11

Ответ на этот вопрос, получивший наибольшее количество голосов (35 голосов), гласит:

Это не разрешено, потому что согласно стандарту 21.4.1 p6 недействительность итераторов / ссылок разрешена только для

- в качестве аргумента любой стандартной библиотечной функции, принимающей в качестве аргумента ссылку на неконстантную basic_string.

- Вызов неконстантных функций-членов, кроме operator [], at, front, back, begin, rbegin, end и rend.

Для строки COW вызов неконстантного оператора [] потребует создания копии (и признания недействительными ссылок), что запрещено в параграфе выше. Следовательно, больше не разрешено иметь строку COW в C ++ 11.

Мне интересно, действительно ли это обоснование, потому что, похоже, C ++ 03 имеет аналогичные требования для недействительности строкового итератора:

Ссылки, указатели и итераторы, относящиеся к элементам последовательности basic_string, могут быть признаны недействительными при следующих применениях этого объекта basic_string:

  • В качестве аргумента для функций, не являющихся членами, swap () (21.3.7.8), operator >> () (21.3.7.9) и getline () (21.3.7.9).
  • В качестве аргумента для basic_string :: swap ().
  • Вызов функций-членов data () и c_str ().
  • Вызов неконстантных функций-членов, кроме operator [] (), at (), begin (), rbegin (), end () и rend ().
  • После любого из вышеперечисленных применений, кроме форм insert () и erase (), которые возвращают итераторы, первый вызов неконстантных функций-членов operator [] (), at (), begin (), rbegin (), end () или rend ().

Это не совсем то же самое, что и в C ++ 11, но, по крайней мере, то же самое для части operator[](), которую исходный ответ принял в качестве основного обоснования. Итак, я полагаю, чтобы оправдать незаконность реализации COW std :: string в C ++ 11, необходимо процитировать некоторые другие стандартные требования. Здесь нужна помощь.


Этот вопрос SO был неактивным более года, поэтому я решил поднять его как отдельный вопрос. Пожалуйста, дайте мне знать, если это неуместно, и я найду другой способ развеять свои сомнения.


person goodbyeera    schedule 03.03.2014    source источник
comment
Также актуально: open-std.org/JTC1 /SC22/WG21/docs/papers/2008/n2534.html   -  person dyp    schedule 03.03.2014
comment
Более того: open-std.org/ jtc1 / sc22 / wg21 / docs / paper / 2008 / n2668.htm   -  person pmr    schedule 03.03.2014
comment
В примечании под цитируемым абзацем в C ++ 03 говорится, что правила предназначены, чтобы разрешить реализацию COW. Я думаю, что последний пункт маркера также делает исключение для первого вызова non -member operator[], но я почему-то нахожу формулировку довольно непонятной.   -  person dyp    schedule 03.03.2014
comment
@dyp: Да, действительно есть различия в этих требованиях к недействительности между 03 и 11, и эти различия подразумевают намерение сделать корову возможной в 03 и невозможной в 11, но пока я не могу понять логическую цепочку, по крайней мере не с тем, что сказано в исходном ответе на связанный вопрос SO. Я постараюсь прочитать документы open-std, предоставленные вами и pmr, чтобы узнать, могу ли я получить какие-то мысли.   -  person goodbyeera    schedule 03.03.2014
comment
Я думаю (но пока не могу доказать), что C ++ 03 позволяет при первом вызове operator[] скопировать строку и аннулировать любые ссылки / итераторы, которые ссылаются на строку, для которой был вызван operator[].   -  person dyp    schedule 03.03.2014
comment
Последний процитированный маркер допускает COW. По сути, это говорит о том, что после операции, которая делает ссылки недействительными, последующая первая операция изменения также может сделать ссылки недействительными. Это позволяет отложить признание недействительности до фактического изменения строки.   -  person zch    schedule 03.03.2014
comment
@dyp Во всяком случае, это намерение. Но вы правы, реальный текст далеко не ясен. (Полное объяснение того, что стоит за формулировкой C ++ 03, см. В комментариях французской национальной организации к CD2 C ++ 98 и в последующих обсуждениях.)   -  person James Kanze    schedule 03.03.2014
comment
Интересно, что, хотя вы можете легко реализовать оператор, который работает даже с COW (используя указатель на строку + смещение), справочную проблему решить невозможно. Если бы только std::string последовал совету Скотта Мейерса (пункт 28 Эффективного C ++): Избегайте возврата дескрипторов к внутренним компонентам объекта ...   -  person Matthieu M.    schedule 03.03.2014
comment
@MatthieuM. Эталонную проблему можно решить; и g ++, и более ранние версии Sun CC использовали COW. Проблемы заключаются в следующем: 1) он не покупает вам столько, сколько должен, из-за утечки дескрипторов (вы должны отключить копирование после утечки дескриптора) и 2) его очень сложно эффективно реализовать в многопоточной среде. - есть ошибка в реализации g ++, а реализация Sun CC работает (или, по крайней мере, была) ужасно медленной.   -  person James Kanze    schedule 03.03.2014
comment
@JamesKanze: проблема утечки решена, поэтому я говорю, что она не решена. Если бы мы использовали подход _1 _ / _ 2_, не было бы утечки дескриптора (и ложного срабатывания).   -  person Matthieu M.    schedule 04.03.2014


Ответы (1)


Ключевым моментом является последний пункт стандарта C ++ 03. Формулировка могла бы быть намного яснее, но цель состоит в том, чтобы первый вызов [], at и т. Д. (Но только первый вызов) после чего-то, что установило новые итераторы (и, таким образом, аннулировало старые), могло сделать итераторы недействительными, но только первый. Формулировка в C ++ 03 была, по сути, быстрым взломом, вставленным в ответ на комментарии французского национального органа к CD2 C ++ 98. Исходная проблема проста: подумайте:

std::string a( "some text" );
std::string b( a );
char& rc = a[2];

На этом этапе изменения через rc должны влиять на a, но не на b. Однако, если COW используется, когда вызывается a[2], a и b разделяют представление; для того, чтобы запись через возвращенную ссылку не повлияла на b, a[2] следует рассматривать как «запись», и ему разрешено сделать ссылку недействительной. Вот что сказал CD2: любой вызов неконстантных [], at или одной из begin или end функций может сделать недействительными итераторы и ссылки. Комментарии французского национального органа указали, что это сделало a[i] == a[j] недействительным, поскольку ссылка, возвращенная одним из [], будет недействительна другим. Последний пункт о C ++ 03, который вы цитируете, был добавлен, чтобы обойти это только при первом обращении к [] et al. может сделать итераторы недействительными.

Не думаю, что кто-то был полностью доволен результатами. Формулировка была сделана быстро, и хотя цель была ясна для тех, кто знал историю и исходную проблему, я не думаю, что она была полностью ясна из стандарта. Вдобавок некоторые эксперты с самого начала начали сомневаться в ценности COW, учитывая относительную невозможность самого строкового класса надежно обнаруживать все записи. (Если a[i] == a[j] является полным выражением, записи нет. Но сам строковый класс должен предполагать, что возвращаемое значение a[i] может привести к записи.) А в многопоточной среде затраты на управление счетчиком ссылок, необходимые для Копирование при записи считалось относительно высокой стоимостью того, что вам обычно не нужно. В результате большинство реализаций (которые поддерживали многопоточность задолго до C ++ 11) в любом случае отходят от COW; Насколько мне известно, единственной крупной реализацией, все еще использующей COW, был g ++ (но в их многопоточной реализации была известная ошибка) и (возможно) Sun CC (который в последний раз, когда я смотрел на него, был чрезмерно медленным из-за стоимость управления счетчиком). Я думаю, что комитет просто избрал то, что им казалось самым простым способом навести порядок, запретив COW.

РЕДАКТИРОВАТЬ:

Еще несколько пояснений относительно того, почему реализация COW должна аннулировать итераторы при первом вызове []. Рассмотрим наивную реализацию COW. (Я просто назову это String и проигнорирую все проблемы, связанные с трейтами и распределителями, которые здесь не имеют отношения к делу. Я также проигнорирую исключения и безопасность потоков, просто чтобы упростить задачу.)

class String
{
    struct StringRep
    {
        int useCount;
        size_t size;
        char* data;
        StringRep( char const* text, size_t size )
            : useCount( 1 )
            , size( size )
            , data( ::operator new( size + 1 ) )
        {
            std::memcpy( data, text, size ):
            data[size] = '\0';
        }
        ~StringRep()
        {
            ::operator delete( data );
        }
    };

    StringRep* myRep;
public:
    String( char const* initial_text )
        : myRep( new StringRep( initial_text, strlen( initial_text ) ) )
    {
    }
    String( String const& other )
        : myRep( other.myRep )
    {
        ++ myRep->useCount;
    }
    ~String()
    {
        -- myRep->useCount;
        if ( myRep->useCount == 0 ) {
            delete myRep;
        }
    }
    char& operator[]( size_t index )
    {
        return myRep->data[index];
    }
};

А теперь представьте, что будет, если я напишу:

String a( "some text" );
String b( a );
a[4] = '-';

Какое значение будет после этого b? (Просмотрите код вручную, если вы не уверены.)

Очевидно, это не работает. Решение состоит в том, чтобы добавить флаг bool uncopyable; к StringRep, который инициализирован значением false, и изменить следующие функции:

String::String( String const& other )
{
    if ( other.myRep->uncopyable ) {
        myRep = new StringRep( other.myRep->data, other.myRep->size );
    } else {
        myRep = other.myRep;
        ++ myRep->useCount;
    }
}

char& String::operator[]( size_t index )
{
    if ( myRep->useCount > 1 ) {
        -- myRep->useCount;
        myRep = new StringRep( myRep->data, myRep->size );
    }
    myRep->uncopyable = true;
    return myRep->data[index];
}

Это, конечно, означает, что [] сделает недействительными итераторы и ссылки, но только при первом вызове объекта. В следующий раз useCount будет одним (и изображение будет невозможно скопировать). Так что a[i] == a[j] работает; независимо от того, какой компилятор на самом деле вычисляет первым (a[i] или a[j]), второй найдет useCount, равный 1, и ему не придется дублировать. И из-за флага uncopyable,

String a( "some text" );
char& c = a[4];
String b( a );
c = '-';

тоже будет работать, а не изменять b.

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

person James Kanze    schedule 03.03.2014
comment
Большое спасибо за ответ. Все еще немного запутался. Говоря the first call to [], at, etc. (but only the first call) **after something which established new iterators** (and thus invalidated old ones) could invalidate iterators, but only the first., каков something which established new iterators в вашем примере перед вызовом a[2]? Строительство a? Возможен ли вызов data() при передаче в качестве аргумента для конструкции b? - person goodbyeera; 03.03.2014
comment
Спасибо за историческую перспективу по этому поводу. - person Matthieu M.; 03.03.2014
comment
В этой статье: open-std.org/ jtc1 / sc22 / wg21 / docs / paper / 2008 / n2668.htm, в нем говорится: Итераторы и ссылки могут быть признаны недействительными в результате: 1. «мутирующих» операций и; 2. первая операция отмены совместного использования, следующая за операцией изменения. Здесь операция unsharing означает операции, такие как operator [], поэтому в статье говорится то же самое, что и вы. Но я до сих пор не знаю, какая часть mutating в вашем примере приводит к тому, что последующие unsharing a[2] аннулируют итераторы. Не могли бы вы рассказать об этом подробнее? Еще раз спасибо. - person goodbyeera; 03.03.2014
comment
@goodbyeera Я отредактировал свое сообщение, чтобы добавить простой пример кода, который должен прояснить ситуацию. - person James Kanze; 03.03.2014
comment
Заставить его работать в многопоточной среде чрезвычайно сложно. Не могли бы вы подробнее рассказать об этом? Delphi делает именно это. В нем есть UniqueString процедура, которая сначала выполняет проверку, чтобы сравнить счетчик ссылок с 1. Если это удастся, то очевидно, что никакой другой поток не может удерживать ссылку на строку. Если это не удается, он выделяет новую область памяти, копирует исходную строку в эту новую область, использует инструкцию атомарного декремента, чтобы уменьшить счетчик ссылок, и, если новый счетчик ссылок равен нулю, освобождает память. Почему это не работает на C ++? - person ; 04.03.2014
comment
@JamesKanze: Большое спасибо за добавление примеров для лучшей иллюстрации. Я понимаю, почему первый вызов operator [] может сделать итераторы недействительными, и я понимаю, что в вашем примере a[2] необходимо сделать недействительными итераторы, чтобы реализация COW работала правильно. Меня смущает его соответствие требованиям стандарта. (будет продолжено) - person goodbyeera; 04.03.2014
comment
@JamesKanze: (продолжение) Стандарт говорит, что первый вызов operator [], который может сделать итераторы недействительными, должен быть Subsequent to any of the above uses .... В вашем примере единственные две операции до a[2] - это построение a и передача a в качестве аргумента конструкции b (через ссылку на аргумент const). Как это можно отразить на 4 маркированных элементах стандарта C ++ 03? - person goodbyeera; 04.03.2014
comment
@hvd [...] сначала выполняет проверку, чтобы сравнить счетчик ссылок с 1. Если это удастся, то очевидно, что никакой другой поток не может содержать ссылку на строку. Это неправда. К глобальному std::string могут обращаться одновременно несколько разных потоков. Таким образом, поток 1 вызывает uniqueString и находит счетчик ссылок 1, поток 2 делает копию строки, увеличивая счетчик ссылок до 2, поток 1 затем продолжает работу, определив, что копия не требуется, чтобы изолировать строку --- в реализации g ++ это означает установку useCount на -1. - person James Kanze; 04.03.2014
comment
@hvd Когда копия, созданная потоком 2, выходит за пределы области видимости, он видит, что реализация изолирована, и удаляет ее. Оставив глобальный экземпляр, указывающий на удаленную копию. - person James Kanze; 04.03.2014
comment
@goodbyeera Список функций в пункте 4 соответствует функциям, которые, возможно, придется перераспределить в любом случае. Перераспределение означает, что любые предыдущие ссылки, указатели и итераторы недействительны, поэтому новый экземпляр реализации снова может использоваться совместно. Пока из него не потечет внутренняя ссылка. - person James Kanze; 04.03.2014
comment
@JamesKanze Это хороший момент, но он применим только в том случае, если два потока обращаются к одной и той же переменной без какой-либо блокировки, и один из них вносит изменения. Это уже не требуется для надежной работы практически для любого типа, а в реализации Delphi это не приведет к повреждению внутренних данных. Поток 1 увидит, что счетчик ссылок равен 1, и не будет выполнять дальнейшие проверки. Поток 2 приведет к увеличению количества ссылок. Поток 1 продолжит работу, полагая, что он единственный, кто изменит указанную строку, оставив счетчик ссылок как есть. Здесь нет магического значения -1. - person ; 04.03.2014
comment
@JamesKanze Итак, когда переменная в потоке 2 выходит за пределы области видимости, счетчик ссылок уменьшается, снова становится 1, ничего не освобождается и нигде нет висящих указателей. (На самом деле, магическое значение тоже есть, но оно означает другое.) - person ; 04.03.2014
comment
@hvd Нет. Для доступа к одной и той же переменной требуется два потока, но ни один из них не требует ее изменения. Другими словами, он не соответствует требованиям Posix по безопасности потоков. И я не знаю о реализации Delphi, но в C ++ вам явно нужно что-то, чтобы указать, что некоторые внутренние компоненты просочились, что позволяет произвести мутацию позже. - person James Kanze; 04.03.2014
comment
@hvd Проблема примерно такая: std::string x( "..." ); char& c = x[i]; /*...*/ c = 'x';. Что произойдет, если во время /*...*/ кто-то сделает копию x? Выражение c = 'x' не может изменять копию. Так что должен быть какой-то способ запретить совместную реализацию. - person James Kanze; 04.03.2014
comment
@JamesKanze Спасибо, это именно тот пример, который я искал, а также то, что ведет себя не так, как можно было бы ожидать в Delphi. var S1: string; procedure X(var C: Char); var S2: string; begin S2 := S1; C := 'x'; ShowMessage(S2); end; S1 := 'abc'; X(S1[2]); показывает axc. (Я верю, что синтаксис Delphi достаточно читабелен, чтобы его можно было понять.) - person ; 04.03.2014
comment
@hvd ведет себя не так, как можно было бы ожидать от Delphi - Ну, это, возможно, зависит от того, сколько на самом деле ожидается от Borland - ›кошмар Embarcadero, который и есть VCL. Обратите внимание, что String также имеет аналогичную нарушенную реализацию через привязки C ++. Еще хуже, когда вы устанавливаете свойство String объекта, а затем случайно изменяете его через старую ссылку позже. - person Jason C; 11.03.2014
comment
Предоставление резервного хранилища изменяемой строки в качестве указателя (изменяемого или нет) требует, чтобы нераспространяемое резервное хранилище для строки существовало до такого раскрытия. Разве не будет законным отложить некоторые из затрат на создание, связанных со строкой менее 500 триллионов символов, до того момента, когда необходимо будет открыть резервное хранилище? Поскольку многим строкам никогда не нужно открывать хранилище резервных копий, в некоторых случаях это может показаться потенциальным выигрышем в производительности. Создание строк размером более 500 триллионов символов будет медленнее, чем 499 триллионов символов, но ... - person supercat; 13.11.2015
comment
... время раскрытия любой строки будет O (1), а стоимость создания строки длины N и ее раскрытия M раз для меньших значений будет O (N) + O (M). - person supercat; 13.11.2015
comment
Просто ради интереса, почему C ++ 11 явно запрещает аннулирование итератора с помощью []? - person Deqing; 06.07.2016