W operatorze przypisania klasy zwykle musisz sprawdzić, czy przypisywany obiekt jest obiektem wywołującym, aby nie zepsuć rzeczy:
Class& Class::operator=(const Class& rhs) {
if (this != &rhs) {
// do the assignment
}
return *this;
}
Czy potrzebujesz tego samego dla operatora przypisania ruchu? Czy jest kiedykolwiek sytuacja, w której this == &rhs
byłoby to prawdą?
? Class::operator=(Class&& rhs) {
?
}
c++
c++11
move-semantics
move-assignment-operator
Seth Carnegie
źródło
źródło
A a; a = std::move(a);
.std::move
jest normalne. Następnie weź pod uwagę aliasing, a kiedy jesteś głęboko w stosie wywołań i masz jedno odniesienieT
, a drugie odniesienie doT
... czy zamierzasz sprawdzić tożsamość tutaj? Czy chcesz znaleźć pierwsze wywołanie (lub wywołania), w których udokumentowanie, że nie możesz dwukrotnie przekazać tego samego argumentu, statycznie udowodni, że te dwa odwołania nie będą aliasami? A może sprawisz, że zadanie do siebie po prostu zadziała?std::sort
lubstd::shuffle
- za każdym razem, gdy zamieniaszi
th ij
th element tablicy bez uprzedniego sprawdzeniai != j
. (std::swap
jest zaimplementowany pod względem przydziału ruchu.)Odpowiedzi:
Wow, jest tu tyle do posprzątania ...
Po pierwsze, kopiowanie i zamiana nie zawsze jest właściwym sposobem implementacji przypisania kopii. Niemal na pewno w przypadku
dumb_array
tego rozwiązania jest to nieoptymalne rozwiązanie.Użycie funkcji Kopiuj i zamień jest
dumb_array
klasycznym przykładem umieszczenia najdroższej operacji z najpełniejszymi funkcjami w dolnej warstwie. Jest idealny dla klientów, którzy chcą mieć pełną funkcjonalność i są gotowi zapłacić karę za wydajność. Dostają dokładnie to, czego chcą.Ale jest to katastrofalne dla klientów, którzy nie potrzebują pełnej funkcjonalności i zamiast tego szukają najwyższej wydajności. Dla nich
dumb_array
to tylko kolejne oprogramowanie, które muszą przepisać, ponieważ jest zbyt wolne. Miałdumb_array
zostały zaprojektowane inaczej, to mogło usatysfakcjonowany zarówno klientów bez kompromisów po obu klienta.Kluczem do satysfakcji obu klientów jest zbudowanie najszybszych operacji na najniższym poziomie, a następnie dodanie do tego API dla pełniejszych funkcji kosztem. Oznacza to, że potrzebujesz silnej gwarancji wyjątków, w porządku, za to płacisz. Nie potrzebujesz tego? Oto szybsze rozwiązanie.
Spójrzmy konkretnie: oto szybki, podstawowy operator gwarancji wyjątku Copy Assignment dla
dumb_array
:Wyjaśnienie:
Jedną z droższych rzeczy, które można zrobić na nowoczesnym sprzęcie, jest podróż na stertę. Wszystko, co możesz zrobić, aby uniknąć wyprawy na stos, to dobrze wykorzystany czas i wysiłek. Klienci
dumb_array
mogą chcieć często przypisywać tablice tego samego rozmiaru. A kiedy to zrobią, wszystko, co musisz zrobić, tomemcpy
(ukryty podstd::copy
). Nie chcesz przydzielać nowej tablicy o tym samym rozmiarze, a następnie cofać przydział starej tablicy o tym samym rozmiarze!Teraz dla Twoich klientów, którzy naprawdę chcą silnego bezpieczeństwa wyjątków:
A może, jeśli chcesz skorzystać z przypisania przeniesienia w C ++ 11, powinno to być:
Jeśli
dumb_array
klienci cenią sobie szybkość, powinni zadzwonić dooperator=
. Jeśli potrzebują silnego zabezpieczenia wyjątków, istnieją ogólne algorytmy, które mogą wywołać, które będą działać na wielu różnych obiektach i wystarczy je zaimplementować tylko raz.Wróćmy teraz do pierwotnego pytania (które w tym momencie ma typ o):
To jest właściwie kontrowersyjne pytanie. Niektórzy powiedzą tak, absolutnie, inni powiedzą nie.
Osobiście uważam, że nie, nie potrzebujesz tego czeku.
Racjonalne uzasadnienie:
Kiedy obiekt wiąże się z odwołaniem do wartości r, jest to jedna z dwóch rzeczy:
Jeśli masz odniesienie do obiektu, który jest rzeczywisty tymczasowy, to z definicji masz unikalne odniesienie do tego obiektu. Nigdzie indziej w całym programie nie może się do niego odwoływać. To
this == &temporary
znaczy nie jest możliwe .Teraz, jeśli twój klient cię okłamał i obiecał ci, że dostajesz tymczasowy, gdy tak nie jest, to klient jest odpowiedzialny za upewnienie się, że nie musisz się tym przejmować. Jeśli chcesz być naprawdę ostrożny, uważam, że byłaby to lepsza implementacja:
Tj Jeśli są przekazywane odniesienie siebie, jest to błąd ze strony klienta, która powinna być stała.
Aby uzyskać kompletność, oto operator przypisania ruchu dla
dumb_array
:W typowym przypadku użycia przeniesienia,
*this
będzie to obiekt przeniesiony, a więc niedelete [] mArray;
powinno być operacją. Bardzo ważne jest, aby implementacje jak najszybciej usuwały dane o wartości nullptr.Ostrzeżenie:
Niektórzy będą twierdzić, że
swap(x, x)
to dobry pomysł lub po prostu zło konieczne. A to, jeśli zamiana przejdzie do domyślnej zamiany, może spowodować przypisanie samodzielnego ruchu.Nie zgadzam się, że
swap(x, x)
jest zawsze dobrym pomysłem. Jeśli zostanie znaleziony w moim własnym kodzie, uznam to za błąd wydajności i naprawię. Ale jeśli chcesz na to zezwolić, zdaj sobie sprawę, żeswap(x, x)
samoczynne przeniesienie przypisuje się tylko do wartości przeniesionej. W naszymdumb_array
przykładzie będzie to całkowicie nieszkodliwe, jeśli po prostu pominiemy potwierdzenie lub ograniczymy je do przypadku przeniesionego z:Jeśli samodzielnie przypiszesz dwa „przeniesione z” (puste)
dumb_array
, nie zrobisz nic złego poza wstawieniem niepotrzebnych instrukcji do swojego programu. Ta sama obserwacja może dotyczyć większości obiektów.<
Aktualizacja>
Poświęciłem więcej uwagi tej sprawie i nieco zmieniłem swoje stanowisko. Teraz uważam, że przypisanie powinno być tolerancyjne dla samodzielnego przypisywania, ale warunki postu dotyczące przypisania kopii i przeniesienia są różne:
W przypadku przypisania kopii:
należy mieć warunek końcowy,
y
aby nie zmieniać wartości. Kiedy&x == &y
wtedy ten warunek końcowy przekłada się na: przypisanie do samodzielnego kopiowania nie powinno mieć wpływu na wartośćx
.Przydział ruchu:
należy mieć warunek końcowy, który
y
ma ważny, ale nieokreślony stan. Kiedy&x == &y
wtedy ten warunek końcowy przekłada się na:x
ma ważny, ale nieokreślony stan. Tzn. Samodzielne przydzielanie ruchu nie musi być nieopuszczalne. Ale to nie powinno się zawiesić. Ten warunek końcowy jest zgodny z pozwoleniemswap(x, x)
na zwykłą pracę:Powyższe działa, o ile
x = std::move(x)
nie ulega awarii. Może pozostawićx
w dowolnym ważnym, ale nieokreślonym stanie.Widzę trzy sposoby zaprogramowania operatora przypisania przenoszenia, aby
dumb_array
to osiągnąć:Powyższa realizacja toleruje zadanie samodzielne, ale
*this
iother
w końcu jest zero wielkości tablicy po cesji własnym ruchem, bez względu na to, co oryginalna wartość*this
jest. Jest okej.Powyższa implementacja toleruje przypisanie własne w taki sam sposób, jak operator przypisania kopii, czyniąc z niej brak działania. To też jest w porządku.
Powyższe jest w porządku tylko wtedy, gdy
dumb_array
nie zawiera zasobów, które powinny zostać zniszczone "natychmiast". Na przykład, jeśli jedynym zasobem jest pamięć, powyższe jest w porządku. Gdybydumb_array
możliwe było utrzymywanie blokad mutex lub stanu otwartego plików, klient mógłby rozsądnie oczekiwać, że zasoby po lewej stronie przydziału przeniesienia zostaną natychmiast zwolnione, a zatem ta implementacja może być problematyczna.Koszt pierwszego to dwa dodatkowe sklepy. Koszt drugiego to test i oddział. Obie działają. Obydwa spełniają wszystkie wymagania z tabeli 22, wymagania MoveAssignable w standardzie C ++ 11. Trzeci działa również modulo problem z zasobami niezwiązanymi z pamięcią.
Wszystkie trzy wdrożenia mogą mieć różne koszty w zależności od sprzętu: Ile kosztuje oddział? Czy rejestrów jest dużo czy bardzo mało?
Wniosek jest taki, że przypisanie do samodzielnego przeniesienia, w przeciwieństwie do przypisania do samodzielnego kopiowania, nie musi zachowywać bieżącej wartości.
<
/Aktualizacja>
Jedna ostatnia (miejmy nadzieję) edycja zainspirowana komentarzem Luca Dantona:
Jeśli piszesz klasę wysokiego poziomu, która nie zarządza bezpośrednio pamięcią (ale może mieć bazy lub członków, którzy to robią), wówczas najlepszą implementacją przypisania ruchu jest często:
Spowoduje to przeniesienie przypisania każdej bazy i każdemu członkowi po kolei i nie będzie obejmować
this != &other
czeku. Zapewni to najwyższą wydajność i podstawowe bezpieczeństwo wyjątków, przy założeniu, że nie ma potrzeby utrzymywania niezmienników wśród baz i członków. Dla swoich klientów wymagających silnego bezpieczeństwa wyjątków, wskaż im kierunekstrong_assign
.źródło
std::swap(x,x)
, to dlaczego miałbym ufać, że poprawnie obsługuje bardziej skomplikowane operacje?std::swap(x,x)
to, działa po prostu nawet wtedy, gdyx = std::move(x)
daje nieokreślony wynik. Spróbuj! Nie musisz mi wierzyć.swap
działa tak długo, jak długox = move(x)
pozostawiax
w stanie, w którym można się przenieść. A algorytmystd::copy
/std::move
są zdefiniowane w taki sposób, aby dawały niezdefiniowane zachowanie na kopiach bez operacji (ach, 20-latekmemmove
ma rację, alestd::move
nie ma!). Więc chyba nie pomyślałem jeszcze o „wsadzie” do samodzielnego zadania. Ale oczywiście samokontrola jest czymś, co zdarza się często w prawdziwym kodzie, niezależnie od tego, czy Standard jest błogosławieństwem, czy nie.Po pierwsze, masz nieprawidłowy podpis operatora przypisania ruchu. Ponieważ ruchy kradną zasoby z obiektu źródłowego, źródło musi być odniesieniem innym niż
const
r-wartość.Należy zauważyć, że nadal powrócić za pośrednictwem (nie
const
) l -wartość odniesienia.W przypadku każdego typu bezpośredniego przypisania standard nie sprawdza, czy przypisanie własne, ale upewnienie się, że przypisanie własne nie powoduje awarii i spalenia. Na ogół nikt nie robi tego
x = x
ani niey = std::move(y)
wywołuje, ale aliasowanie, szczególnie za pomocą wielu funkcji, może prowadzića = b
lub prowadzićc = std::move(d)
do samodzielnego przypisywania. Wyraźne sprawdzenie samo-przypisania, tj.this == &rhs
Pomijanie treści funkcji, gdy prawda, jest jednym ze sposobów zapewnienia bezpieczeństwa samo-przypisania. Ale jest to jeden z najgorszych sposobów, ponieważ optymalizuje (miejmy nadzieję) rzadki przypadek, podczas gdy jest anty-optymalizacją dla bardziej powszechnego przypadku (ze względu na rozgałęzienia i możliwe błędy pamięci podręcznej).Teraz, gdy (przynajmniej) jeden z operandów jest obiektem bezpośrednio tymczasowym, nigdy nie możesz mieć scenariusza samo-przypisania. Niektórzy opowiadają się za przyjęciem takiego przypadku i zoptymalizowaniem kodu do tego stopnia, że kod staje się samobójczo głupi, gdy założenie jest błędne. Mówię, że porzucanie sprawdzania tego samego obiektu na użytkownikach jest nieodpowiedzialne. Nie tworzymy tego argumentu przy przypisywaniu kopii; po co odwrócić stanowisko w przypadku przypisania ruchu?
Zróbmy przykład, odmieniony od innego respondenta:
To przypisanie do kopiowania z wdziękiem obsługuje przypisanie własne bez jawnego sprawdzenia. Jeśli rozmiary źródłowe i docelowe różnią się, cofnięcie przydziału i ponowna alokacja poprzedzają kopiowanie. W przeciwnym razie zostanie wykonane tylko kopiowanie. Samodzielne przypisanie nie zapewnia zoptymalizowanej ścieżki, zostaje zrzucone na tę samą ścieżkę, co wtedy, gdy rozmiary źródła i miejsca docelowego są równe. Kopiowanie jest technicznie niepotrzebne, gdy dwa obiekty są równoważne (w tym, gdy są tym samym obiektem), ale jest to cena, gdy nie wykonuje się kontroli równości (pod względem wartości lub adresu), ponieważ samo sprawdzenie byłoby marnotrawstwem czasu. Zwróć uwagę, że samo przypisanie obiektu w tym miejscu spowoduje serię przypisań własnych na poziomie elementu; typ elementu musi być do tego bezpieczny.
Podobnie jak jego przykład źródłowy, to przypisanie kopii zapewnia podstawową gwarancję bezpieczeństwa wyjątków. Jeśli potrzebujesz silnej gwarancji, użyj ujednoliconego operatora przypisania z oryginalnego zapytania Kopiuj i Zamień , który obsługuje zarówno przypisanie kopiowania , jak i przenoszenia. Ale celem tego przykładu jest zmniejszenie bezpieczeństwa o jeden stopień, aby uzyskać prędkość. (Nawiasem mówiąc, zakładamy, że wartości poszczególnych elementów są niezależne; że nie ma niezmiennego ograniczenia ograniczającego niektóre wartości w porównaniu z innymi).
Spójrzmy na przypisanie ruchu dla tego samego typu:
Typ wymienny, który wymaga dostosowania, powinien mieć dwuparametrową funkcję wolną o nazwie
swap
w tej samej przestrzeni nazw, co typ. (Ograniczenie przestrzeni nazw umożliwia zamianę wywołań niekwalifikowanych). Typ kontenera powinien również dodawać publicznąswap
funkcję składową, aby pasowała do standardowych kontenerów. Jeśli element członkowskiswap
nie zostanie podany, funkcja wolnaswap
prawdopodobnie musi zostać oznaczona jako przyjaciel typu wymiennego. Jeśli dostosujesz ruchy do użyciaswap
, musisz podać własny kod wymiany; standardowy kod wywołuje kod przeniesienia typu, co spowodowałoby nieskończoną wzajemną rekursję dla typów dostosowanych do przenoszenia.Podobnie jak destruktory, funkcje swap i operacje przenoszenia nie powinny być nigdy rzucane, jeśli to w ogóle możliwe, i prawdopodobnie oznaczone jako takie (w C ++ 11). Standardowe typy bibliotek i procedury mają optymalizacje dla typów ruchomych, których nie można rzucać.
Ta pierwsza wersja przydziału przeniesienia spełnia podstawową umowę. Znaczniki zasobów źródła są przenoszone do obiektu docelowego. Stare zasoby nie zostaną ujawnione, ponieważ zarządza nimi teraz obiekt źródłowy. A obiekt źródłowy pozostaje w stanie użytecznym, dzięki czemu można na nim zastosować dalsze operacje, w tym przypisanie i zniszczenie.
Zauważ, że to przypisanie ruchu jest automatycznie bezpieczne do samodzielnego przypisania, ponieważ
swap
wywołanie jest. Jest również zdecydowanie bezpieczny dla wyjątków. Problem polega na niepotrzebnym zatrzymywaniu zasobów. Stare zasoby dla miejsca docelowego nie są już koncepcyjnie potrzebne, ale tutaj są nadal dostępne tylko po to, aby obiekt źródłowy mógł pozostać ważny. Jeśli zaplanowane zniszczenie obiektu źródłowego jest bardzo odległe, marnujemy miejsce w zasobach lub, co gorsza, jeśli łączna ilość zasobów jest ograniczona, a inne prośby o zasoby pojawią się, zanim (nowy) obiekt źródłowy oficjalnie umrze.Ta kwestia spowodowała kontrowersyjne porady obecnych guru dotyczące samodzielnego kierowania się podczas przydziału ruchu. Sposób pisania przypisania do przeniesienia bez zalegających zasobów jest podobny do:
Źródło jest resetowane do warunków domyślnych, podczas gdy stare zasoby docelowe są niszczone. W przypadku samooceny twój obecny obiekt kończy się samobójstwem. Głównym sposobem na obejście tego jest otoczenie kodu akcji
if(this != &other)
blokiem lub wkręcenie go i umożliwienie klientom zjedzeniaassert(this != &other)
początkowej linii (jeśli czujesz się dobrze).Alternatywą jest zbadanie, jak sprawić, aby przypisanie kopii było silnie bezpieczne dla wyjątków, bez przypisania ujednoliconego i zastosować je do przypisania przeniesienia:
Kiedy
other
ithis
są odrębne,other
są opróżniane przez ruchtemp
i pozostają w ten sposób. Następniethis
traci swoje stare zasobytemp
, uzyskując zasoby pierwotnie posiadaneother
. Wtedy stare zasobythis
giną, kiedy tak siętemp
dzieje.Kiedy dochodzi
other
dotemp
samozadania, opróżnianiethis
również się opróżnia . Następnie obiekt docelowy odzyskuje swoje zasobytemp
ithis
zamienia się. Śmierćtemp
żąda pustego przedmiotu, który powinien być praktycznie nieopuszczalny. Obiektthis
/other
zachowuje swoje zasoby.Przypisanie ruchu nie powinno być nigdy rzucane, o ile konstrukcja ruchu i zamiana również są. Koszt bycia bezpiecznym podczas samodzielnego przydzielania to kilka dodatkowych instrukcji dla typów niskiego poziomu, które powinny zostać zniwelowane przez wezwanie do cofnięcia przydziału.
źródło
delete
drugiego bloku kodu?std::copy
powoduje niezdefiniowane zachowanie, jeśli zakresy źródłowy i docelowy nakładają się (w tym przypadek, gdy pokrywają się). Zobacz C ++ 14 [alg.copy] / 3.Jestem w obozie tych, którzy chcą bezpiecznych operatorów do samodzielnego przypisywania, ale nie chcą pisać kontroli przypisania do siebie w implementacjach
operator=
. I faktycznie, w ogóle nie chcę wdrażaćoperator=
, chcę, aby domyślne zachowanie działało „od razu po wyjęciu z pudełka”. Najlepsi członkowie specjalni to ci, którzy przychodzą za darmo.Biorąc to pod uwagę, wymagania MoveAssignable obecne w standardzie są opisane w następujący sposób (z 17.6.3.1 Wymagania dotyczące argumentów szablonu [utility.arg.requirements], n3290):
gdzie symbole zastępcze są opisane jako: „
t
[jest] modyfikowalną lwartością typu T;” i „rv
jest wartością r typu T;”. Zauważ, że są to wymagania nałożone na typy używane jako argumenty w szablonach biblioteki Standard, ale patrząc gdzie indziej w standardzie zauważam, że każde wymaganie przy przypisaniu przeniesienia jest podobne do tego.Oznacza to, że
a = std::move(a)
musi być „bezpieczny”. Jeśli potrzebujesz testu tożsamości (np.this != &other
), Zrób to, bo inaczej nie będziesz w stanie nawet umieścić swoich obiektówstd::vector
! (Chyba, że nie korzystają z tych członków / operacje, które wymagają MoveAssignable, ale pomijając to.) Zauważ, że z poprzedniego przykładua = std::move(a)
, wtedythis == &other
rzeczywiście trzymać.źródło
a = std::move(a)
brak pracy może spowodować, że klasa nie będzie działaćstd::vector
? Przykład?std::vector<T>::erase
nie jest dozwolone, chyba żeT
jest to MoveAssignable. (Na marginesie IIRC niektóre wymagania MoveAssignable zostały złagodzone do MoveInsertable zamiast w C ++ 14.)T
musi być MoveAssignable, ale dlaczego miałobyerase()
polegać na przenoszeniu elementu do siebie ?Ponieważ twoja obecna
operator=
funkcja jest zapisywana, ponieważ utworzyłeś argument rvalue-referenceconst
, nie ma możliwości, abyś mógł "ukraść" wskaźniki i zmienić wartości przychodzącej referencji rvalue ... po prostu nie możesz tego zmienić, mógł tylko czytać. Widziałbym tylko problem, gdybyś zaczął wywoływaćdelete
wskaźniki itp. W swoimthis
obiekcie, tak jak w normalnej metodzie referencyjnej lvaueoperator=
, ale ten rodzaj pokonuje punkt wersji r-wartości ... tj. wydaje się zbędne używanie wersji rvalue do wykonywania tych samych operacji, które normalnie pozostawiono metodzieconst
-lvalueoperator=
.Teraz, jeśli zdefiniowałeś swój jako odniesienie
operator=
do wartości innej niżconst
r, jedynym sposobem, w jaki mogłem zobaczyć, że wymagane jest sprawdzenie, było przekazaniethis
obiektu do funkcji, która celowo zwróciła odwołanie do wartości r, a nie tymczasowe.Na przykład, przypuśćmy, że ktoś próbował napisać
operator+
funkcję i użyć mieszanki odwołań do r-wartości i l-wartości, aby „zapobiec” utworzeniu dodatkowych elementów tymczasowych podczas wykonywania operacji dodawania stosu na typie obiektu:Teraz, z tego, co rozumiem o referencjach rvalue, robienie powyższego jest odradzane (tj. Powinieneś po prostu zwrócić tymczasowe odniesienie, a nie rvalue), ale gdyby ktoś nadal to robił, to chciałbyś sprawdzić, czy czy przychodzące odwołanie do wartości r nie odnosiło się do tego samego obiektu co
this
wskaźnik.źródło
const
, możesz tylko z niego czytać, więc jedyną potrzebą Sprawdzenie byłoby, gdybyś zdecydował sięoperator=(const T&&)
wykonać tę samą ponowną inicjalizacjęthis
, jaką wykonałbyś w typowejoperator=(const T&)
metodzie, zamiast operacji w stylu zamiany (tj. kradzieży wskaźników itp. zamiast wykonywania głębokich kopii).Moja odpowiedź jest nadal taka, że przypisanie ruchu nie musi być chronione przed samodzielnym przypisaniem, ale ma inne wyjaśnienie. Rozważmy std :: unique_ptr. Gdybym miał zaimplementować jeden, zrobiłbym coś takiego:
Jeśli spojrzysz na wyjaśnienia Scotta Meyersa , robi coś podobnego. (Jeśli wędrujesz, dlaczego nie zrobić zamiany - ma jeden dodatkowy zapis). A to nie jest bezpieczne dla samodzielnego zadania.
Czasami jest to niefortunne. Rozważ wyprowadzenie z wektora wszystkich liczb parzystych:
Jest to w porządku dla liczb całkowitych, ale nie sądzę, aby coś takiego działało z semantyką przenoszenia.
Podsumowując: przeniesienie przypisania do samego obiektu nie jest w porządku i trzeba na to uważać.
Mała aktualizacja.
swap(x, x)
powinno działać. Algorytmy uwielbiają te rzeczy! Zawsze miło jest, gdy narożnik po prostu działa. (I jeszcze nie widziałem przypadku, w którym nie jest to darmowe. Nie oznacza to jednak, że nie istnieje).unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...}
jest bezpieczne dla samodzielnego przypisywania ruchu.źródło
Jest sytuacja, o której (this == rhs) przychodzi mi do głowy. W przypadku tej instrukcji: Myclass obj; std :: move (obj) = std :: move (obj)
źródło