Zwracanie unikatowej wartości z funkcji


367

unique_ptr<T>nie zezwala na tworzenie kopii, zamiast tego obsługuje semantykę przenoszenia. Jednak mogę zwrócić a unique_ptr<T>z funkcji i przypisać zwróconą wartość do zmiennej.

#include <iostream>
#include <memory>

using namespace std;

unique_ptr<int> foo()
{
  unique_ptr<int> p( new int(10) );

  return p;                   // 1
  //return move( p );         // 2
}

int main()
{
  unique_ptr<int> p = foo();

  cout << *p << endl;
  return 0;
}

Powyższy kod kompiluje się i działa zgodnie z przeznaczeniem. Dlaczego więc ta linia 1nie wywołuje konstruktora kopiowania i nie powoduje błędów kompilatora? Gdybym musiał 2zamiast tego użyć linii, miałoby to sens (używanie linii również 2działa, ale nie jesteśmy do tego zobowiązani).

Wiem, że C ++ 0x zezwala na ten wyjątek, unique_ptrponieważ zwracana wartość jest obiektem tymczasowym, który zostanie zniszczony, gdy tylko funkcja wyjdzie, co gwarantuje unikalność zwracanego wskaźnika. Jestem ciekawy, jak to jest zaimplementowane, czy jest to specjalnie uwzględnione w kompilatorze, czy też istnieje specyfikacja językowa, którą wykorzystuje?


Hipotetycznie, jeśli wdrażasz metodę fabryczną , czy wolisz 1 lub 2, aby zwrócić wydajność fabryki? Zakładam, że byłoby to najczęstsze użycie 1, ponieważ przy odpowiedniej fabryce faktycznie chcesz, aby własność skonstruowanej rzeczy przeszła na osobę dzwoniącą.
Xharlie,

7
@Xharlie? Obaj przechodzą na własność unique_ptr. Całe pytanie dotyczy 1 i 2, które są dwoma różnymi sposobami osiągnięcia tego samego.
Pretorianian

w tym przypadku RVO ma również miejsce w c ++ 0x, zniszczenie obiektu unique_ptr nastąpi raz, co zostanie wykonane po mainwyjściu z funkcji, ale nie przy foowyjściu.
ampawd

Odpowiedzi:


218

czy w specyfikacji języka jest jakaś inna klauzula?

Tak, patrz 12.8 §34 i §35:

Po spełnieniu określonych kryteriów implementacja może pominąć konstrukcję kopiowania / przenoszenia obiektu klasy [...] Ta operacja operacji kopiowania / przenoszenia, zwana operacją kopiowania , jest [...] dozwolona w instrukcji return w funkcja z typem zwracanym przez klasę, gdy wyrażenie jest nazwą nielotnego obiektu automatycznego o tym samym typie bez kwalifikacji cv co typ zwracany przez funkcję [...]

Gdy kryteria wyboru operacji kopiowania są spełnione, a obiekt, który ma zostać skopiowany, jest oznaczany przez wartość, rozdzielczość przeciążania w celu wybrania konstruktora dla kopii jest najpierw wykonywana tak, jakby obiekt został wyznaczony przez wartość .


Chciałem tylko dodać jeszcze jeden punkt, że zwracanie wartości powinno być domyślnym wyborem, ponieważ w najgorszym przypadku nazwana wartość w instrukcji return, tj. Bez elekcji w C ++ 11, C ++ 14 i C ++ 17 jest traktowana jako wartość. Na przykład następująca funkcja kompiluje się z -fno-elide-constructorsflagą

std::unique_ptr<int> get_unique() {
  auto ptr = std::unique_ptr<int>{new int{2}}; // <- 1
  return ptr; // <- 2, moved into the to be returned unique_ptr
}

...

auto int_uptr = get_unique(); // <- 3

Po ustawieniu flagi podczas kompilacji w tej funkcji występują dwa ruchy (1 i 2), a następnie jeden ruch później (3).


@juanchopanza Czy w gruncie rzeczy masz na myśli, że foo()tak naprawdę wkrótce też zostanie zniszczony (jeśli nie zostałby do niczego przypisany), podobnie jak wartość zwracana w ramach funkcji, a zatem ma sens, że C ++ używa konstruktora ruchu podczas działania unique_ptr<int> p = foo();?
krów

1
Ta odpowiedź mówi, że implementacja może coś zrobić ... nie mówi, że musi, więc jeśli byłaby to jedyna odpowiednia sekcja, to sugerowałaby, że poleganie na tym zachowaniu nie jest przenośne. Ale nie sądzę, że to prawda. Jestem skłonny sądzić, że poprawna odpowiedź ma więcej wspólnego z konstruktorem ruchów, jak opisano w odpowiedzi Nikoli Smiljanić i Bartosza Milewskiego.
Don Hatch

6
@DonHatch Mówi, że w tych przypadkach dozwolone jest wykonywanie operacji kopiowania / przenoszenia, ale nie mówimy tutaj o operacji kopiowania. Obowiązuje tutaj drugi cytowany akapit, który stosuje się do zasad elekcji kopii, ale sam nie jest kopią elekcji. W drugim akapicie nie ma wątpliwości - jest całkowicie przenośny.
Joseph Mansfield

@juanchopanza Zdaję sobie sprawę, że to już 2 lata później, ale czy nadal czujesz, że to źle? Jak wspomniałem w poprzednim komentarzu, nie chodzi tu o usunięcie kopii. Zdarza się tak, że w przypadkach, w których może obowiązywać eliminacja kopii (nawet jeśli nie można jej zastosować std::unique_ptr), istnieje specjalna zasada, aby najpierw traktować obiekty jako wartości. Myślę, że zgadza się to całkowicie z odpowiedzią Nikoli.
Joseph Mansfield

1
Dlaczego więc nadal pojawia się błąd „próba odwołania się do usuniętej funkcji” dla mojego typu tylko do przenoszenia (usunięty konstruktor kopii), gdy zwraca ją dokładnie w taki sam sposób jak w tym przykładzie?
DrumM

104

Nie jest to w żaden sposób specyficzne std::unique_ptr, ale dotyczy każdej klasy, która jest ruchoma. Gwarantują to reguły językowe, ponieważ wracasz wartościowo. Kompilator próbuje wymyślić kopie, wywołuje konstruktor przenoszenia, jeśli nie może usunąć kopii, wywołuje konstruktor kopii, jeśli nie może się poruszyć, i kompiluje się, jeśli nie może skopiować.

Gdybyś miał funkcję, która przyjmuje std::unique_ptrjako argument, nie byłbyś w stanie przekazać do niej p. Będziesz musiał jawnie wywołać konstruktor ruchu, ale w tym przypadku nie powinieneś używać zmiennej p po wywołaniu bar().

void bar(std::unique_ptr<int> p)
{
    // ...
}

int main()
{
    unique_ptr<int> p = foo();
    bar(p); // error, can't implicitly invoke move constructor on lvalue
    bar(std::move(p)); // OK but don't use p afterwards
    return 0;
}

3
@Fred - cóż, niezupełnie. Chociaż pnie jest to tymczasowe, wynikiem tego foo(), co jest zwracane, jest; dlatego jest to wartość i można ją przenosić, co umożliwia przypisanie main. Powiedziałbym, że się myliłeś, z tym wyjątkiem, że Nikola wydaje się stosować tę zasadę do psiebie, co JEST błędne.
Edward Strange

Dokładnie to, co chciałem powiedzieć, ale nie mogłem znaleźć słów. Usunąłem tę część odpowiedzi, ponieważ nie była bardzo jasna.
Nikola Smiljanić

Mam pytanie: czy w pierwotnym pytaniu jest jakaś istotna różnica między linią 1a linią 2? Moim zdaniem jest to samo od kiedy buduje psię main, to tylko dba o rodzaju typu powrotnej foo, prawda?
Hongxu Chen,

1
@HongxuChen W tym przykładzie absolutnie nie ma różnicy, patrz cytat ze standardu w zaakceptowanej odpowiedzi.
Nikola Smiljanić

Właściwie możesz użyć p później, o ile go do tego przypisasz. Do tego czasu nie możesz próbować odwoływać się do treści.
Alan

38

Unique_ptr nie ma tradycyjnego konstruktora kopiowania. Zamiast tego ma „konstruktor ruchu”, który wykorzystuje odwołania do wartości:

unique_ptr::unique_ptr(unique_ptr && src);

Odwołanie do wartości (podwójny znak ampersand) będzie wiązało się tylko z wartością. Dlatego pojawia się błąd, gdy próbujesz przekazać wartość unikalna_ptr do funkcji. Z drugiej strony wartość zwracana z funkcji jest traktowana jak wartość, więc konstruktor ruchu jest wywoływany automatycznie.

Nawiasem mówiąc, będzie to działać poprawnie:

bar(unique_ptr<int>(new int(44));

Tymczasowe unikalne_ptr tutaj jest wartością.


8
Myślę, że chodzi o to, dlaczego p- „oczywiście” wartość - może być traktowana jako wartość w instrukcji return return p;w definicji foo. Nie sądzę, aby istniał problem z tym, że wartość zwracaną przez samą funkcję można „przenieść”.
CB Bailey,

Czy zawijanie zwróconej wartości z funkcji w std :: move oznacza, że ​​zostanie ona dwukrotnie przeniesiona?

3
@RodrigoSalazar std :: move to tylko fantazyjna obsada z odniesienia do wartości (&) na odwołanie do wartości (&&). Zewnętrzne użycie std :: move na wartości referencyjnej będzie po prostu noop
TiMoch

13

Myślę, że jest to doskonale wyjaśnione w punkcie 25 Effective Modern C ++ Scotta Meyersa . Oto fragment:

Część Standardowego błogosławieństwa RVO mówi dalej, że jeśli warunki dla RVO są spełnione, ale kompilatory decydują się nie wykonywać wyboru kopiowania, zwracany obiekt należy traktować jak wartość. W efekcie Norma wymaga, aby gdy RVO było dozwolone, albo odbywa się usuwanie kopii, albo std::movejest domyślnie stosowane do zwracanych obiektów lokalnych.

W tym przypadku RVO odnosi się do optymalizacji wartości zwracanej , a jeśli warunki dla RVO są spełnione, oznacza to zwrócenie lokalnego obiektu zadeklarowanego w funkcji, której można oczekiwać w RVO , co również jest ładnie wyjaśnione w punkcie 25 jego książki, odnosząc się do standard (tutaj obiekt lokalny obejmuje obiekty tymczasowe utworzone przez instrukcję return). Największym fragmentem fragmentu jest eliminacja kopii lub std::movejest ona domyślnie stosowana do zwracanych obiektów lokalnych . Scott wspomina w punkcie 25, który std::movejest domyślnie stosowany, gdy kompilator zdecyduje się nie pomijać kopii, a programista nie powinien tego wyraźnie robić.

W twoim przypadku kod jest wyraźnie kandydatem do RVO, ponieważ zwraca obiekt lokalny, pa jego typ pjest taki sam, jak typ zwracany, co powoduje usunięcie kopii. A jeśli kompilator zdecyduje się nie ominąć kopii, z jakiegokolwiek powodu, std::movewszedłby do linii 1.


5

Jedną rzeczą, której nie widziałem w innych odpowiedziach, jestAby wyjaśnić inne odpowiedzi, że istnieje różnica między zwracaniem std :: unique_ptr, który został utworzony w funkcji, a tą, która została mu przekazana.

Przykład może wyglądać tak:

class Test
{int i;};
std::unique_ptr<Test> foo1()
{
    std::unique_ptr<Test> res(new Test);
    return res;
}
std::unique_ptr<Test> foo2(std::unique_ptr<Test>&& t)
{
    // return t;  // this will produce an error!
    return std::move(t);
}

//...
auto test1=foo1();
auto test2=foo2(std::unique_ptr<Test>(new Test));

Wspomina o tym fredoverflow - wyraźnie zaznaczony „ automatyczny obiekt”. Odwołanie (w tym odwołanie do wartości) nie jest obiektem automatycznym.
Toby Speight

@TobySpeight Ok, przepraszam. Chyba mój kod jest więc tylko wyjaśnieniem.
v010dya
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.