Zastanawiam się, jakie są możliwe zalety kopiowania przy zapisie? Oczywiście nie oczekuję osobistych opinii, ale praktyczne scenariusze z rzeczywistego świata, w których może to być technicznie i praktycznie korzystne w namacalny sposób. A przez namacalny mam na myśli coś więcej niż oszczędzanie pisania &
postaci.
Aby wyjaśnić, to pytanie jest w kontekście typów danych, w których konstrukcja przypisania lub kopii tworzy niejawną płytką kopię, ale modyfikacje w niej tworzą niejawną głęboką kopię i stosuje do niej zmiany zamiast oryginalnego obiektu.
Powodem, dla którego pytam, jest to, że nie znajduję żadnych zalet posiadania COW jako domyślnego domyślnego zachowania. Używam Qt, który ma COW zaimplementowany dla wielu typów danych, praktycznie wszystkich, które mają jakieś dynamiczne przydzielanie pamięci. Ale jak to naprawdę wpływa na użytkownika?
Przykład:
QString s("some text");
QString s1 = s; // now both s and s1 internally use the same resource
qDebug() << s1; // const operation, nothing changes
s1[o] = z; // s1 "detaches" from s, allocates new storage and modifies first character
// s is still "some text"
Co wygrywamy, używając COW w tym przykładzie?
Jeśli wszystko, co zamierzamy zrobić, to użyć operacji const, s1
jest redundantne, może równie dobrze użyć s
.
Jeśli zamierzamy zmienić wartość, wówczas COW opóźnia kopię zasobu tylko do pierwszej operacji non-const, przy (choć minimalnym) koszcie zwiększenia liczby odwołań dla niejawnego udostępniania i odłączania od pamięci wspólnej. Wygląda na to, że cały koszt związany z COW jest bezcelowy.
Nie różni się zbytnio w kontekście przekazywania parametrów - jeśli nie zamierzasz modyfikować wartości, podaj jako const referen, jeśli chcesz zmodyfikować, albo wykonaj niejawną głęboką kopię, jeśli nie chcesz modyfikować oryginalny obiekt lub przekaż referencję, jeśli chcesz go zmodyfikować. Ponownie COW wydaje się niepotrzebnym narzutem, który niczego nie osiąga, i dodaje jedynie ograniczenie, że nie możesz modyfikować oryginalnej wartości, nawet jeśli chcesz, ponieważ każda zmiana odłączy się od oryginalnego obiektu.
Tak więc w zależności od tego, czy wiesz o COW, czy jesteś tego nieświadomy, może to skutkować kodem z niejasnym zamiarem i niepotrzebnym narzutem, lub całkowicie mylącym zachowaniem, które nie spełnia oczekiwań i powoduje, że drapiesz się po głowie.
Wydaje mi się, że istnieją bardziej wydajne i czytelniejsze rozwiązania, niezależnie od tego, czy chcesz uniknąć niepotrzebnej głębokiej kopii, czy zamierzasz ją wykonać. Więc gdzie jest praktyczna korzyść z COW? Zakładam, że musi to przynieść pewne korzyści, ponieważ jest wykorzystywany w tak popularnym i potężnym frameworku.
Co więcej, z tego, co przeczytałem, COW jest teraz wyraźnie zabronione w standardowej bibliotece C ++. Nie wiem, czy wady, które w nim widzę, mają coś z tym wspólnego, ale tak czy inaczej, musi być ku temu powód.
[]
operatora. Więc COW umożliwia zły projekt - nie brzmi to jak pożytek :) Punkt w ostatnim akapicie wydaje się słuszny, ale ja sam nie jestem wielkim fanem niejawnego zachowania - ludzie biorą to za pewnik, trudno jest ustalić, dlaczego kod nie działa zgodnie z oczekiwaniami, i zastanawiać się, dopóki nie wymyślą, co kryje się pod ukrytym zachowaniem.const_cast
wydaje się, że może on złamać COW równie łatwo, jak może przerwać przekazywanie przez stałą referencyjną. Na przykładQString::constData()
zwraca aconst QChar *
-const_cast
to i COW zwija się - mutujesz dane oryginalnego obiektu.char*
oczywiście nie jest świadoma). Jeśli chodzi o ukryte zachowanie, myślę, że masz rację, są z tym problemy. Interfejs API zapewnia stałą równowagę między dwiema skrajnościami. Zbyt niejawne, a ludzie zaczynają polegać na specjalnym zachowaniu, tak jakby było to de facto częścią specyfikacji. Zbyt jawne, a interfejs API staje się zbyt nieporęczny, gdy ujawniasz zbyt wiele podstawowych szczegółów, które nie były tak naprawdę ważne i nagle zostały zapisane w specyfikacji interfejsu API.string
klasy uzyskały zachowanie COW, ponieważ projektanci kompilatora zauważyli, że duża część kodu kopiuje ciągi zamiast używać const-referencji. Gdyby dodali COW, mogliby zoptymalizować tę sprawę i uszczęśliwić więcej ludzi (i było to legalne, aż do C ++ 11). Doceniam ich pozycję: chociaż zawsze przekazuję ciągi przez stałe odniesienie, widziałem wszystkie te składniowe śmieci, które po prostu zmniejszają czytelność. Nienawidzę pisaćconst std::shared_ptr<const std::string>&
tylko po to, by uchwycić prawidłową semantykę!Wydaje się, że w przypadku ciągów znaków i pesymizacji bardziej powszechne przypadki użycia niż nie, ponieważ częstym przypadkiem ciągów są często małe ciągi, a tam narzut COW zwykle przewyższałby koszt zwykłego kopiowania małego ciągu. Mała optymalizacja bufora ma dla mnie znacznie większy sens, aby uniknąć przydziału sterty w takich przypadkach zamiast kopii ciągów.
Jeśli jednak masz cięższy obiekt, taki jak android, a chciałeś go skopiować i po prostu wymienić jego cybernetyczne ramię, COW wydaje się całkiem rozsądnym sposobem na zachowanie zmiennej składni przy jednoczesnym unikaniu konieczności głębokiego kopiowania całego Androida tylko w celu nadaj kopii wyjątkowe ramię. Uczynienie go po prostu niezmiennym jako trwała struktura danych w tym momencie może być lepsza, ale „częściowe COW” zastosowane na poszczególnych częściach Androida wydaje się uzasadnione w tych przypadkach.
W takim przypadku dwie kopie Androida dzieliłyby / wystąpiły ten sam tors, nogi, stopy, głowę, szyję, ramiona, miednicę itp. Jedyne dane, które byłyby między nimi różne i nie byłyby udostępnione, to ramię, które zostało wykonane unikalny dla drugiego Androida po nadpisaniu ramienia.
źródło
std::vector<std::string>
wcześniejszej wersjiemplace_back
semantyki w C ++ 11) . Ale w zasadzie używamy również instancji. System węzłów może modyfikować dane lub nie. Mamy takie elementy, jak węzły tranzytowe, które nic nie robią z danymi wejściowymi, ale po prostu wypisują kopię (są one dla organizacji użytkownika jego programu). W takich przypadkach wszystkie dane są płytko kopiowane dla złożonych typów ...A
zostanie skopiowany i nic nie zostanie zrobione, aby się sprzeciwićB
, jest to tania płytka kopia dla złożonych typów danych, takich jak siatki. Teraz, jeśli zmodyfikujemyB
, dane, które zmodyfikujemy,B
staną się unikalne przez COW, aleA
pozostaną nietknięte (z wyjątkiem niektórych liczb atomowych).