Jak przekazać argument unikalny_ptr do konstruktora lub funkcji?


400

Jestem nowy, aby przenieść semantykę w C ++ 11 i nie wiem zbyt dobrze, jak obsługiwać unique_ptrparametry 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_ptrargumenty?

Czy muszę używać std::movekodu wywołującego?

Base::UPtr b1;
Base::UPtr b2(new Base());

b1->setNext(b2); //should I write b1->setNext(std::move(b2)); instead?


1
Czy nie jest to błąd segmentacji, gdy wywołujesz b1-> setNext na pustym wskaźniku?
balki

Odpowiedzi:


836

Oto możliwe sposoby przyjęcia unikalnego wskaźnika jako argumentu, a także związane z nimi znaczenie.

(A) Według wartości

Base(std::unique_ptr<Base> n)
  : next(std::move(n)) {}

Aby użytkownik mógł to wywołać, musi wykonać jedną z następujących czynności:

Base newBase(std::move(nextBase));
Base fromTemp(std::unique_ptr<Base>(new Base(...));

Przyjęcie unikalnego wskaźnika pod względem wartości oznacza, że przenosisz własność wskaźnika na daną funkcję / obiekt / etc. Po newBasezbudowaniu nextBasegwarantuje 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::movetak naprawdę niczego nie rusza ; to tylko fantazyjna obsada. std::move(nextBase)zwraca a Base&&, do którego odnosi się wartość r nextBase. 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. Tworzy std::unique_ptr<Base>zBase&& , który daliśmy funkcję przez std::move(nextBase). Jest to konstrukcja tego tymczasowego, która faktycznie przenosi wartość z nextBaseargumentu funkcji n.

(B) Przez odniesienie do stałej wartości l

Base(std::unique_ptr<Base> &n)
  : next(std::move(n)) {}

To musi być wywołane na rzeczywistej wartości l (nazwanej zmiennej). Nie można go wywołać tymczasowo:

Base newBase(std::unique_ptr<Base>(new Base)); //Illegal in this case.

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:

Base newBase(nextBase);

Nie ma gwarancji, że nextBasejest pusta. To może być pusty; może nie. To naprawdę zależy od tego, co Base::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

Base(std::unique_ptr<Base> const &n);

Nie pokazuję implementacji, ponieważ nie możesz przejść z const&. Przekazując a const&, mówisz, że funkcja może uzyskać dostęp Basedo 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

Base(std::unique_ptr<Base> &&n)
  : next(std::move(n)) {}

Jest to mniej więcej identyczne z przypadkiem „według niezmiennej wartości odniesienia l”. Różnice to dwie rzeczy.

  1. Państwo może przekazać tymczasowy:

    Base newBase(std::unique_ptr<Base>(new Base)); //legal now..
  2. Państwo musi użyć std::movepodczas przechodzenia nietymczasowe argumenty.

Ten ostatni jest naprawdę problemem. Jeśli zobaczysz ten wiersz:

Base newBase(std::move(nextBase));

Masz uzasadnione oczekiwania, że ​​po wypełnieniu tego wiersza nextBasepowinno 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

  • (A) Według wartości: jeśli masz na myśli, że funkcja żąda prawa własności do unique_ptr, weź ją według wartości.
  • (C) Przez odwołanie do stałej wartości: Jeśli masz na myśli, że funkcja po prostu używa unique_ptrprzez czas wykonywania tej funkcji, weź ją const&. Alternatywnie, podaj a &lub const&do wskazanego typu, zamiast używać unique_ptr.
  • (D) Przez odniesienie do wartości r: Jeśli funkcja może, ale nie musi rościć sobie prawa własności (w zależności od wewnętrznych ścieżek kodu), weź ją &&. 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_ptrwartość według, możesz z niej swobodnie się poruszać. Ale ruch tak naprawdę się nie zdarza z powodu std::move. Weź następujące oświadczenie:

std::unique_ptr<Base> newPtr(std::move(oldPtr));

To naprawdę dwie wypowiedzi:

std::unique_ptr<Base> &&temporary = std::move(oldPtr);
std::unique_ptr<Base> newPtr(temporary);

(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 temporaryto tylko odniesienie do wartości r oldPtr. To w konstruktorze miejsca, w newPtrktórym odbywa się ruch. unique_ptrKonstruktor ruchu (konstruktor, który bierze się &&do siebie) jest tym, co wykonuje ruch.

Jeśli masz unique_ptrwartość i chcesz ją gdzieś przechowywać, musisz użyć std::movetej pamięci.


5
@Nicol: ale std::movenie podaje nazwy zwracanej wartości. Pamiętaj, że nazwane odniesienia wartości są wartościami lv. ideone.com/VlEM3
R. Martinho Fernandes

31
Zasadniczo zgadzam się z tą odpowiedzią, ale mam kilka uwag. (1) Nie sądzę, aby istniał prawidłowy przypadek użycia do przekazywania odniesienia do wartości stałej: wszystko, co odbiorca mógłby z tym zrobić, może to zrobić również w odniesieniu do stałego wskaźnika (goły), a nawet samego wskaźnika [i to nie jego sprawa wiedzieć, że własność jest utrzymywana przez unique_ptr; być może niektórzy inni wywołujący potrzebują tej samej funkcji, ale shared_ptrzamiast 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.
Marc van Leeuwen

8
... (3) Chociaż twój argument przemawiający za przekazywaniem wartości nad przekazywaniem referencji wartości ma sens, myślę, że sam standard zawsze przekazuje unique_ptrwartości referencji wartości (na przykład podczas ich przekształcania shared_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ęte std::move, ale nie nagie wartości lvalues).
Marc van Leeuwen

19
Wystarczy powtórzyć to, co powiedział Marc i cytując Suttera : „Nie używaj stałej const_ unikatowej jako parametru; zamiast tego użyj widgetu *”
Jon

17
Odkryliśmy problem z wartością według - przenoszenie odbywa się podczas inicjalizacji argumentu, co jest nieuporządkowane w stosunku do innych ocen argumentów (z wyjątkiem oczywiście listy inicjalizacyjnej). Natomiast zaakceptowanie odwołania do wartości zdecydowanie nakazuje ruch po wystąpieniu funkcji, a zatem po ocenie innych argumentów. Dlatego akceptacja odniesienia do wartości powinna być preferowana za każdym razem, gdy zostanie przejęta własność.
Ben Voigt

57

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_ptrszablonu klasy; dotyczy to również starszego std::auto_ptrszablonu 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ływania std::move), i do pewnego stopnia również std::shared_ptr.

Jako konkretny przykład do dyskusji rozważę następujący prosty typ listy

struct node;
typedef std::unique_ptr<node> list;
struct node { int entry; list next; }

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 listwskaźnik początkowy . Jeśli kod klienta wie, że lista, którą przechowuje, nigdy nie będzie pusta, może również zapisać pierwszą nodebezpośrednio zamiast list. 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 dlalist jest oczywiście stronniczy unique_ptr, ale definicję można zmienić na use auto_ptrlubshared_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 getmetody lub z adresu operatora &).

Na przykład funkcja obliczająca długość takiej listy nie powinna być listargumentem, lecz surowym wskaźnikiem:

size_t length(const node* p)
{ size_t l=0; for ( ; p!=nullptr; p=p->next.get()) ++l; return l; }

Klient, który posiada zmienną, list headmoże wywołać tę funkcję jako length(head.get()), podczas gdy klient, który zamiast tego wybrał przechowywanie node nreprezentują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, constjeś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:

list copy(const node* p)
{ return list( p==nullptr ? nullptr : new node{p->entry,copy(p->next.get())} ); }

Ten kod zasługuje na dokładne przyjrzenie się, zarówno dla pytania, dlaczego w ogóle się kompiluje (wynik wywołania rekurencyjnego copyna liście inicjalizacyjnej wiąże się z argumentem referencyjnym wartości w konstruktorze przenoszenia unique_ptr<node>, czyli listpodczas inicjowania nextpola pola wygenerowane node) i na pytanie, dlaczego jest bezpieczny w wyjątkach (jeśli podczas procesu alokacji rekurencyjnej skończy się pamięć i jakieś wywołanie newrzutówstd::bad_alloc , wówczas wskaźnik do częściowo skonstruowanej listy jest trzymany anonimowo w tymczasowym typie listutworzony 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) drugiego nullptrprzezp, 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 argumentustd::movejeś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_ptrlub std::auto_ptrjest 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 zobacz reversedprzykł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 (z std::unique_ptrlub std::auto_ptr): ponieważ rzeczywista operacja przesuwania ma miejsce podczas przekazywania zmiennej wskaźnikowejpprzez wyrażenie std::move(p)nie można założyć, że pma 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 nie pnastąpi przed wywołaniem funkcji, dzięki czemu inne argumenty mogą bezpiecznie uzyskać dostęp do wartości p.

W przypadku użycia z std::shared_ptrtym 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 call std::move). Na przykład

void f(std::shared_ptr<X> x) // call by shared cash
{ container.insert(std::move(x)); } // store shared pointer in container

void client()
{ std::shared_ptr<X> p = std::make_shared<X>(args);
  f(p); // lvalue argument; store pointer in container but keep a copy
  f(std::make_shared<X>(args)); // prvalue argument; fresh pointer is just stored away
  f(std::move(p)); // xvalue argument; p is transferred to container and left null
}

To samo można osiągnąć poprzez osobne zdefiniowanie void f(const std::shared_ptr<X>& x)(dla przypadku void 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życiu x), 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::moverzutowanie z Y&&na Y&, przy Yuż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, liststanowi przykład takiego użycia:

void prepend (int x, list& l) { l = list( new node{ x, std::move(l)} ); }

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 prependpołączenie nie powiedzie się z powodu braku wolnej pamięci. Wtedy newpołączenie rzuci std::bad_alloc; w tym momencie, ponieważ nie nodemożna było przypisać żadnego , pewne jest, że przekazane odwołanie do wartości (tryb 3) z std::move(l)nie mogło być jeszcze sfałszowane, ponieważ byłoby to zrobione w celu skonstruowania nextpola tego, nodektóry nie został przydzielony. Tak więc oryginalny inteligentny wskaźnik lnadal 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, lgdyby przetrwała dzięki odpowiednio wczesnej catchklauzuli, 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:

void remove_first(int x, list& l)
{ list* p = &l;
  while ((*p).get()!=nullptr and (*p)->entry!=x)
    p = &(*p)->next;
  if ((*p).get()!=nullptr)
    (*p).reset((*p)->next.release()); // or equivalent: *p = std::move((*p)->next); 
}

Ponownie poprawność jest tutaj dość subtelna. W szczególności, w końcowej instrukcji wskaźnik (*p)->nexttrzymany w węźle, który ma zostać usunięty, jest odłączony (przez release, co zwraca wskaźnik, ale powoduje, że pierwotny jest pusty), zanim reset (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 przypisania std::unique_ptrinstancji list; standard mówi 20.7.1.2.3; 2, że operator ten powinien postępować „tak, jakby wzywając reset(u.release())”, dlatego też tutaj czas powinien być bezpieczny.)

Należy pamiętać, że prependi remove_firstnie może być wywołana przez klientów, którzy przechowują lokalną nodezmienną 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 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 listtypu, który wykorzystuje przekazywanie argumentów w trybie 3. Przenoszenie listy bna koniec innej listy ajest typowym przykładem; jednak a(który przetrwa i zatrzyma wynik operacji) lepiej jest przejść za pomocą trybu 2:

void append (list& a, list&& b)
{ list* p=&a;
  while ((*p).get()!=nullptr) // find end of list a
    p=&(*p)->next;
  *p = std::move(b); // attach b; the variable b relinquishes ownership here
}

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.

list reversed (list&& l) noexcept // pilfering reversal of list
{ list p(l.release()); // move list into temporary for traversal
  list result(nullptr);
  while (p.get()!=nullptr)
  { // permute: result --> p->next --> p --> (cycle to result)
    result.swap(p->next);
    result.swap(p);
  }
  return result;
}

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 lbezpośrednio zamiast p, 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 (w std::tr1) do podjęcia modyfikowalna lwartości odniesienia (podobnie jak w auto_ptr<T>&konstruktorze kopii), a zatem można nazwać z auto_ptr<T>lwartości pjak w std::shared_ptr<T> q(p), po czym pzostał 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ć na std::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 reversedpowyż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 catchklauzula 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 w returninstrukcji). Nikt nie chce uzyskać odniesienia do wskaźnika, który prawdopodobnie został właśnie usunięty.


1
+1 dla Trybu 0 - przekazanie podstawowego wskaźnika zamiast unikalnego_ptr. Nieco temat (ponieważ pytanie dotyczy przekazania unikalnego_ptr), ale jest prosty i pozwala uniknąć problemów.
Machta

tutaj tryb 1 nie powoduje wycieku pamięci ” - oznacza to, że tryb 3 powoduje wyciek pamięci, co nie jest prawdą. Niezależnie od tego, czy unique_ptrzostał przeniesiony, czy nie, nadal ładnie usunie wartość, jeśli nadal ją zachowuje po każdym zniszczeniu lub ponownym użyciu.
rustyx

@RustyX: Nie rozumiem, jak interpretujesz tę implikację, i nigdy nie zamierzałem mówić tego, co według ciebie sugerowałem. Chodziło mi tylko o to, że tak jak gdzie indziej użycie unique_ptrpamię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.
Marc van Leeuwen

4

Tak, jeśli musisz wziąć unique_ptrwartość konstruktora. Explicity to miła rzecz. Ponieważ unique_ptrjest to niemożliwe do skopiowania (prywatne kopiowanie ctor), to co napisałeś powinno dać ci błąd kompilatora.


3

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::movejest to, że ludzie, którzy cię mijają, unique_ptrpowinni 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_ptrw swoich metodach, a nie do unique_ptrsiebie. To i tak nie zadziała, ponieważ przekazanie zwykłego starego unique_ptrwymagałoby wykonania kopii, a jest to wyraźnie zabronione w interfejsie dla unique_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:

Base(Base::UPtr &&n) : next(::std::move(n)) {} // Spaces for readability

void setNext(Base::UPtr &&n) { next = ::std::move(n); }

Wtedy ludzie używający metod zrobiliby to:

Base::UPtr objptr{ new Base; }
Base::UPtr objptr2{ new Base; }
Base fred(::std::move(objptr)); // objptr now loses ownership
fred.setNext(::std::move(objptr2)); // objptr2 now loses ownership

Jak widać, ::std::movewyraż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, gdyby objptrnagle straciły własność bez wyraźnego powodu.


2
Nazwane odniesienia wartości są wartościami lv.
R. Martinho Fernandes,

jesteś pewien, że tak jest Base fred(::std::move(objptr));i nie Base::UPtr fred(::std::move(objptr));?
codablank1,

1
Aby dodać do mojego poprzedniego komentarza: ten kod nie zostanie skompilowany. Nadal musisz użyć std::movew implementacji zarówno konstruktora, jak i metody. Nawet jeśli przekazujesz wartość, osoba dzwoniąca musi nadal używać wartości std::movelvalu. 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.
R. Martinho Fernandes

@ codablank1: Tak. Pokazuję, jak używać konstruktora i metod w bazie, które pobierają odwołania do wartości.
Wszechobecny

@ R.MartinhoFernandes: Och, interesujące. Myślę, że to ma sens. Spodziewałem się, że się mylisz, ale faktyczne testy wykazały, że masz rację. Naprawiono teraz.
Wszechobecny

0
Base(Base::UPtr n):next(std::move(n)) {}

powinno być znacznie lepsze jako

Base(Base::UPtr&& n):next(std::forward<Base::UPtr>(n)) {}

i

void setNext(Base::UPtr n)

Powinien być

void setNext(Base::UPtr&& n)

z tym samym ciałem.

I ... co jest evtw handle()środku?


3
Używanie std::forwardtutaj nie ma żadnej korzyści : Base::UPtr&&jest zawsze typem odniesienia do wartości i std::moveprzekazuje je jako wartość. Jest już poprawnie przesłane.
R. Martinho Fernandes

7
Definitywnie się z tym nie zgadzam. Jeśli funkcja przyjmuje unique_ptrwartość według, masz gwarancję, że konstruktor ruchu został wywołany na nowej wartości (lub po prostu otrzymałeś tymczasową). To , zapewnia , że unique_ptrzmienna 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 z std::movepodejrzanych przez użytkownika jest mylące. Używanie std::movezawsze powinno gwarantować, że coś zostało przeniesione .
Nicol Bolas,

@NicolBolas: Masz rację. Usunę moją odpowiedź, ponieważ choć działa, twoja obserwacja jest całkowicie poprawna.
Wszechobecny

0

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:

  • dla dzwoniącego:

Muszę napisać kod Base newBase(std::move(<lvalue>))lub Base newBase(<rvalue>).

  • dla odbiorcy:

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

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.