Законност на внедряването на COW std::string в C++11

Разбрах, че копирането при запис не е жизнеспособен начин за прилагане на съответстващ std::string в C++11, но когато наскоро се появи в дискусия, се оказа, че не мога директно да подкрепя това твърдение.

Правилно ли съм, че C++11 не допуска внедрявания на std::string, базирани на COW?

Ако е така, това ограничение посочено ли е изрично някъде в новия стандарт (къде)?

Или това ограничение се подразбира, в смисъл, че комбинираният ефект от новите изисквания върху std::string изключва внедряване на std::string, базирано на COW. В този случай бих се заинтересувал от извеждане на стил на глава и стих на „C++11 ефективно забранява COW базирани std::string реализации“.


person acm    schedule 30.08.2012    source източник
comment
Грешката в GCC за техния низ COW е gcc.gnu.org/bugzilla/ show_bug.cgi?id=21334#c45 . Един от бъговете, проследяващи нова C++11 компилираща реализация на std::string в libstdc++, е gcc.gnu.org/bugzilla/show_bug.cgi?id=53221   -  person user7610    schedule 01.05.2014


Отговори (7)


Не е позволено, тъй като съгласно стандарта 21.4.1 p6, анулирането на итератори/препратки е разрешено само за

— като аргумент за всяка стандартна библиотечна функция, вземаща препратка към неконстантен базов_низ като аргумент.

— Извикване на неконстантни членски функции, с изключение на operator[], at, front, back, begin, rbegin, end и rend.

За низ COW извикването на non-const operator[] ще изисква правене на копие (и обезсилване на препратки), което е забранено от параграфа по-горе. Следователно вече не е законно да имате COW низ в C++11.

person Dave S    schedule 30.08.2012
comment
Някои обосновки: N2534 - person M.M; 18.12.2014
comment
-1 Логиката не издържа. По време на копиране на COW няма препратки или итератори, които могат да бъдат анулирани, целият смисъл на извършването на копирането е, че такива препратки или итератори сега се получават, така че копирането е необходимо. Но все пак може да се окаже, че C++11 забранява внедрявания на COW. - person Cheers and hth. - Alf; 18.12.2014
comment
@Cheersandhth.-Alf: Логиката може да се види в следното, ако COW беше разрешено: std::string a("something"); char& c1 = a[0]; std::string b(a); char& c2 = a[1]; c1 е препратка към a. След това копирате a. След това, когато се опитате да вземете препратката втори път, тя трябва да направи копие, за да получи неконстантна препратка, тъй като има два низа, които сочат към един и същ буфер. Това би трябвало да обезсили първото взето позоваване и противоречи на цитирания по-горе раздел. - person Dave S; 18.12.2014
comment
@DaveS: В случай, че скицирате, няма споделяне на буфер, тъй като има (възможни) външни препратки към буфера на оригиналния низ. Следователно заключението за правене на копие по-късно е погрешно. Само за капак, предположението, че това ще обезсили препратките към буфера на оригиналния низ, също е погрешно, но е много без значение, тъй като късното копиране не се случва тук. В по-общ план е логически невъзможно да се конструира пример, който прави това, което искате, защото избягването на този случай е това, което COW е проектиран да прави. Ето защо всеки опит завършва с безброй грешки. - person Cheers and hth. - Alf; 18.12.2014
comment
@Cheersandhth.-Алф, според това поне Имплементацията на COW на GCC прави точно това, което DaveS казва. Така че поне този стил на COW е забранен от стандарта. - person Tavian Barnes; 18.01.2015
comment
@BenVoigt: Гласуването против е в сила: отговорът е неправилен и се основава на прости заблуди. Читателите могат да демонстрират това на себе си, като се опитат да създадат конкретен пример. (Тавиан Барнс препраща към доклад за грешка за gcc и твърди, че грешката показва, че правилно внедряване не може да работи в рамките на правилата на стандарта. Това отново е заблуда. Това ли според вас показва, че коментарите ми са неправилни?) - person Cheers and hth. - Alf; 05.03.2015
comment
@Alf: Този отговор твърди, че non-const operator[] (1) трябва да направи копие и че (2) е незаконно да го направи. С коя от тези две точки не сте съгласни? Разглеждайки първия ви коментар, изглежда, че внедряването може да сподели низа, поне при това изискване, до момента, в който бъде осъществен достъп, но и достъпът за четене, и записът ще трябва да го отмени. Това ли е вашето разсъждение? - person Ben Voigt; 05.03.2015
comment
@BenVoigt: цитирам себе си, по време на копиране на COW няма препратки или итератори, които могат да бъдат анулирани. Твърдението, че копирането по необходимост обезсилва препратките, е просто погрешно. Не може да се конструира пример, защото не е верен. И съм почти сигурен, че вече сте го разбрали. - person Cheers and hth. - Alf; 22.03.2015
comment
За по-голяма яснота, под COW копиране говорите ли за увеличаване на броя на споделянията или за отказ от споделяне? - person Ben Voigt; 22.03.2015
comment
Несподелянето. съжалявам Мислех, че това копие ще означава ... копиране. - person Cheers and hth. - Alf; 22.03.2015
comment
@BenVoigt: Добавих правилен отговор. Противно на това, което очаквах (но тъй като останах отворен в първия си коментар тук в този списък), наистина е практически невъзможно да се осигури съответстваща имплементация с преброени препратки за C++11 и по-нови, поради нови изисквания за неизхвърляне. - person Cheers and hth. - Alf; 22.03.2015
comment
@Cheersandhth.-Алф, какво не е наред с разсъжденията ми в gcc.gnu .org/bugzilla/show_bug.cgi?id=21334#c47 ? - person Jonathan Wakely; 22.03.2015
comment
@JonathanWakely: Не виждам нищо лошо в това. Това е добър доклад за грешка. Вашите разсъждения (или по-скоро наблюдения) там изглеждат 100% валидни. От друга страна, мнението, че този бъг означава нещо за формалното, което някой друг е твърдял, е глупост. - person Cheers and hth. - Alf; 22.03.2015
comment
Нека продължим тази дискусия в чата. - person Jonathan Wakely; 22.03.2015
comment
@Cheersandhth.-Алф, добавих мой собствен отговор, който просто разширява правилния отговор от Дейв С по-горе. Може би, ако сега прочетете отново коментарите ми по-горе, ще видите, че исках да предоставите някакво оправдание за собствените си привидно необикновени твърдения като От друга страна, мнението, че този бъг означава нещо за формалното, че някой друг твърди, е глупост. (това беше моето собствено мнение, което ти определи като глупост, но аз съм този, обвинен в атаки ad hominem и ставам личен, когато използвам същата дума! Колко обидно!) - person Jonathan Wakely; 23.03.2015
comment
@DaveS Наистина ли е невалиден? Защото c1 е все още жив до b. Вярно е, че &c1 == &a[0] вече не е вярно, но c1 е препратка към обект (района за съхранение на самия символ), а не към име (a[0]), и обектът все още е жив, така че препратката е добра -дефиниран. - person Peregring-lk; 29.10.2016

Отговорите на Dave S и gbjbaanb са правилни. (И това на Люк Дантон също е правилно, въпреки че е по-скоро страничен ефект от забраната на низовете COW, отколкото първоначалното правило, което го забранява.)

Но за да изясня някои обърквания, ще добавя още малко изложение. Различни коментари препращат към мой коментар за GCC bugzilla, който дава следния пример:

std::string s("str");
const char* p = s.data();
{
    std::string s2(s);
    (void) s[0];
}
std::cout << *p << '\n';  // p is dangling

Смисълът на този пример е да демонстрира защо преброеният низ (COW) на GCC не е валиден в C++11. Стандартът C++11 изисква този код да работи правилно. Нищо в кода не позволява p да бъде анулиран в C++11.

Използвайки старата референция std::string на GCC, този код има недефинирано поведение, защото p е невалиден, превръщайки се във висящ указател. (Това, което се случва е, че когато s2 се конструира, той споделя данните с s, но получаването на неконстантна препратка чрез s[0] изисква данните да не бъдат споделяни, така че s прави „копиране при запис“, тъй като препратката s[0] потенциално може да се използва за запишете в s, след което s2 излиза извън обхвата, унищожавайки масива, към който сочи p).

Стандартът C++03 изрично разрешава това поведение в 21.3 [lib.basic.string] p5, където се казва, че след извикване на data() първото извикване на operator[]() може да направи невалидни указатели, препратки и итератори. Така низът COW на GCC беше валидна C++03 реализация.

Стандартът C++11 вече не позволява това поведение, тъй като нито едно извикване на operator[]() не може да направи невалидни указатели, препратки или итератори, независимо дали следват извикване на data().

Така че примерът по-горе трябва да работи в C++11, но не работи с вида COW низ на libstdc++, следователно този вид COW низ не е разрешен в C++ 11.

person Jonathan Wakely    schedule 22.03.2015
comment
Реализация, която прекратява споделянето при извикване на .data() (и при всяко връщане на указател, препратка или итератор), не страда от този проблем. т.е. (неизменен) буферът е несподелен по всяко време или в противен случай е споделен без външни препратки. Мислех, че сте възнамерявали коментара за този пример като неофициален доклад за грешка като коментар, много съжалявам за погрешното разбиране! Но както можете да видите, като разгледате такава реализация, както описвам тук, която работи добре в C++11, когато изискванията noexcept се игнорират, примерът не казва нищо за формалното. Мога да предоставя код, ако искате. - person Cheers and hth. - Alf; 23.03.2015
comment
Ако прекратите споделянето при почти всеки достъп до низа, губите всички предимства на споделянето. Реализацията на COW трябва да е практична, за да може стандартната библиотека да си прави труда да използва това като std::string и аз искрено се съмнявам, че можете да демонстрирате полезен, производителен низ на COW, който отговаря на изискванията за обезсилване на C++11. Така че твърдя, че спецификациите noexcept, които бяха добавени в последния момент, са следствие от забраната на COW низовете, а не основната причина. N2668 изглежда напълно ясен, защо продължавате да отричате ясното доказателство за намерението на комитета, очертано там? - person Jonathan Wakely; 23.03.2015
comment
Освен това не забравяйте, че data() е константна членна функция, така че трябва да е безопасно да се извиква едновременно с други константни членове и например да се извиква data() едновременно с друга нишка, правеща копие на низа. Така че ще имате нужда от всички режийни разходи на mutex за всяка операция с низ, дори константни, или сложността на незаключваща променлива структура с преброени препратки, и след всичко това получавате само споделяне, ако никога не модифицирате или осъществявате достъп до низовете си, толкова много, много низове ще имат референтен брой от един. Моля, предоставете код, не се колебайте да игнорирате noexcept гаранциите. - person Jonathan Wakely; 23.03.2015
comment
Просто събрах малко код, сега открих, че има 129 basic_string членски функции плюс безплатни функции. Разходи за абстракция: този стандартен неоптимизиран нов код на нулева версия е 50 до 100% по-бавен както с g++, така и с MSVC. Не осигурява безопасността на нишките (достатъчно лесно използване на shared_ptr, мисля) и е достатъчно, за да поддържа сортиране на речник за целите на времето, но модулни грешки доказва, че преброената препратка basic_string е разрешена, с изключение на изискванията на C++ noexcept . github.com/alfps/In-principle-demo-of -ref-counted-basic_string - person Cheers and hth. - Alf; 23.03.2015
comment
shared_ptr няма да ви помогне да заобиколите проблема с безопасността на нишките в последния ми коментар: трябва да можете да извиквате data() едновременно в две нишки, което означава разпределяне на нов низ и замяна на текущо задържаните знаци. shared_ptr не поддържа едновременни актуализации, само едновременни четения. Така че все още се нуждаете от мютекс. Освен това, ако имате const string, неговият shared_ptr член ще бъде const и така или иначе не може да бъде актуализиран. Като се има предвид, че основната мотивация за забрана на низовете COW беше безопасността на нишките, не трябва да се опитвате да го закрепите след това, трябва да го проектирате. - person Jonathan Wakely; 23.03.2015
comment
Добре, виждам mutable на вашия shared_ptr член. Това обаче не помага при надпреварите за данни. Няма нужда да губите време с поддръжка на разпределител, това е ортогонално на разглеждания въпрос. По същия начин присвояване/замяна/намиране и т.н. - person Jonathan Wakely; 23.03.2015
comment
Простата идея за безопасност на нишката на const функции е да копирате shared_ptr в локална във всяка такава функция, след което да използвате локалната. Вярвам, че има някои атомни поддържащи функции, които могат да направят това копиране? Не ги използва. - person Cheers and hth. - Alf; 23.03.2015
comment
Да, можете да ги използвате за атомно актуализиране на shared_ptr, сега всичките ви низове споделят глобален набор от мютекси, които трябва да използвате при всяко извикване на data() или operator[] или begin() дори на константен низ. Здравей раздор. И пак ще имате TOCTTOU състезание в Refcounted_string_::ensure_unique_buffer(). Може да успеете да докажете, че C++11 COW низ е възможен, ако вашата реализация на std::lib е щастлива да унищожи напълно производителността на програмата. Но в реалния свят това не е опция. - person Jonathan Wakely; 23.03.2015
comment
Също така не е достатъчно да използвате атомарните операции във всяка такава функция, това е надпревара за данни, за да направите напр. return p_buffer_->capacity(); в една нишка, докато друга нишка прави atomic_load(&p_buffer_), така че трябва да използвате атомарните операции за всеки един достъп до p_buffer_. - person Jonathan Wakely; 23.03.2015
comment
Изгледите, които рисувате за mutexes и т.н., са малко прекалено богати (не съм сигурен, че съобщих това достатъчно добре), но мисля, че е безопасно да се каже, че точно както noexcept ограниченията са формалните бариери, които са налице за изпълнение с преброяване на препратки, така че изискванията за безопасност на нишките са практическите бариери, които оказват влияние върху производителността в определени сценарии. Мисля, че може би можем да се съгласим с това. След това остава тълкуването на това, което OP означава с COW, където виждам разумен референтен преброен тип низ, който е само формално спрян от noexcept. - person Cheers and hth. - Alf; 23.03.2015
comment
влияние върху производителността в определени сценарии. ROFL. Добре, сега опитайте да изпълните изискванията за сложност в 21.4.5 [string.access]. noexcept не е проблемът. Добре е понякога да грешиш, Алф. - person Jonathan Wakely; 23.03.2015
comment
Нека продължим тази дискусия в чата. - person Jonathan Wakely; 23.03.2015
comment
Добро наблюдение, това са нови изисквания на C++11 и те не могат да бъдат изпълнени от внедряване на COW. В допълнение към noexcept не може да се срещне. Относно „ОК, ако греша“, ще изтриете ли този отговор? Когато имам време (съжалявам, вече използвах твърде много), ще актуализирам отговора си с двете ви наблюдения: че (1) C++03 направи специално изключение за извиквания за първи път, където напр. s[i] може да направи невалиден указател, получен от s.data(), въпреки че като цяло клиентският код не може да знае дали извикването е за първи път или не, и (2) нови O(1) изисквания за сложност на операциите с низове, където C++03 нямаше такива . - person Cheers and hth. - Alf; 23.03.2015
comment
@JonathanWakely: Мога да си представя някои случаи, в които низ, който споделя резервни хранилища, докато не бъде достъпен, може да бъде полезен. Например, свързването на два низа със споделени резервни хранилища може да доведе до нов обект на низ, който съдържа препратки към двете резервни хранилища; свързването на това с нещо друго може да изгради дърво. Ако междинните низове не се използват за никакви цели, освен като фураж за бъдещи конкатенации, такъв подход може да изглежда полезен. Не би било точно копиране при запис в обичайния смисъл, но идеята за споделяне на данни, стига да е практично, ще остане. - person supercat; 12.11.2015
comment
@supercat, това би добавило много режийни разходи, но би песимизирало всички употреби, които не включват конкатениране на низове. Вижте също мнението, което отбелязах в stackoverflow.com/questions/12199710/ - person Jonathan Wakely; 13.11.2015
comment
−1 Отговорите на Dave S и gbjbaanb са правилни. е лъжа. По-специално, твърдението на Дейв е ирационално, с език, който има вид и усещане за логика, но не е. Заключението в края на този отговор, че бъгавата COW в libstdc++ на g++ не е разрешена, е ОК. - person Cheers and hth. - Alf; 29.07.2016

Така е, CoW е приемлив механизъм за създаване на по-бързи низове... но...

прави многонишковия код по-бавен (цялото това заключване, за да се провери дали сте единственият, който пише, убива производителността, когато използвате много низове). Това беше основната причина CoW да бъде убит преди години.

Другите причини са, че операторът [] ще ви върне данните за низа, без никаква защита за вас да презапишете низ, който някой друг очаква да бъде непроменен. Същото важи и за c_str() и data().

Quick google казва, че многонишковостта е основно причината, поради която е ефективно забранено (не изрично).

В предложението се казва:

Предложение

Ние предлагаме да направим всички итератори и операции за достъп до елемент безопасно едновременно изпълними.

Увеличаваме стабилността на операциите дори в последователен код.

Тази промяна ефективно забранява внедряването на копиране при запис.

следван от

Най-голямата потенциална загуба на производителност, дължаща се на преминаване от реализации на копиране при запис, е увеличеното потребление на памет за приложения с много големи низове, предимно за четене. Ние обаче вярваме, че за тези приложения въжетата са по-добро техническо решение и препоръчваме да се разгледа предложение за въже за включване в библиотека TR2.

Въжетата са част от STLPort и SGIs STL.

person gbjbaanb    schedule 30.08.2012
comment
Проблемът с operator[] всъщност не е проблем. Константният вариант предлага защита, а неконстантният вариант винаги има опцията да изпълни CoW по това време (или да бъде наистина луд и да зададе грешка на страницата, за да я задейства). - person Christopher Smith; 30.09.2014
comment
+1 Преминава към проблемите. - person Cheers and hth. - Alf; 18.12.2014
comment
просто е глупаво, че клас std::cow_string не е включен, с lock_buffer() и т.н. Има много пъти, когато знам, че нишката не е проблем. по-често, отколкото не, всъщност. - person Erik Aronesty; 16.04.2015
comment
Харесва ми предложението за алтернатива, т.е. въжета. Чудя се дали има други алтернативни видове и реализации. - person Voltaire; 09.05.2019

От 21.4.2 basic_string конструктори и оператори за присвояване [string.cons]

basic_string(const basic_string<charT,traits,Allocator>& str);

[...]

2 Ефекти: Конструира обект от клас basic_string, както е посочено в таблица 64. [...]

Таблица 64 полезно документира, че след изграждането на обект чрез този (копиращ) конструктор, this->data() има като стойност:

сочи към първия елемент на разпределено копие на масива, чийто първи елемент е насочен от str.data()

Подобни изисквания има и за други подобни конструктори.

person Luc Danton    schedule 30.08.2012
comment
+1 Обяснява как C++11 (поне частично) забранява COW. - person Cheers and hth. - Alf; 18.12.2014
comment
Съжалявам, бях уморен. Това не обяснява нищо повече от това, че извикването на .data() трябва да задейства COW копиране, ако буферът е споделен в момента. Все пак това е полезна информация, така че оставям гласуването да остане в сила. - person Cheers and hth. - Alf; 18.12.2014

Тъй като вече е гарантирано, че низовете се съхраняват непрекъснато и вече ви е позволено да вземете указател към вътрешното хранилище на низ (т.е. &str[0] работи като за масив), не е възможно да се направи полезна COW изпълнение. Ще трябва да направите копие за твърде много неща. Дори само използването на operator[] или begin() върху неконстантен низ ще изисква копие.

person Dirk Holsopple    schedule 30.08.2012
comment
Мисля, че низовете в C++11 са гарантирани, че се съхраняват непрекъснато. - person mfontanini; 30.08.2012
comment
В миналото трябваше да правите копия във всички тези ситуации и това не беше проблем... - person David Rodríguez - dribeas; 30.08.2012
comment
@mfontanini да, но не са били преди - person Dirk Holsopple; 30.08.2012
comment
Въпреки че C++11 гарантира, че низовете са съседни, това е ортогонално на забраната на COW низовете. Низът COW на GCC е непрекъснат, така че очевидно твърдението ви, че не е възможно да се направи полезна реализация на COW, е фалшиво. - person Jonathan Wakely; 22.03.2015
comment
@JonathanWakely: Ако някой свърже два низа и попита получения низ за резервното си хранилище, той трябва да отчете непрекъсната последователност от знаци. Има ли някакви изисквания относно съхранението на низове, чието резервно хранилище никога не е било изложено? - person supercat; 12.11.2015
comment
@supercat, искането за резервно хранилище (напр. чрез извикване на c_str()) трябва да бъде O(1) и не може да хвърля и не трябва да въвежда състезания за данни, така че е много трудно да се изпълнят тези изисквания, ако мързеливо свързвате. На практика единственият разумен вариант е винаги да се съхраняват непрекъснати данни. - person Jonathan Wakely; 13.11.2015

Забранен ли е COW basic_string в C++11 и по-нови?

Относно

Прав ли съм, че C++11 не допуска внедрявания на std::string, базирани на COW?

Да.

Относно

Ако е така, това ограничение посочено ли е изрично някъде в новия стандарт (къде)?

Почти директно, чрез изисквания за постоянна сложност за редица операции, които биха изисквали O(n) физическо копиране на низовите данни в COW изпълнение.

Например за членските функции

auto operator[](size_type pos) const -> const_reference;
auto operator[](size_type pos) -> reference;

което при внедряване на COW би ¹и двете задействали копиране на низови данни, за да отмени споделянето на низовата стойност, стандартът C++11 изисква

C++11 §21.4.5/4:

Сложност: постоянно време.

което изключва подобно копиране на данни и следователно COW.

C++03 поддържа внедрявания на COW, като не има тези изисквания за постоянна сложност и, при определени ограничителни условия, позволява извиквания към operator[](), at(), begin(), rbegin(), end() или rend() за невалидни препратки, указатели и итератори, отнасящи се до низовите елементи, т.е. евентуално да предизвикат COW копиране на данни. Тази поддръжка беше премахната в C++11.


COW също ли е забранен чрез правилата за невалидност на C++11?

В друг отговор, който към момента на писане е избран като решение и който е силно одобрен и следователно очевидно се вярва, се твърди, че

За низ COW извикването на не-const operator[] ще изисква правене на копие (и обезсилване на препратки), което е забранено от [цитирания] параграф по-горе [C++11 §21.4.1/6] . Следователно вече не е законно да имате COW низ в C++11.

Това твърдение е неправилно и подвеждащо по два основни начина:

  • Това неправилно показва, че само инструментите за достъп на елемент, които не са const, трябва да задействат копиране на данни COW.
    Но също и инструментите за достъп на елемент const трябва да задействат копиране на данни, защото позволяват на клиентския код да формира препратки или указатели, които (в C++11) не е разрешено да се анулират по-късно чрез операциите, които могат да задействат копиране на COW данни.
  • Неправилно се приема, че копирането на COW данни може да причини невалидност на препратката.
    Но при правилно изпълнение копирането на COW данни, прекратяването на споделянето на стойността на низа, се извършва в момент, преди да има препратки, които могат бъде обезсилен.

За да видите как би работило правилно C++11 COW изпълнение на basic_string, когато изискванията на O(1), които правят това невалидно, се игнорират, помислете за изпълнение, при което низ може да превключва между правилата за собственост. Екземплярът на низ започва с правило Споделяемо. Когато тази политика е активна, не може да има препратки към външни елементи. Екземплярът може да премине към уникална политика и трябва да го направи, когато е потенциално създадена препратка към елемент, като например с извикване на .c_str() (поне ако това създава указател към вътрешния буфер). В общия случай на множество екземпляри, споделящи собствеността върху стойността, това води до копиране на низовите данни. След този преход към Уникална политика екземплярът може да премине обратно към Споделяем само чрез операция, която прави невалидни всички препратки, като например присвояване.

Така че, докато заключението на този отговор, че низовете COW са изключени, е правилно, предложеното разсъждение е неправилно и силно подвеждащо.

Подозирам, че причината за това недоразумение е ненормативна бележка в приложение C на C++11:

C++11 §C.2.11 [diff.cpp03.strings], about §21.3:

Промяна: Изискванията на basic_string вече не позволяват низове с преброени препратки
Обосновка: Невалидността е леко различна при низове с преброени препратки. Тази промяна регулира поведението (sic) за този международен стандарт.
Ефект върху оригиналната характеристика: Валиден C ++ 2003 код може да се изпълнява по различен начин в този международен стандарт

Тук обосновката обяснява основната защо човек реши да премахне специалната поддръжка на C++03 COW. Тази обосновка, защо, не е как стандартът ефективно забранява прилагането на COW. Стандартът не позволява COW чрез изискванията на O(1).

Накратко, правилата за анулиране на C++11 не изключват внедряване на COW на std::basic_string. Но те изключват сравнително ефективна неограничена COW реализация в стил C++03 като тази в поне една от стандартните библиотечни реализации на g++. Специалната поддръжка на C++03 COW позволи практическа ефективност, по-специално използването на const елементи за достъп, с цената на фини, сложни правила за невалидност:

C++03 §21.3/5 which includes “first call” COW support:

Препратките, указателите и итераторите, препращащи към елементите на 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().
— Извикване на членски функции, които не са const, освен operator[](), at(), begin(), rbegin(), end() и rend().
— След което и да е от горните употреби, с изключение на формите на insert() и erase(), които връщат итератори, първото извикване на не-const членски функции operator[](), at(), begin(), rbegin(), end() или rend().

Тези правила са толкова сложни и фини, че се съмнявам, че много програмисти, ако има такива, биха могли да дадат точно обобщение. Аз не можах.


Ами ако изискванията O(1) не бъдат взети предвид?

Ако C++11 изискванията за постоянно време на напр. operator[] се пренебрегват, тогава COW за basic_string може да бъде технически осъществимо, но трудно за изпълнение.

Операциите, които могат да получат достъп до съдържанието на низ, без да налагат копиране на COW данни, включват:

  • Конкатенация чрез +.
  • Изход чрез <<.
  • Използване на basic_string като аргумент за стандартни библиотечни функции.

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

Освен това внедряването може да предложи различни нестандартни функции за достъп до съдържанието на низове, без да задейства копиране на COW данни.

Основен усложняващ фактор е, че в C++11 basic_string достъпът до елемент трябва да задейства копиране на данни (прекратяване на споделянето на низовите данни), но се изисква да не се хвърля, напр. C++11 §21.4.5/3 Хвърля: Нищо.. И така не може да използва обикновено динамично разпределение, за да създаде нов буфер за копиране на COW данни. Един от начините за заобикаляне на това е да се използва специална купчина, където паметта може да бъде резервирана, без да бъде действително разпределена, и след това да се запази необходимата сума за всяка логическа препратка към стойност на низ. Резервирането и премахването на резервирането в такава купчина може да бъде постоянно време, O(1), а разпределянето на сумата, която вече е резервирана, може да бъде noexcept. За да се спазват изискванията на стандарта, с този подход изглежда, че ще трябва да има една такава специална купчина, базирана на резервации, за всеки отделен разпределител.


Бележки:
¹ Инструментът за достъп до елемент const задейства копиране на COW данни, тъй като позволява на клиентския код да получи препратка или указател към данните, които не е разрешено да обезсилва от по-късен копиране на данни, задействано от напр. не-const елементът за достъп.

person Cheers and hth. - Alf    schedule 29.07.2016
comment
Но при правилно изпълнение COW копирането на данни, премахването на споделянето на стойността на низа, се извършва в момент, преди да има препратки, които могат да бъдат анулирани. Примерът в моя отговор противоречи на това, показвайки как вземането на справка преди каквото и да е споделяне ви дава справка, която по-късно може да стане невалидна. Не можете да прекратите споделянето на низ, който все още не е споделен. Ако възнамерявате да гласувате против други отговори, защото не сте съгласни с подробностите, трябва да разберете вашите специфики правилно. - person Jonathan Wakely; 29.07.2016
comment
@JonathanWakely: Вашият пример е добър пример за неправилна реализация за C++11. Вероятно беше правилно за C++03. Но този SO въпрос е за C++11 и по-нови. Ако възнамерявате да гласувате против други отговори, защото не сте съгласни с подробностите, трябва да разберете вашите специфики правилно. - person Cheers and hth. - Alf; 29.07.2016
comment
Вашият пример е добър пример за неправилна реализация за C++11. Вероятно е било правилно за C++03. Да, това е смисълът на примера. Той показва COW низ, който е бил легален в C++03, защото не нарушава старите правила за анулиране на итератор и не е законен в C++11, защото нарушава новите правила за анулиране на итератор. И също така противоречи на твърдението, което цитирах в коментара по-горе. - person Jonathan Wakely; 29.07.2016
comment
@JonathanWakely: Не, вашият пример за неправилна реализация не противоречи на нищо. Това е същото като при неправилен опит за отваряне на врата, напр. душене на пантите. Фактът, че този неправилен подход не отваря врата, няма никакво значение за това дали вратата може да се отвори: просто е глупаво. - person Cheers and hth. - Alf; 29.07.2016
comment
@Jonathan: В случай, че не успеете да видите как би работило правилно изпълнение (пренебрегване по модул на изискванията на O(1), т.е. правилно освен това), когато s.c_str() се извика s става несподелено, ако е било споделено/споделено. - person Cheers and hth. - Alf; 29.07.2016
comment
Ами ако s още не е споделено, когато се обадите на s.c_str()? Как се справяте с по-късно обезсилване на указателя, върнат от c_str()? Низът трябва да запомни дали някаква препратка е била някога взета, дори докато е била уникална собственост, и след това никога да не позволява споделяне. Това е непрактично в реалния свят. - person Jonathan Wakely; 29.07.2016
comment
@Jonathan: Първоначално се споделя COW низ. Това означава, че действа така, сякаш има отделна стойност с препратен брой, като shared_ptr. - person Cheers and hth. - Alf; 29.07.2016
comment
Не, първоначално е уникална собственост. Това не се споделя. Той става споделен, когато броят на препратките надхвърли едно. Ако ще предефинирате какво означава COW, различно от това, за което е въпросът, тогава го направете някъде другаде. - person Jonathan Wakely; 29.07.2016
comment
Ако приемем неправилно внедряване, както правите с Не, то първоначално е уникално притежавано, не води до никъде: това е кръгова логика. Или още по-лошо. Душането ми не отваря вратата, не може да се отвори. Какво, да използвам дръжката? Не, правилното отваряне на вратата, както го определям, се извършва изключително с подушване на пантите. - person Cheers and hth. - Alf; 29.07.2016
comment
@JonathanWakely: Отново приемайки, че имате предвид това, което пишете, и просто не измамвате нещата изцяло, помислете за имплементация, при която низ може да превключва между политиките за собственост. Низът COW започва с политика за споделяне. Може да премине към уникална политика и трябва да го направи, когато е потенциално създадена препратка към елемент, като например с извикване на .c_str() (поне ако това създава указател към вътрешния буфер). Това обикновено включва копиране на низови данни. След това може да прехвърли Sharable само чрез операция, която обезсилва всички препратки, напр. задание. Ясно ли е? - person Cheers and hth. - Alf; 29.07.2016
comment
Ако бяхте казали споделено, а не първоначално споделено, нямаше да споря. Казването, че нещо първоначално е споделено, е просто объркващо. Споделено със себе си? Не това означава думата. Но повтарям: опитът ви да твърдите, че правилата за невалидност на итератора на C++11 не забраняват някакъв хипотетичен низ COW, който никога не е бил използван на практика (и би имал неприемлива производителност), когато те със сигурност забраняват вида на COW низ, който е използван на практика, е донякъде академичен и безсмислен. - person Jonathan Wakely; 29.07.2016
comment
@JonathanWakely: Не, въпросът какво позволява C++11 не е за старите C++03 COW низове на g++. Това е само погрешно насочване от ваша страна. Въпросът е какво C++11 позволява или не. И C++11 не позволява COW имплементации на basic_string. Правилно ли е да се гласува против отговор, който прави правилно този основен факт, когато има други грешни части? Да, гласувам против некоректността, когато човекът отказва да оправи нещата. - person Cheers and hth. - Alf; 29.07.2016
comment
Предложеният от вас низ COW е интересен, но не съм сигурен колко полезен би бил. Смисълът на низ COW е да копира само данните от низа в случай, че са записани двата низа. Предложената от вас реализация изисква копиране, когато се случи дефинирана от потребителя операция за четене. Дори ако компилаторът знае, че това е само четене, той пак трябва да копира. Освен това, копирането на уникален низ ще доведе до копиране на неговите низови данни (вероятно до състояние на споделяне), което отново прави COW доста безсмислено. Така че без гаранциите за сложност, можете да напишете... наистина скапан низ COW. - person Nicol Bolas; 29.07.2016
comment
Така че, докато сте технически прави, че гаранциите за сложност са това, което ви пречи да пишете каквато и да е форма на COW, наистина [basic.string]/5 ви пречи да пишете каквото и да е наистина полезно< /i> форма на низ COW. - person Nicol Bolas; 29.07.2016
comment
изисква копиране, когато се случи дефинирана от потребителя операция за четене, не, това е неправилно. - person Cheers and hth. - Alf; 29.07.2016
comment
наистина [basic.string]/5 ви пречи да пишете каквато и да е наистина полезна форма на низ COW, за щастие въпросът не беше за субективно мнение за полезност :-), а за това какво е абсолютно разрешено или не. - person Cheers and hth. - Alf; 29.07.2016
comment
OP каза Разбрах, че копирането при запис не е жизнеспособен начин за прилагане на съответстващ std::string в C++11. Неприемливото представяне не би било жизнеспособен начин за внедряване на съответстващ std::string. Потребителите ми не биха го приели и няма да го приемат, ако им кажа, че тяхното субективно мнение относно полезността му не е от значение. - person Jonathan Wakely; 03.08.2016
comment
@JonathanWakely: (1) Вашият цитат не е въпросът. Ето въпроса: „Прав ли съм, че C++11 не допуска COW базирани реализации на std::string? Ако е така, това ограничение посочено ли е изрично някъде в новия стандарт (къде)?“ (2) Вашето мнение, че COW std::string, когато пренебрегне изискванията на O(1), би било неефективно, е вашето мнение. Не знам какво би могло да бъде изпълнението, но мисля, че това твърдение е изложено повече заради усещането от него, заради вибрациите, които предава, отколкото заради някакво отношение към този отговор. - person Cheers and hth. - Alf; 03.08.2016

Винаги съм се чудел за неизменните крави: след като кравата е създадена, мога да бъда променен само чрез присвояване от друга крава, следователно тя ще бъде съвместима със стандарта.

Имах време да го изпробвам днес за прост сравнителен тест: карта с размер N, зададена от низ/крава, като всеки възел държи набор от всички низове в картата (имаме NxN брой обекти).

С низове с размер ~300 байта и N=2000 кравите са малко по-бързи и използват почти порядък по-малко памет. Вижте по-долу, размерите са в kbs, изпълнение b е с крави.

~/icow$ ./tst 2000
preparation a
run
done a: time-delta=6 mem-delta=1563276
preparation b
run
done a: time-delta=3 mem-delta=186384
person zzz777    schedule 16.10.2019