Jestem nowy, aby przenieść semantykę w C ++ 11 i nie wiem zbyt dobrze, jak obsługiwać unique_ptr
parametry w konstruktorach lub funkcjach. Rozważ tę klasę, która sama się odwołuje:
#include <memory>
class Base
{
public:
typedef unique_ptr<Base> UPtr;
Base(){}
Base(Base::UPtr n):next(std::move(n)){}
virtual ~Base(){}
void setNext(Base::UPtr n)
{
next = std::move(n);
}
protected :
Base::UPtr next;
};
Czy tak powinienem pisać funkcje przyjmujące unique_ptr
argumenty?
Czy muszę używać std::move
kodu wywołującego?
Base::UPtr b1;
Base::UPtr b2(new Base());
b1->setNext(b2); //should I write b1->setNext(std::move(b2)); instead?
c++
arguments
c++11
unique-ptr
codablank1
źródło
źródło
Odpowiedzi:
Oto możliwe sposoby przyjęcia unikalnego wskaźnika jako argumentu, a także związane z nimi znaczenie.
(A) Według wartości
Aby użytkownik mógł to wywołać, musi wykonać jedną z następujących czynności:
Przyjęcie unikalnego wskaźnika pod względem wartości oznacza, że przenosisz własność wskaźnika na daną funkcję / obiekt / etc. Po
newBase
zbudowaniunextBase
gwarantuje się, że będzie pusty . Nie jesteś właścicielem obiektu i nawet nie masz do niego wskaźnika. Odeszło.Jest to zapewnione, ponieważ przyjmujemy parametr według wartości.
std::move
tak naprawdę niczego nie rusza ; to tylko fantazyjna obsada.std::move(nextBase)
zwraca aBase&&
, do którego odnosi się wartość rnextBase
. To wszystko, co robi.Ponieważ
Base::Base(std::unique_ptr<Base> n)
jego argument jest oparty na wartości zamiast na wartości r-wartość, C ++ automatycznie skonstruuje dla nas tymczasowe. Tworzystd::unique_ptr<Base>
zBase&&
, który daliśmy funkcję przezstd::move(nextBase)
. Jest to konstrukcja tego tymczasowego, która faktycznie przenosi wartość znextBase
argumentu funkcjin
.(B) Przez odniesienie do stałej wartości l
To musi być wywołane na rzeczywistej wartości l (nazwanej zmiennej). Nie można go wywołać tymczasowo:
Znaczenie tego jest takie samo, jak znaczenie jakiegokolwiek innego użycia odwołań innych niż const: funkcja może, ale nie musi rościć prawa własności do wskaźnika. Biorąc pod uwagę ten kod:
Nie ma gwarancji, że
nextBase
jest pusta. To może być pusty; może nie. To naprawdę zależy od tego, coBase::Base(std::unique_ptr<Base> &n)
chce zrobić. Z tego powodu nie jest bardzo oczywiste tylko z podpisu funkcji, co się stanie; musisz przeczytać implementację (lub powiązaną dokumentację).Z tego powodu nie sugerowałbym tego jako interfejsu.
(C) Według stałej wartości odniesienia
Nie pokazuję implementacji, ponieważ nie możesz przejść z
const&
. Przekazując aconst&
, mówisz, że funkcja może uzyskać dostępBase
do wskaźnika za pomocą wskaźnika, ale nie może przechowywać go dowolnym miejscu. Nie może rościć sobie prawa własności.To może być przydatne. Niekoniecznie w twoim konkretnym przypadku, ale zawsze dobrze jest dać komuś wskaźnik i wiedzieć, że nie może (bez łamania zasad C ++, jak bez rzucania)
const
) posiadać własność. Nie mogą tego przechowywać. Mogą przekazać to innym, ale ci inni muszą przestrzegać tych samych zasad.(D) Przez wartość odniesienia r
Jest to mniej więcej identyczne z przypadkiem „według niezmiennej wartości odniesienia l”. Różnice to dwie rzeczy.
Państwo może przekazać tymczasowy:
Państwo musi użyć
std::move
podczas przechodzenia nietymczasowe argumenty.Ten ostatni jest naprawdę problemem. Jeśli zobaczysz ten wiersz:
Masz uzasadnione oczekiwania, że po wypełnieniu tego wiersza
nextBase
powinno być puste. Powinien zostać przeniesiony. W końcu to maszstd::move
siedzisz tam, mówiąc, że nastąpił ruch.Problem polega na tym, że tak nie było. Nie ma gwarancji, że został przeniesiony. Być może został przeniesiony, ale dowiesz się tylko, patrząc na kod źródłowy. Nie można odróżnić tylko od podpisu funkcji.
Rekomendacje
unique_ptr
, weź ją według wartości.unique_ptr
przez czas wykonywania tej funkcji, weź jąconst&
. Alternatywnie, podaj a&
lubconst&
do wskazanego typu, zamiast używaćunique_ptr
.&&
. Ale zdecydowanie odradzam robienie tego, gdy tylko jest to możliwe.Jak manipulować unikalnym narzędziem?
Nie możesz skopiować
unique_ptr
. Możesz go tylko przenieść. Właściwy sposób to zrobić za pomocąstd::move
standardową funkcją biblioteki.Jeśli weźmiesz
unique_ptr
wartość według, możesz z niej swobodnie się poruszać. Ale ruch tak naprawdę się nie zdarza z powodustd::move
. Weź następujące oświadczenie:To naprawdę dwie wypowiedzi:
(uwaga: powyższy kod nie jest technicznie kompilowany, ponieważ nietrwałe odwołania do wartości r nie są tak naprawdę wartościami r. Znajdują się tutaj wyłącznie w celach demonstracyjnych).
Jest
temporary
to tylko odniesienie do wartości roldPtr
. To w konstruktorze miejsca, wnewPtr
którym odbywa się ruch.unique_ptr
Konstruktor ruchu (konstruktor, który bierze się&&
do siebie) jest tym, co wykonuje ruch.Jeśli masz
unique_ptr
wartość i chcesz ją gdzieś przechowywać, musisz użyćstd::move
tej pamięci.źródło
std::move
nie podaje nazwy zwracanej wartości. Pamiętaj, że nazwane odniesienia wartości są wartościami lv. ideone.com/VlEM3unique_ptr
; być może niektórzy inni wywołujący potrzebują tej samej funkcji, aleshared_ptr
zamiast tego trzymają wywołanie] (2) przy pomocy odwołania do wartości może być przydatne, jeśli wywoływana funkcja modyfikuje wskaźnik, np. dodawanie lub usuwanie (będących własnością listy) węzłów z połączonej listy.unique_ptr
wartości referencji wartości (na przykład podczas ich przekształcaniashared_ptr
). Uzasadnieniem tego może być to, że jest on nieco bardziej wydajny (nie następuje przejście do tymczasowych wskaźników), podczas gdy daje dokładnie te same prawa dzwoniącemu (może przekazywać wartości rvalu lub wartości lvalu zawiniętestd::move
, ale nie nagie wartości lvalues).Pozwól, że spróbuję podać różne możliwe tryby przekazywania wskaźników do obiektów, których pamięcią zarządza instancja
std::unique_ptr
szablonu klasy; dotyczy to również starszegostd::auto_ptr
szablonu klasy (który, jak wierzę, zezwala na wszystkie zastosowania tego unikalnego wskaźnika, ale dla których dodatkowo modyfikowalne wartości będą akceptowane tam, gdzie oczekiwane są wartości bez konieczności wywoływaniastd::move
), i do pewnego stopnia równieżstd::shared_ptr
.Jako konkretny przykład do dyskusji rozważę następujący prosty typ listy
Wystąpienia takiej listy (której nie można udostępniać części innym wystąpieniom lub być okrągłe) są w całości własnością tego, kto posiada
list
wskaźnik początkowy . Jeśli kod klienta wie, że lista, którą przechowuje, nigdy nie będzie pusta, może również zapisać pierwsząnode
bezpośrednio zamiastlist
. Brak destruktora dlanode
trzeba definiować : ponieważ destruktory dla jego pól są wywoływane automatycznie, cała lista zostanie rekursywnie usunięta przez inteligentny wskaźnik destruktor wskaźnika, gdy upłynie okres użytkowania początkowego wskaźnika lub węzła.Ten typ rekurencyjny daje okazję do omówienia niektórych przypadków, które są mniej widoczne w przypadku inteligentnego wskaźnika do zwykłych danych. Również same funkcje czasami dostarczają (rekurencyjnie) przykład kodu klienta. Typedef dla
list
jest oczywiście stronniczyunique_ptr
, ale definicję można zmienić na useauto_ptr
lubshared_ptr
zamiast tego bez potrzeby zmiany tego, co powiedziano poniżej (w szczególności w odniesieniu do zapewnienia bezpieczeństwa wyjątkowego bez konieczności pisania niszczycieli).Tryby przekazywania inteligentnych wskaźników
Tryb 0: przekaż wskaźnik lub argument referencyjny zamiast inteligentnego wskaźnika
Jeśli twoja funkcja nie dotyczy własności, jest to preferowana metoda: nie zmuszaj jej wcale do inteligentnego wskaźnika. W takim przypadku twoja funkcja nie musi się martwić, kto jest właścicielem wskazanego obiektu lub w jaki sposób zarządzanie własnością jest zarządzane, więc przekazanie surowego wskaźnika jest zarówno całkowicie bezpieczne, jak i najbardziej elastyczną formą, ponieważ niezależnie od własności klient zawsze może tworzy surowy wskaźnik (przez wywołanie
get
metody lub z adresu operatora&
).Na przykład funkcja obliczająca długość takiej listy nie powinna być
list
argumentem, lecz surowym wskaźnikiem:Klient, który posiada zmienną,
list head
może wywołać tę funkcję jakolength(head.get())
, podczas gdy klient, który zamiast tego wybrał przechowywanienode n
reprezentującej niepustej listy, może zadzwonićlength(&n)
.Jeśli gwarantujemy, że wskaźnik ma wartość inną niż null (co nie ma tu miejsca, ponieważ listy mogą być puste), można raczej przekazać referencję niż wskaźnik. Może to być wskaźnik / odniesienie do,
const
jeśli funkcja nie musi aktualizować zawartości węzła (-ów), bez dodawania lub usuwania któregokolwiek z nich (ten ostatni wymagałby własności).Ciekawym przypadkiem należącym do kategorii trybu 0 jest (głęboka) kopia listy; chociaż funkcja, która to robi, musi oczywiście przenieść własność tworzonej przez siebie kopii, nie dotyczy to własności listy, którą kopiuje. Można go zatem zdefiniować w następujący sposób:
Ten kod zasługuje na dokładne przyjrzenie się, zarówno dla pytania, dlaczego w ogóle się kompiluje (wynik wywołania rekurencyjnego
copy
na liście inicjalizacyjnej wiąże się z argumentem referencyjnym wartości w konstruktorze przenoszeniaunique_ptr<node>
, czylilist
podczas inicjowanianext
pola pola wygenerowanenode
) i na pytanie, dlaczego jest bezpieczny w wyjątkach (jeśli podczas procesu alokacji rekurencyjnej skończy się pamięć i jakieś wywołanienew
rzutówstd::bad_alloc
, wówczas wskaźnik do częściowo skonstruowanej listy jest trzymany anonimowo w tymczasowym typielist
utworzony dla listy inicjalizującej, a jej destruktor wyczyści tę listę częściową). Nawiasem mówiąc, należy oprzeć się pokusie zastąpienia (jak na początku) drugiegonullptr
przezp
, który przecież w tym momencie wiadomo, że jest pusty: nie można zbudować inteligentnego wskaźnika ze (surowego) wskaźnika na stały , nawet jeśli wiadomo, że jest pusty.Tryb 1: przekaż inteligentny wskaźnik wartości
Funkcja, która przyjmuje wartość inteligentnego wskaźnika jako argument, przejmuje w posiadanie obiekt wskazany od razu: inteligentny wskaźnik trzymany przez wywołującego (niezależnie od tego, czy jest to zmienna o nazwie, czy anonimowy tymczasowy) jest kopiowany do wartości argumentu przy wejściu do funkcji i wywołującego wskaźnik stał się zerowy (w przypadku tymczasowej kopia mogła zostać pominięta, ale w każdym przypadku dzwoniący utracił dostęp do wskazanego obiektu). Chciałbym wywołać ten tryb wywołania gotówką, : dzwoniący płaci z góry za usługę, o której mowa, i nie może mieć złudzeń co do własności po zakończeniu połączenia. Aby to wyjaśnić, reguły językowe wymagają od osoby dzwoniącej zawarcia argumentu
std::move
jeśli inteligentny wskaźnik jest przechowywany w zmiennej (technicznie, jeśli argument jest wartością); w tym przypadku (ale nie dla trybu 3 poniżej) ta funkcja robi to, co sugeruje jej nazwa, mianowicie przenosi wartość ze zmiennej na tymczasową, pozostawiając zmienną zerową.W przypadkach, gdy wywoływana funkcja bezwarunkowo przejmuje na własność (przechwytuje) wskazany obiekt, ten tryb jest używany z
std::unique_ptr
lubstd::auto_ptr
jest dobrym sposobem przekazywania wskaźnika wraz z jego własnością, co pozwala uniknąć ryzyka wycieku pamięci. Niemniej jednak uważam, że jest bardzo niewiele sytuacji, w których tryb 3 poniżej nie powinien być preferowany (choćby nieznacznie) w stosunku do trybu 1. Z tego powodu nie podam żadnych przykładów użycia tego trybu. (Ale zobaczreversed
przykład trybu 3 poniżej, w którym zauważono, że tryb 1 zrobiłby co najmniej równie dobrze.) Jeśli funkcja przyjmuje więcej argumentów niż tylko ten wskaźnik, może się zdarzyć, że istnieje dodatkowy powód techniczny, aby unikać trybu 1 (zstd::unique_ptr
lubstd::auto_ptr
): ponieważ rzeczywista operacja przesuwania ma miejsce podczas przekazywania zmiennej wskaźnikowejp
przez wyrażeniestd::move(p)
nie można założyć, żep
ma on użyteczną wartość podczas oceny innych argumentów (kolejność oceny jest nieokreślona), co może prowadzić do subtelnych błędów; przeciwnie, użycie trybu 3 zapewnia, że żadne przeniesienie niep
nastąpi przed wywołaniem funkcji, dzięki czemu inne argumenty mogą bezpiecznie uzyskać dostęp do wartościp
.W przypadku użycia z
std::shared_ptr
tym trybem jest interesujący, ponieważ z definicją jednej funkcji pozwala wywołującemu wybrać, czy zachować dla siebie kopię udostępniania wskaźnika podczas tworzenia nowej kopii udostępniania do użycia przez funkcję (dzieje się tak, gdy wartość jest ważna dostarczony jest argument; konstruktor kopii dla wskaźników wspólnych używanych podczas wywołania zwiększa liczbę referencji) lub po prostu daje funkcji kopię wskaźnika bez zachowania jednego lub dotykania liczby referencji (dzieje się tak, gdy zapewniony jest argument wartości wartość zawinięta w callstd::move
). Na przykładTo samo można osiągnąć poprzez osobne zdefiniowanie
void f(const std::shared_ptr<X>& x)
(dla przypadkuvoid f(std::shared_ptr<X>&& x)
lvalue ) i (dla przypadku rvalue), przy czym ciała funkcji różnią się tylko tym, że pierwsza wersja wywołuje semantykę kopiowania (przy użyciu konstrukcji / przypisania kopii przy użyciux
), ale druga wersja przenosi semantykę (std::move(x)
zamiast tego pisze , jak w przykładowym kodzie). W przypadku wskaźników wspólnych tryb 1 może być przydatny, aby uniknąć powielania kodu.Tryb 2: przekaż inteligentny wskaźnik przez (modyfikowalne) odwołanie do wartości
W tym przypadku funkcja wymaga jedynie modyfikowalnego odniesienia do inteligentnego wskaźnika, ale nie daje żadnych wskazówek, co z nią zrobi. Chciałbym nazwać tę metodę call by card : osoba dzwoniąca zapewnia płatność, podając numer karty kredytowej. Odwołanie może być użyte do przejęcia własności wskazanego obiektu, ale nie musi. Ten tryb wymaga podania modyfikowalnego argumentu wartości, odpowiadającego faktowi, że pożądany efekt funkcji może obejmować pozostawienie użytecznej wartości w zmiennej argumentu. Osoba wywołująca z wyrażeniem rvalue, które chce przekazać do takiej funkcji, byłaby zmuszona do przechowywania jej w nazwie zmiennej, aby móc wykonać wywołanie, ponieważ język zapewnia jedynie domyślną konwersję do stałejlvalue referencja (odnosząca się do wartości tymczasowej) z wartości. (W przeciwieństwie do odwrotnej sytuacji obsługiwanej przez
std::move
rzutowanie zY&&
naY&
, przyY
użyciu inteligentnego typu wskaźnika, nie jest możliwe; mimo to tę konwersję można uzyskać za pomocą prostej funkcji szablonu, jeśli jest to naprawdę pożądane; patrz https://stackoverflow.com/a/24868376 / 1436796 ). W przypadku, gdy wywoływana funkcja zamierza bezwarunkowo przejąć na własność obiekt, kradnąc z argumentu, obowiązek podania argumentu wartości daje zły sygnał: zmienna nie będzie miała żadnej użytecznej wartości po wywołaniu. Dlatego do takiego użycia należy preferować tryb 3, który daje identyczne możliwości w ramach naszej funkcji, ale prosi osoby dzwoniące o podanie wartości.Jednak istnieje uzasadniony przypadek użycia dla trybu 2, a mianowicie funkcje, które mogą modyfikować wskaźnik lub obiekt wskazany w sposób obejmujący własność . Na przykład funkcja, która poprzedza węzeł węzłem,
list
stanowi przykład takiego użycia:Najwyraźniej byłoby tutaj niepożądane wymuszenie użycia dzwoniących
std::move
, ponieważ ich inteligentny wskaźnik nadal posiada dobrze zdefiniowaną i niepustą listę po połączeniu, choć inną niż wcześniej.Znów interesujące jest obserwowanie, co się stanie, jeśli
prepend
połączenie nie powiedzie się z powodu braku wolnej pamięci. Wtedynew
połączenie rzucistd::bad_alloc
; w tym momencie, ponieważ nienode
można było przypisać żadnego , pewne jest, że przekazane odwołanie do wartości (tryb 3) zstd::move(l)
nie mogło być jeszcze sfałszowane, ponieważ byłoby to zrobione w celu skonstruowanianext
pola tego,node
który nie został przydzielony. Tak więc oryginalny inteligentny wskaźnikl
nadal zawiera oryginalną listę, gdy zostanie zgłoszony błąd; ta lista albo zostanie odpowiednio zniszczona przez inteligentny wskaźnik niszczący wskaźnik, albo w przypadku,l
gdyby przetrwała dzięki odpowiednio wczesnejcatch
klauzuli, nadal będzie zawierać oryginalną listę.To był konstruktywny przykład; mrugając do tego pytania można również podać bardziej niszczycielski przykład usunięcia pierwszego węzła zawierającego daną wartość, jeśli taka istnieje:
Ponownie poprawność jest tutaj dość subtelna. W szczególności, w końcowej instrukcji wskaźnik
(*p)->next
trzymany w węźle, który ma zostać usunięty, jest odłączony (przezrelease
, co zwraca wskaźnik, ale powoduje, że pierwotny jest pusty), zanimreset
(domyślnie) zniszczy ten węzeł (gdy niszczy starą przechowywaną wartośćp
), zapewniając, że jeden i tylko jeden węzeł jest zniszczone w tym czasie. (W alternatywnej formie, o której mowa w komentarzu, ten czas byłby pozostawiony wewnętrznym aspektom implementacji operatora przeniesienia przypisaniastd::unique_ptr
instancjilist
; standard mówi 20.7.1.2.3; 2, że operator ten powinien postępować „tak, jakby wzywającreset(u.release())
”, dlatego też tutaj czas powinien być bezpieczny.)Należy pamiętać, że
prepend
iremove_first
nie może być wywołana przez klientów, którzy przechowują lokalnąnode
zmienną na zawsze zakaz pustą listę, i słusznie, ponieważ podane implementacje nie może pracować w takich przypadkach.Tryb 3: przekaż inteligentny wskaźnik przez (modyfikowalną) wartość referencyjną wartości
Jest to preferowany tryb do użycia, gdy po prostu przejmujesz na własność wskaźnik. Chciałbym wywołać tę metodę call by check : osoba dzwoniąca musi zaakceptować zrzeczenie się własności, jak gdyby zapewniała gotówkę, podpisując czek, ale faktyczna wypłata jest odroczona do momentu, aż wywołana funkcja faktycznie przesunie wskaźnik (dokładnie tak, jak w przypadku trybu 2 ). „Podpisanie czeku” konkretnie oznacza, że dzwoniący muszą owinąć argument
std::move
(jak w trybie 1), jeśli jest to wartość (jeśli jest to wartość, część „rezygnacja z własności” jest oczywista i nie wymaga osobnego kodu).Zauważ, że technicznie tryb 3 zachowuje się dokładnie tak samo jak tryb 2, więc wywoływana funkcja nie musi przejmować własności; Chciałbym jednak twierdzą, że jeśli istnieje jakakolwiek niepewność co do przeniesienia własności (w normalnych warunkach użytkowania), tryb 2 powinny być preferowane do trybu 3, tak, że przy użyciu trybu 3 jest niejawnie sygnał do rozmówców, że są dające się własności. Można powiedzieć, że przekazanie tylko argumentu trybu 1 naprawdę oznacza wymuszoną utratę własności przez osoby dzwoniące. Ale jeśli klient ma jakiekolwiek wątpliwości co do zamiarów wywoływanej funkcji, powinna znać specyfikację wywoływanej funkcji, co powinno usunąć wszelkie wątpliwości.
Zaskakująco trudno jest znaleźć typowy przykład dotyczący naszego
list
typu, który wykorzystuje przekazywanie argumentów w trybie 3. Przenoszenie listyb
na koniec innej listya
jest typowym przykładem; jednaka
(który przetrwa i zatrzyma wynik operacji) lepiej jest przejść za pomocą trybu 2:Czysty przykład przekazywania argumentów trybu 3 jest następujący, który pobiera listę (i jej własność) i zwraca listę zawierającą identyczne węzły w odwrotnej kolejności.
Tę funkcję można wywołać jako in,
l = reversed(std::move(l));
aby odwrócić listę do siebie, ale odwróconej listy można również używać w inny sposób.Tutaj argument jest natychmiast przenoszony do zmiennej lokalnej dla wydajności (można było użyć parametru
l
bezpośrednio zamiastp
, ale dostęp do niego za każdym razem wymagałby dodatkowego poziomu pośredniego); stąd różnica w przekazywaniu argumentów w trybie 1 jest minimalna. W rzeczywistości przy użyciu tego trybu argument mógł służyć bezpośrednio jako zmienna lokalna, unikając w ten sposób początkowego przesunięcia; jest to tylko przykład ogólnej zasady, że jeśli argument przekazany przez referencję służy tylko do zainicjowania zmiennej lokalnej, równie dobrze można przekazać ją zamiast wartości i użyć parametru jako zmiennej lokalnej.Używanie trybu 3 wydaje się być zalecane przez standard, o czym świadczy fakt, że wszystkie dostarczone funkcje biblioteczne, które przenoszą własność inteligentnych wskaźników za pomocą trybu 3. Szczególnym przekonującym przykładem jest konstruktor
std::shared_ptr<T>(auto_ptr<T>&& p)
. Że konstruktor stosowany (wstd::tr1
) do podjęcia modyfikowalna lwartości odniesienia (podobnie jak wauto_ptr<T>&
konstruktorze kopii), a zatem można nazwać zauto_ptr<T>
lwartościp
jak wstd::shared_ptr<T> q(p)
, po czymp
został zresetowany do wartości null. Ze względu na zmianę z trybu 2 na 3 w przekazywaniu argumentów, stary kod należy teraz przepisać nastd::shared_ptr<T> q(std::move(p))
i będzie on nadal działał. Rozumiem, że komitetowi nie spodobał się tutaj tryb 2, ale mieli możliwość przejścia do trybu 1, poprzez zdefiniowaniestd::shared_ptr<T>(auto_ptr<T> p)
zamiast tego mogliby zapewnić, że stary kod działa bez modyfikacji, ponieważ (w przeciwieństwie do wskaźników unikatowych) auto-wskaźniki można dyskretnie wyrejestrować na wartość (sam obiekt wskaźnika jest zresetowany do wartości zerowej w tym procesie). Najwyraźniej komitet tak bardzo wolał opowiadać się za trybem 3 niż za trybem 1, że postanowił aktywnie złamać istniejący kod zamiast używać trybu 1, nawet w przypadku przestarzałego użycia.Kiedy preferować tryb 3 niż tryb 1
Tryb 1 jest doskonale użyteczny w wielu przypadkach i może być preferowany nad trybem 3 w przypadkach, w których przyjęcie własności przybierałoby formę przeniesienia inteligentnego wskaźnika do zmiennej lokalnej, jak w
reversed
powyższym przykładzie. Widzę jednak dwa powody, dla których wolę tryb 3 w bardziej ogólnym przypadku:Nieco skuteczniej jest przekazać referencję niż utworzyć tymczasowy i nix stary wskaźnik (obsługa gotówki jest nieco pracochłonna); w niektórych scenariuszach wskaźnik może zostać kilkakrotnie przekazany w niezmienionej postaci do innej funkcji, zanim zostanie w rzeczywistości sfałszowany. Takie przekazywanie zwykle wymaga pisania
std::move
(chyba że używany jest tryb 2), ale należy pamiętać, że jest to po prostu obsada, która w rzeczywistości nic nie robi (w szczególności bez dereferencji), więc ma zerowy koszt.Czy można sobie wyobrazić, że cokolwiek zgłasza wyjątek między początkiem wywołania funkcji a punktem, w którym on (lub niektóre wywołanie zawarte) faktycznie przenosi wskazany obiekt do innej struktury danych (a ten wyjątek nie został jeszcze wychwycony w samej funkcji ), a następnie w trybie 1 obiekt, do którego odnosi się inteligentny wskaźnik, zostanie zniszczony, zanim
catch
klauzula będzie w stanie obsłużyć wyjątek (ponieważ parametr funkcji został zniszczony podczas rozwijania stosu), ale nie w przypadku użycia trybu 3. Ten ostatni daje właściwość w takich przypadkach osoba wywołująca ma możliwość odzyskania danych obiektu (poprzez wychwycenie wyjątku). Zauważ, że tutaj tryb 1 nie powoduje wycieku pamięci , ale może prowadzić do nieodwracalnej utraty danych dla programu, co również może być niepożądane.Zwracanie inteligentnego wskaźnika: zawsze według wartości
Kończąc słowo o zwróceniu inteligentnego wskaźnika, prawdopodobnie wskazuje na obiekt stworzony do użycia przez osobę dzwoniącą. Nie jest to tak naprawdę przypadek porównywalny z przekazywaniem wskaźników do funkcji, ale dla kompletności chciałbym nalegać, aby w takich przypadkach zawsze zwracać wartość (i nie używać
std::move
wreturn
instrukcji). Nikt nie chce uzyskać odniesienia do wskaźnika, który prawdopodobnie został właśnie usunięty.źródło
unique_ptr
został przeniesiony, czy nie, nadal ładnie usunie wartość, jeśli nadal ją zachowuje po każdym zniszczeniu lub ponownym użyciu.unique_ptr
pamięci zapobiega wyciekom pamięci (a więc w pewnym sensie spełnia swoją umowę), ale tutaj (tj. Przy użyciu trybu 1) może powodować (w określonych okolicznościach) coś, co można uznać za jeszcze bardziej szkodliwe , a mianowicie utratę danych (zniszczenie wskazanej wartości), której można było uniknąć za pomocą trybu 3.Tak, jeśli musisz wziąć
unique_ptr
wartość konstruktora. Explicity to miła rzecz. Ponieważunique_ptr
jest to niemożliwe do skopiowania (prywatne kopiowanie ctor), to co napisałeś powinno dać ci błąd kompilatora.źródło
Edycja: Ta odpowiedź jest nieprawidłowa, chociaż ściśle mówiąc, kod działa. Zostawiam to tutaj, ponieważ dyskusja pod nim jest zbyt przydatna. Ta inna odpowiedź jest najlepszą odpowiedzią udzieloną w czasie, gdy ostatnio edytowałem: Jak przekazać argument unikalny_ptr do konstruktora lub funkcji?
Podstawową ideą
::std::move
jest to, że ludzie, którzy cię mijają,unique_ptr
powinni używać go do wyrażenia wiedzy, że wiedzą,unique_ptr
że przechodzą, stracą własność.Oznacza to, że powinieneś używać odwołania do wartości
unique_ptr
w swoich metodach, a nie dounique_ptr
siebie. To i tak nie zadziała, ponieważ przekazanie zwykłego staregounique_ptr
wymagałoby wykonania kopii, a jest to wyraźnie zabronione w interfejsie dlaunique_ptr
. Co ciekawe, użycie nazwanego odwołania do wartości ponownie zamienia go z powrotem w wartość, więc musisz użyć::std::move
wewnątrz twoich metod, jak również.Oznacza to, że dwie metody powinny wyglądać tak:
Wtedy ludzie używający metod zrobiliby to:
Jak widać,
::std::move
wyraża, że wskaźnik straci własność w punkcie, w którym jest to najbardziej odpowiednie i pomocne. Gdyby stało się to niewidocznie, byłoby bardzo mylące dla osób korzystających z twojej klasy, gdybyobjptr
nagle straciły własność bez wyraźnego powodu.źródło
Base fred(::std::move(objptr));
i nieBase::UPtr fred(::std::move(objptr));
?std::move
w implementacji zarówno konstruktora, jak i metody. Nawet jeśli przekazujesz wartość, osoba dzwoniąca musi nadal używać wartościstd::move
lvalu. Główna różnica polega na tym, że interfejs przekazujący wartość powoduje, że własność zostanie utracona. Zobacz komentarz Nicola Bolasa na temat innej odpowiedzi.powinno być znacznie lepsze jako
i
Powinien być
z tym samym ciałem.
I ... co jest
evt
whandle()
środku?źródło
std::forward
tutaj nie ma żadnej korzyści :Base::UPtr&&
jest zawsze typem odniesienia do wartości istd::move
przekazuje je jako wartość. Jest już poprawnie przesłane.unique_ptr
wartość według, masz gwarancję, że konstruktor ruchu został wywołany na nowej wartości (lub po prostu otrzymałeś tymczasową). To , zapewnia , żeunique_ptr
zmienna użytkownik ma obecnie pusty . Jeśli przejmiesz go&&
zamiast tego, zostanie on opróżniony tylko wtedy, gdy kod wywoła operację przenoszenia. Po swojemu możliwe jest, że zmienna, z której użytkownik nie musiał zostać przeniesiony. Co sprawia, że korzystanie zstd::move
podejrzanych przez użytkownika jest mylące. Używaniestd::move
zawsze powinno gwarantować, że coś zostało przeniesione .Do góry głosowała odpowiedź. Wolę omijać wartość referencyjną.
Rozumiem, co może powodować problem z pominięciem odniesienia do wartości. Podzielmy ten problem na dwie strony:
Muszę napisać kod
Base newBase(std::move(<lvalue>))
lubBase newBase(<rvalue>)
.Autor biblioteki powinien zagwarantować, że faktycznie przeniesie unikatową opcję, aby zainicjować członka, jeśli chce mieć własność.
To wszystko.
Jeśli przejdziesz przez odwołanie do wartości, wywoła tylko jedną instrukcję „move”, ale jeśli przekażesz wartość, będą to dwie.
Tak, jeśli autor biblioteki nie jest ekspertem w tym zakresie, nie może przenieść unikatowego narzędzia do zainicjowania członka, ale jest to problem autora, a nie ciebie. Cokolwiek przekazuje przez wartość lub odwołanie do wartości, twój kod jest taki sam!
Jeśli piszesz bibliotekę, teraz wiesz, że powinieneś ją zagwarantować, więc po prostu zrób to, przekazywanie referencji do wartości jest lepszym wyborem niż wartością. Klient korzystający z Twojej biblioteki po prostu napisze ten sam kod.
Teraz na twoje pytanie. Jak przekazać argument unikalny_ptr do konstruktora lub funkcji?
Wiesz jaki jest najlepszy wybór.
http://scottmeyers.blogspot.com/2014/07/should-move-only-types-ever-be-passed.html
źródło