Widziałem gdzieś kod, w którym ktoś zdecydował się skopiować obiekt, a następnie przenieść go do członka danych klasy. Wprawiło mnie to w zakłopotanie, ponieważ myślałem, że celem przeniesienia jest uniknięcie kopiowania. Oto przykład:
struct S
{
S(std::string str) : data(std::move(str))
{}
};
Oto moje pytania:
- Dlaczego nie bierzemy odniesienia do r-wartości
str
? - Czy kopia nie będzie droga, zwłaszcza biorąc pod uwagę coś takiego
std::string
? - Jaki byłby powód, dla którego autor zdecydowałby się na wykonanie kopii, a następnie na ruch?
- Kiedy mam to zrobić samodzielnie?
c++
c++11
move-semantics
user2030677
źródło
źródło
Odpowiedzi:
Zanim odpowiem na twoje pytania, wydaje się, że się mylisz: branie według wartości w C ++ 11 nie zawsze oznacza kopiowanie. Jeśli przekazana zostanie wartość r, zostanie ona przeniesiona (pod warunkiem, że istnieje realny konstruktor przenoszenia), a nie zostanie skopiowana. I
std::string
ma konstruktora ruchu.W przeciwieństwie do C ++ 03, w C ++ 11 często idiomatyczne jest przyjmowanie parametrów według wartości, z powodów, które opiszę poniżej. Zobacz również te pytania i odpowiedzi dotyczące StackOverflow, aby uzyskać bardziej ogólny zestaw wskazówek dotyczących akceptowania parametrów.
Ponieważ uniemożliwiłoby to przekazywanie wartości lv, takich jak:
Gdyby
S
tylko miał konstruktor, który akceptuje rvalues, powyższe nie skompilowałoby się.Jeśli przekażesz wartość r, zostanie ona przeniesiona do
str
, a ostatecznie zostanie przeniesiona dodata
. Kopiowanie nie zostanie wykonane. Z drugiej strony, jeśli przekażesz lwartość, ta lwartość zostanie skopiowana dostr
, a następnie przeniesiona dodata
.Podsumowując, dwa ruchy dla rvalues, jedna kopia i jeden ruch dla lvalues.
Po pierwsze, jak wspomniałem powyżej, pierwsza nie zawsze jest kopią; a to powiedziawszy, odpowiedź brzmi: „ Ponieważ jest wydajne (przemieszczanie
std::string
przedmiotów jest tanie) i proste ”.Przy założeniu, że ruchy są tanie (pomijając tutaj SSO), można je praktycznie pominąć, biorąc pod uwagę ogólną wydajność tego projektu. Jeśli to zrobimy, mamy jedną kopię dla lwartości (tak jak byśmy mieli, gdybyśmy przyjęli odniesienie do
const
lwartości) i żadnych kopii dla rwartości (podczas gdy nadal mielibyśmy kopię, gdybyśmy zaakceptowali odniesienie do lwartościconst
).Oznacza to, że przyjmowanie według wartości jest tak samo dobre, jak przyjmowanie przez odniesienie do
const
lwartości, kiedy podawane są l-wartości, a lepsze, gdy podawane są wartości r.PS: Aby podać kontekst, myślę, że jest to pytanie i odpowiedź, do której odnosi się OP.
źródło
const T&
przekazywanie argumentów: w najgorszym przypadku (lwartość) jest to to samo, ale w przypadku tymczasowego wystarczy przenieść tymczasowy. Win-win.data
członku)? Miałbyś kopię, nawet gdybyś wziął ją przez odniesienie doconst
data
!Aby zrozumieć, dlaczego jest to dobry wzorzec, powinniśmy zbadać alternatywy, zarówno w C ++ 03, jak i C ++ 11.
Mamy metodę C ++ 03 polegającą na zrobieniu
std::string const&
:w takim przypadku zawsze zostanie wykonana jedna kopia. Jeśli konstruujesz z surowego ciągu C,
std::string
zostanie skonstruowany, a następnie skopiowany ponownie: dwie alokacje.Istnieje metoda C ++ 03 polegająca na pobraniu odwołania do a
std::string
, a następnie zamianie go na lokalnystd::string
:to jest wersja „semantyki przenoszenia” w języku C ++ 03 i
swap
często można ją zoptymalizować, aby była bardzo tania (podobnie jak amove
). Należy to również analizować w kontekście:i zmusza cię do utworzenia nietymczasowego
std::string
, a następnie odrzuć go. (Tymczasowystd::string
puszka nie wiążą się const odnośnik). Dokonuje się jednak tylko jednego przydziału. Wersja C ++ 11&&
wymagałaby wywołania go zstd::move
lub z wartością tymczasową: wymaga to jawnego utworzenia kopii poza wywołaniem i przeniesienia tej kopii do funkcji lub konstruktora.Posługiwać się:
Następnie możemy wykonać pełną wersję C ++ 11, która obsługuje zarówno kopiowanie, jak i
move
:Następnie możemy sprawdzić, jak to jest używane:
Jest całkiem jasne, że ta technika 2 przeciążenia jest co najmniej tak samo wydajna, jeśli nie bardziej, niż dwa powyższe style C ++ 03. Nazwę tę wersję z 2 przeciążeniami jako „najbardziej optymalną”.
Teraz przyjrzymy się wersji do pobrania:
w każdym z tych scenariuszy:
Jeśli porównasz tę wersję obok siebie z „najbardziej optymalną” wersją, zrobimy dokładnie jedną dodatkową
move
! Ani razu nie robimy nic więcejcopy
.Więc jeśli założymy, że
move
jest tania, ta wersja zapewnia nam prawie taką samą wydajność, jak wersja najbardziej optymalna, ale 2 razy mniej kodu.A jeśli bierzesz powiedzmy od 2 do 10 argumentów, redukcja kodu jest wykładnicza - 2x mniej z 1 argumentem, 4x z 2, 8x z 3, 16x z 4, 1024x z 10 argumentami.
Teraz możemy obejść ten problem poprzez doskonałe przekazywanie i SFINAE, pozwalające na napisanie pojedynczego konstruktora lub szablonu funkcji, który przyjmuje 10 argumentów, robi SFINAE, aby upewnić się, że argumenty są odpowiedniego typu, a następnie przenosi lub kopiuje je do stan lokalny zgodnie z wymaganiami. Chociaż zapobiega to tysiąckrotnemu wzrostowi rozmiaru programu, nadal może istnieć cały stos funkcji generowanych z tego szablonu. (instancje funkcji szablonu generują funkcje)
A wiele generowanych funkcji oznacza większy rozmiar kodu wykonywalnego, co samo w sobie może zmniejszyć wydajność.
Kosztem kilku
move
sekund otrzymujemy krótszy kod i prawie taką samą wydajność, a często łatwiejszy do zrozumienia kod.Teraz to działa tylko dlatego, że wiemy, kiedy wywoływana jest funkcja (w tym przypadku konstruktor), że będziemy potrzebować lokalnej kopii tego argumentu. Chodzi o to, że jeśli wiemy, że będziemy robić kopię, powinniśmy poinformować dzwoniącego, że robimy kopię, umieszczając ją na naszej liście argumentów. Następnie mogą zoptymalizować fakt, że dadzą nam kopię (na przykład przechodząc do naszej argumentacji).
Inną zaletą techniki „weź według wartości” jest to, że często konstruktory przenoszenia nie są wyjątkiem. Oznacza to, że funkcje, które pobierają wartość i wychodzą z argumentu, często nie są wyjątkiem, przenosząc dowolne
throw
s z ciała do zakresu wywołującego (kto może czasami tego uniknąć poprzez bezpośrednią konstrukcję lub skonstruować przedmioty imove
wprowadzić je do argumentu, aby kontrolować, gdzie ma miejsce rzucanie) .Robienie metod zamiast rzutów często jest tego warte.źródło
noexcept
. Biorąc dane po kopii, możesz utworzyć swoją funkcjęnoexcept
i sprawić , że każda konstrukcja kopii spowoduje potencjalne wyrzuty (na przykład brak pamięci) poza wywołaniem funkcji.Jest to prawdopodobnie zamierzone i podobne do idiomu kopiuj i zamień . Zasadniczo, ponieważ ciąg jest kopiowany przed konstruktorem, sam konstruktor jest bezpieczny pod względem wyjątków, ponieważ zamienia (przesuwa) tylko tymczasowy ciąg str.
źródło
Nie chcesz się powtarzać, pisząc konstruktora dla ruchu i jednego dla kopii:
To jest bardzo standardowy kod, zwłaszcza jeśli masz wiele argumentów. Twoje rozwiązanie pozwala uniknąć tego powielania kosztem niepotrzebnej przeprowadzki. (Jednak operacja przenoszenia powinna być dość tania).
Konkurencyjny idiom to użycie idealnego przekazywania:
Magia szablonu wybierze przeniesienie lub skopiowanie w zależności od przekazanego parametru. Zasadniczo rozwija się do pierwszej wersji, w której oba konstruktory zostały napisane ręcznie. Dodatkowe informacje można znaleźć w poście Scotta Meyera dotyczącym uniwersalnych odniesień .
Z punktu widzenia wydajności, idealna wersja przekazująca jest lepsza od twojej wersji, ponieważ pozwala uniknąć niepotrzebnych ruchów. Można jednak argumentować, że twoja wersja jest łatwiejsza do czytania i pisania. W każdym razie możliwy wpływ na wydajność nie powinien mieć znaczenia w większości sytuacji, więc ostatecznie wydaje się, że jest to kwestia stylu.
źródło