Niedawno czytałem o konstruktorach przenoszenia w C ++ (patrz np. Tutaj ) i staram się zrozumieć, jak one działają i kiedy powinienem ich używać.
O ile rozumiem, konstruktor ruchu służy do zmniejszenia problemów z wydajnością spowodowanych kopiowaniem dużych obiektów. Strona Wikipedii mówi: „Chroniczny problem z wydajnością w C ++ 03 to kosztowne i niepotrzebne głębokie kopie, które mogą się zdarzyć niejawnie, gdy obiekty są przekazywane przez wartość”.
Zwykle zajmuję się takimi sytuacjami
- przekazując obiekty przez odniesienie, lub
- za pomocą inteligentnych wskaźników (np. boost :: shared_ptr), aby ominąć obiekt (inteligentne wskaźniki zostaną skopiowane zamiast obiektu).
W jakich sytuacjach powyższe dwie techniki są niewystarczające, a korzystanie z konstruktora ruchu jest wygodniejsze?
c++
programming-practices
Giorgio
źródło
źródło
shared_ptr
tylko ze względu na szybkie kopiowanie) i jeśli semantyka ruchu może osiągnąć to samo bez prawie żadnego karania za kodowanie, semantykę i czystość.Odpowiedzi:
Semantyka Move wprowadza cały wymiar do C ++ - nie chodzi tylko o to, by tanio zwracać wartości.
Na przykład bez semantyki move
std::unique_ptr
nie działa - spójrz nastd::auto_ptr
, która została przestarzała wraz z wprowadzeniem semantyki move i usunięta w C ++ 17. Przenoszenie zasobu znacznie różni się od kopiowania. Umożliwia przeniesienie własności unikatowego przedmiotu.Na przykład, nie patrzmy
std::unique_ptr
, ponieważ jest dość dobrze omówione. Spójrzmy, powiedzmy, na obiekt bufora wierzchołków w OpenGL. Bufor wierzchołków reprezentuje pamięć na GPU - należy go przydzielić i zwolnić przy użyciu specjalnych funkcji, prawdopodobnie mających ścisłe ograniczenia dotyczące czasu jego życia. Ważne jest również, aby korzystał z niego tylko jeden właściciel.Teraz można to zrobić za pomocą
std::shared_ptr
- ale tego zasobu nie można udostępniać. To sprawia, że używanie wskaźnika wspólnego jest mylące. Możesz użyćstd::unique_ptr
, ale wciąż wymaga to semantyki ruchu.Oczywiście nie wdrożyłem konstruktora ruchów, ale masz pomysł.
Istotne jest tutaj to, że niektórych zasobów nie można skopiować . Możesz przesuwać wskaźniki zamiast się poruszać, ale o ile nie użyjesz unikalnego_ptr, istnieje problem własności. Warto wyjaśnić, jaki jest cel kodu, więc konstruktor ruchów jest prawdopodobnie najlepszym rozwiązaniem.
źródło
Semantyka przesuwania niekoniecznie jest tak wielką poprawą, gdy
shared_ptr
zwracasz wartość - i kiedy / jeśli używasz (lub czegoś podobnego) prawdopodobnie przedwcześnie pesymujesz. W rzeczywistości prawie wszystkie racjonalnie nowoczesne kompilatory wykonują tak zwaną Optymalizację Wartości Zwrotnej (RVO) i Optymalizację Nazwanej Zwrotu Wartości (NRVO). Oznacza to, że kiedy wracasz wartość, zamiast faktycznie kopiowanie wartości w ogóle, po prostu przekazują ukryty wskaźnik / odniesienie do miejsca, w którym wartość zostanie przypisana po zwrocie, a funkcja używa tego do utworzenia wartości, w której ma się ona skończyć. Standard C ++ zawiera specjalne postanowienia, aby to umożliwić, więc nawet jeśli (na przykład) twój konstruktor kopiowania ma widoczne skutki uboczne, nie jest wymagane użycie konstruktora kopii do zwrócenia wartości. Na przykład:Podstawowa idea tutaj jest dość prosta: stwórz klasę z wystarczającą ilością treści, wolelibyśmy unikać kopiowania, jeśli to możliwe (
std::vector
wypełnimy 32767 losowymi liczbami całkowitymi). Mamy wyraźny ctor kopiowania, który pokaże nam kiedy / jeśli zostanie skopiowany. Mamy też trochę więcej kodu do zrobienia czegoś z losowymi wartościami w obiekcie, więc optymalizator nie (przynajmniej łatwo) wyeliminuje wszystko w klasie tylko dlatego, że nic nie robi.Następnie mamy kod, aby zwrócić jeden z tych obiektów z funkcji, a następnie użyć sumowania, aby upewnić się, że obiekt został naprawdę utworzony, a nie tylko całkowicie zignorowany. Kiedy go uruchamiamy, przynajmniej z najnowszymi / nowoczesnymi kompilatorami, okazuje się, że napisany przez nas konstruktor kopiowania nigdy nie działa - i tak, jestem prawie pewien, że nawet szybka kopia z poleceniem
shared_ptr
jest nadal wolniejsza niż brak kopiowania w ogóle.Przenoszenie pozwala ci robić wiele rzeczy, których po prostu nie możesz (bez nich) zrobić. Rozważ część „scalania” zewnętrznego typu scalania - masz, powiedzmy, 8 plików, które chcesz scalić razem. Idealnie chciałbyś umieścić wszystkie 8 tych plików w pliku
vector
- ale ponieważvector
(od C ++ 03) musi być w stanie kopiować elementy, aifstream
s nie może być kopiowane, utkniesz z niektórymiunique_ptr
/shared_ptr
, lub coś w tej kolejności, aby móc umieścić je w wektorze. Należy pamiętać, że nawet jeśli (przykładowo) myreserve
miejsca wvector
tak jesteśmy pewni nasiifstream
s będzie naprawdę nigdy nie mogą być kopiowane, kompilator nie będzie wiedział, że, więc kod nie będzie kompilować choć my wiemy, że konstruktor kopia nigdy nie będzie i tak używane.Mimo że nadal nie można go skopiować, w C ++ 11
ifstream
można go przenieść. W tym przypadku obiekty prawdopodobnie nigdy nie zostaną przeniesione, ale fakt, że mogą być w razie potrzeby, sprawia, że kompilator jest szczęśliwy, dzięki czemu możemy umieścić naszeifstream
obiektyvector
bezpośrednio, bez żadnych inteligentnych hacków wskaźnika.Wektor, który się rozwija, to całkiem niezły przykład czasu, w którym semantyka ruchu naprawdę może być / jest użyteczna. W takim przypadku RVO / NRVO nie pomoże, ponieważ nie mamy do czynienia z wartością zwracaną z funkcji (lub czegokolwiek bardzo podobnego). Mamy jeden wektor zawierający niektóre obiekty i chcemy przenieść te obiekty do nowej, większej części pamięci.
W C ++ 03 dokonano tego, tworząc kopie obiektów w nowej pamięci, a następnie niszcząc stare obiekty w starej pamięci. Wykonywanie tych wszystkich kopii tylko po to, by wyrzucić stare, było jednak stratą czasu. W C ++ 11 można oczekiwać, że zostaną przeniesione. Zazwyczaj pozwala to nam zasadniczo wykonać płytką kopię zamiast (zwykle o wiele wolniejszej) głębokiej kopii. Innymi słowy, za pomocą łańcucha lub wektora (tylko dla kilku przykładów) po prostu kopiujemy wskaźnik (i) w obiektach, zamiast tworzyć kopie wszystkich danych, do których odnoszą się te wskaźniki.
źródło
Rozważać:
Dodając ciągi do v, rozszerzy się ono w razie potrzeby i przy każdej realokacji ciągi będą musiały zostać skopiowane. W przypadku konstruktorów ruchów jest to w zasadzie problem.
Oczywiście możesz także zrobić coś takiego:
Ale to zadziała dobrze tylko dlatego, że
std::unique_ptr
implementuje konstruktor ruchu.Używanie
std::shared_ptr
ma sens tylko w (rzadkich) sytuacjach, gdy faktycznie masz współwłasność.źródło
string
mamy instancję, wFoo
której ma 30 członków danych?unique_ptr
Wersja nie będzie bardziej efektywny?Zwracane wartości są tam, gdzie najczęściej chciałbym przekazać wartość zamiast jakiegoś odniesienia. Byłoby miło móc szybko zwrócić obiekt „na stos” bez ogromnej kary za wydajność. Z drugiej strony nie jest szczególnie trudno obejść ten problem (wspólne wskaźniki są tak łatwe w użyciu ...), więc nie jestem pewien, czy naprawdę warto wykonywać dodatkową pracę na moich obiektach, aby móc to zrobić.
źródło