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
:
dumb_array& operator=(const dumb_array& other)
{
if (this != &other)
{
if (mSize != other.mSize)
{
delete [] mArray;
mArray = nullptr;
mArray = other.mSize ? new int[other.mSize] : nullptr;
mSize = other.mSize;
}
std::copy(other.mArray, other.mArray + mSize, mArray);
}
return *this;
}
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ć, to memcpy
(ukryty pod std::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:
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
swap(lhs, rhs);
return lhs;
}
A może, jeśli chcesz skorzystać z przypisania przeniesienia w C ++ 11, powinno to być:
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
lhs = std::move(rhs);
return lhs;
}
Jeśli dumb_array
klienci cenią sobie szybkość, powinni zadzwonić do operator=
. 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):
Class&
Class::operator=(Class&& rhs)
{
if (this == &rhs) // is this check needed?
{
// ...
}
return *this;
}
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:
- Tymczasowy.
- Obiekt, który dzwoniący chce, abyś uwierzył, jest tymczasowy.
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:
Class&
Class::operator=(Class&& other)
{
assert(this != &other);
// ...
return *this;
}
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
:
dumb_array& operator=(dumb_array&& other)
{
assert(this != &other);
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
W typowym przypadku użycia przeniesienia, *this
będzie to obiekt przeniesiony, a więc nie delete [] 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ę, że swap(x, x)
samoczynne przeniesienie przypisuje się tylko do wartości przeniesionej. W naszym dumb_array
przykładzie będzie to całkowicie nieszkodliwe, jeśli po prostu pominiemy potwierdzenie lub ograniczymy je do przypadku przeniesionego z:
dumb_array& operator=(dumb_array&& other)
{
assert(this != &other || mSize == 0);
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
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:
x = y;
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:
x = std::move(y);
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 pozwoleniem swap(x, x)
na zwykłą pracę:
template <class T>
void
swap(T& x, T& y)
{
// assume &x == &y
T tmp(std::move(x));
// x and y now have a valid but unspecified state
x = std::move(y);
// x and y still have a valid but unspecified state
y = std::move(tmp);
// x and y have the value of tmp, which is the value they had on entry
}
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ąć:
dumb_array& operator=(dumb_array&& other)
{
delete [] mArray;
// set *this to a valid state before continuing
mSize = 0;
mArray = nullptr;
// *this is now in a valid state, continue with move assignment
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
Powyższa realizacja toleruje zadanie samodzielne, ale *this
i other
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.
dumb_array& operator=(dumb_array&& other)
{
if (this != &other)
{
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
}
return *this;
}
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.
dumb_array& operator=(dumb_array&& other)
{
swap(other);
return *this;
}
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. Gdyby dumb_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:
Class& operator=(Class&&) = default;
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 kierunek strong_assign
.