Co to jest semantyka ruchu?


1701

Właśnie skończyłem słuchać wywiadu radiowego podcastu Software Engineering ze Scottem Meyersem na temat C ++ 0x . Większość nowych funkcji ma dla mnie sens i jestem podekscytowany C ++ 0x, z wyjątkiem jednej. Nadal nie dostaję semantyki ruchu ... Co to dokładnie jest?


20
Znalazłem [artykuł na blogu Eli Bendersky] ( eli.thegreenplace.net/2011/12/15/... ) o wartościach i wartościach w C i C ++ dość pouczających. Wspomina także o referencjach do wartości w C ++ 11 i przedstawia je małymi przykładami.
Nils,


19
Co roku zastanawiam się, na czym polega „nowa” semantyka przenoszenia w C ++, szukam w Google i przechodzę do tej strony. Czytam odpowiedzi, mój mózg się wyłącza. Wracam do C i wszystko zapominam! Jestem zakleszczony.
niebo,

7
@sky Rozważ std :: vector <> ... Gdzieś tam jest wskaźnik do tablicy na stercie. Jeśli skopiujesz ten obiekt, nowy bufor musi zostać przydzielony, a dane z bufora muszą zostać skopiowane do nowego bufora. Czy są jakieś okoliczności, w których dobrze byłoby po prostu ukraść wskaźnik? Odpowiedź brzmi TAK, gdy kompilator wie, że obiekt jest tymczasowy. Semantyka przesuwania pozwala określić, w jaki sposób wnętrzności klas mogą być przenoszone i upuszczane w innym obiekcie, gdy kompilator wie, że obiekt, z którego się poruszasz, wkrótce zniknie.
dicroce,

Jedyne odniesienie, które mogę zrozumieć: learncpp.com/cpp-tutorial/... , tzn. Pierwotne uzasadnienie semantyki przenoszenia pochodzi z inteligentnych wskaźników.
jw_

Odpowiedzi:


2479

Najłatwiej jest mi zrozumieć semantykę przenoszenia z przykładowym kodem. Zacznijmy od bardzo prostej klasy łańcuchowej, która zawiera tylko wskaźnik do bloku pamięci przydzielonego na stos:

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = std::strlen(p) + 1;
        data = new char[size];
        std::memcpy(data, p, size);
    }

Ponieważ sami postanowiliśmy zarządzać pamięcią, musimy przestrzegać zasady trzech . Mam zamiar odłożyć pisanie operatora przypisania i zaimplementować na razie tylko destruktor i konstruktor kopiowania:

    ~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = std::strlen(that.data) + 1;
        data = new char[size];
        std::memcpy(data, that.data, size);
    }

Konstruktor kopiowania definiuje, co to znaczy kopiować obiekty łańcuchowe. Ten parametr const string& thatwiąże się ze wszystkimi wyrażeniami typu string, co pozwala na tworzenie kopii w następujących przykładach:

string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3

Teraz pojawia się kluczowy wgląd w semantykę ruchu. Zauważ, że tylko w pierwszym wierszu, w którym kopiujemy, xta głęboka kopia jest naprawdę konieczna, ponieważ możemy chcieć sprawdzić xpóźniej i bylibyśmy bardzo zaskoczeni, gdyby xsię jakoś zmieniła. Czy zauważyłeś, jak powiedziałem xtrzy razy (cztery razy, jeśli załączysz to zdanie) i oznaczało dokładnie ten sam obiekt za każdym razem? Wyrażenia takie jak x„wartości lv”.

Argumenty w wierszach 2 i 3 nie są wartościami wartościowymi, lecz wartościami wartościowymi, ponieważ leżące u ich podstaw obiekty łańcuchowe nie mają nazw, więc klient nie ma możliwości ich ponownego sprawdzenia w późniejszym czasie. wartości r oznaczają wartości tymczasowe, które są niszczone w następnym średniku (a ściślej: na końcu pełnego wyrażenia, które leksykalnie zawiera wartość rvalue). Jest to ważne, ponieważ podczas inicjalizacji bi cmogliśmy zrobić wszystko, co chcieliśmy z łańcuchem źródłowym, a klient nie mógł powiedzieć różnicy !

C ++ 0x wprowadza nowy mechanizm o nazwie „odwołanie do wartości”, który między innymi pozwala nam wykryć argumenty wartości poprzez przeciążenie funkcji. Wszystko, co musimy zrobić, to napisać konstruktor z parametrem odniesienia wartości. Wewnątrz tego konstruktora możemy zrobić wszystko, co chcemy ze źródłem, o ile pozostawimy go w pewnym prawidłowym stanie:

    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }

Co my tu zrobiliśmy? Zamiast dogłębnego kopiowania danych sterty, właśnie skopiowaliśmy wskaźnik, a następnie ustawiliśmy oryginalny wskaźnik na null (aby zapobiec „delete []” z destruktora obiektu źródłowego przed zwolnieniem naszych „właśnie skradzionych danych”). W rezultacie „ukradliśmy” dane, które pierwotnie należały do ​​ciągu źródłowego. Ponownie kluczową kwestią jest to, że pod żadnym pozorem klient nie mógł wykryć, że źródło zostało zmodyfikowane. Ponieważ tak naprawdę nie robimy tutaj kopii, nazywamy ten konstruktor „konstruktorem ruchu”. Jego zadaniem jest przenoszenie zasobów z jednego obiektu do drugiego zamiast ich kopiowania.

Gratulacje, teraz rozumiesz podstawy semantyki ruchów! Kontynuujmy, wdrażając operatora przypisania. Jeśli nie znasz idiomu kopiowania i zamiany , naucz się go i wróć, ponieważ jest to niesamowity idiom C ++ związany z bezpieczeństwem wyjątków.

    string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};

Co to jest? „Gdzie jest odniesienie do wartości?” możesz zapytać. „Nie potrzebujemy tego tutaj!” to moja odpowiedź :)

Zauważ, że przekazujemy parametr that przez wartość , więc thatmusi być inicjowany tak jak każdy inny obiekt łańcucha. Dokładnie w jaki sposób thatzostanie zainicjowany? W dawnych czasach C ++ 98 odpowiedzią byłby „konstruktor kopii”. W C ++ 0x kompilator wybiera między konstruktorem kopiującym a konstruktorem przenoszenia na podstawie tego, czy argumentem operatora przypisania jest wartość lvalue czy rvalue.

Więc jeśli powiesz a = b, konstruktor kopiowania zostanie zainicjowany that(ponieważ wyrażenie bjest wartością), a operator przypisania zamieni zawartość z świeżo utworzoną, głęboką kopią. To jest właśnie definicja kopiowania i zamiany idiomu - zrób kopię, zamień zawartość z kopią, a następnie pozbądź się kopii, opuszczając zakres. Nic nowego tutaj.

Ale jeśli powiesz a = x + y, konstruktor ruchu zostanie zainicjowany that(ponieważ wyrażenie x + yjest wartością), więc nie ma w nim głębokiej kopii, tylko efektywny ruch. thatjest nadal niezależnym obiektem od argumentu, ale jego konstrukcja była trywialna, ponieważ dane sterty nie musiały być kopiowane, tylko przenoszone. Nie trzeba go było kopiować, ponieważ x + yjest to wartość, i znowu można przesuwać się od obiektów ciągów oznaczonych wartościami rvalues.

Podsumowując, konstruktor kopii tworzy głęboką kopię, ponieważ źródło musi pozostać nietknięte. Z drugiej strony konstruktor ruchu może po prostu skopiować wskaźnik, a następnie ustawić wskaźnik w źródle na null. W ten sposób można „zerować” obiekt źródłowy, ponieważ klient nie ma możliwości ponownej inspekcji obiektu.

Mam nadzieję, że ten przykład zdobył główny punkt. Jest o wiele więcej do wartościowania referencji i przenoszenia semantyki, które celowo pominąłem, aby było to proste. Jeśli chcesz uzyskać więcej informacji, zobacz moją dodatkową odpowiedź .


40
@Ale jeśli mój ctor dostaje wartość, której nigdy nie można użyć później, dlaczego w ogóle muszę zawracać sobie głowę pozostawieniem jej w spójnym / bezpiecznym stanie? Zamiast ustawiać that.data = 0, dlaczego by tak nie zostawić?
einpoklum

70
@einpoklum Ponieważ bez that.data = 0tego postacie zostałyby zniszczone o wiele za wcześnie (kiedy umiera tymczasowy), a także dwukrotnie. Chcesz ukraść dane, a nie je udostępniać!
fredoverflow

19
@einpoklum Regularnie zaplanowany destruktor jest nadal uruchamiany, więc musisz upewnić się, że stan po przeniesieniu obiektu źródłowego nie spowoduje awarii. Lepiej upewnij się, że obiekt źródłowy może być również odbiorcą zadania lub innego zapisu.
CTMacUser

12
@pranitkothari Tak, wszystkie obiekty muszą zostać zniszczone, nawet przeniesione z obiektów. A ponieważ nie chcemy, aby tablica char była usuwana, kiedy to się dzieje, musimy ustawić wskaźnik na null.
fredoverflow

7
@ Virus721 delete[]na nullptr jest zdefiniowany przez standard C ++ jako no-op.
fredoverflow

1057

Moją pierwszą odpowiedzią było bardzo uproszczone wprowadzenie do semantyki przenoszenia, a wiele szczegółów zostało pominiętych, aby uprościć to. Jednak semantyka wymaga znacznie więcej i pomyślałem, że nadszedł czas na drugą odpowiedź, aby wypełnić luki. Pierwsza odpowiedź jest już dość stara i nie wystarczyło zastąpić ją zupełnie innym tekstem. Myślę, że nadal dobrze służy jako pierwsze wprowadzenie. Ale jeśli chcesz kopać głębiej, czytaj dalej :)

Stephan T. Lavavej poświęcił czas na przekazanie cennych informacji zwrotnych. Dziękuję bardzo, Stephan!

Wprowadzenie

Przenieś semantykę pozwala obiektowi, pod pewnymi warunkami, przejąć na własność zasoby zewnętrzne innego obiektu. Jest to ważne na dwa sposoby:

  1. Przekształcanie drogich kopii w tanie ruchy. Zobacz moją pierwszą odpowiedź na przykład. Należy zauważyć, że jeśli obiekt nie zarządza co najmniej jednym zasobem zewnętrznym (bezpośrednio lub pośrednio poprzez swoje obiekty składowe), semantyka przenoszenia nie będzie oferować żadnych korzyści w porównaniu z semantyką kopiowania. W takim przypadku kopiowanie obiektu i przenoszenie obiektu oznacza dokładnie to samo:

    class cannot_benefit_from_move_semantics
    {
        int a;        // moving an int means copying an int
        float b;      // moving a float means copying a float
        double c;     // moving a double means copying a double
        char d[64];   // moving a char array means copying a char array
    
        // ...
    };
  2. Wdrażanie bezpiecznych typów „tylko ruch”; to znaczy typy, dla których kopiowanie nie ma sensu, ale przenosi. Przykłady obejmują zamki, uchwyty plików i inteligentne wskaźniki z unikalną semantyką własności. Uwaga: W tej odpowiedzi omówiono std::auto_ptrprzestarzały szablon biblioteki standardowej C ++ 98, który został zastąpiony przez std::unique_ptrC ++ 11. Pośredni programiści C ++ są prawdopodobnie przynajmniej w pewnym stopniu zaznajomieni std::auto_ptr, a ze względu na wyświetlaną „semantykę ruchu” wydaje się to dobrym punktem wyjścia do omawiania semantyki ruchu w C ++ 11. YMMV.

Co to jest ruch?

Standardowa biblioteka C ++ 98 oferuje inteligentny wskaźnik o unikalnej semantyce własności, tzw std::auto_ptr<T>. Jeśli nie jesteś zaznajomiony auto_ptr, jego celem jest zagwarantowanie, że dynamicznie przydzielany obiekt jest zawsze zwalniany, nawet w obliczu wyjątków:

{
    std::auto_ptr<Shape> a(new Triangle);
    // ...
    // arbitrary code, could throw exceptions
    // ...
}   // <--- when a goes out of scope, the triangle is deleted automatically

Niezwykłe auto_ptrjest to, że „kopiuje”:

auto_ptr<Shape> a(new Triangle);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        |
        |
  +-----|---+
  |   +-|-+ |
a | p | | | |
  |   +---+ |
  +---------+

auto_ptr<Shape> b(a);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        +----------------------+
                               |
  +---------+            +-----|---+
  |   +---+ |            |   +-|-+ |
a | p |   | |          b | p | | | |
  |   +---+ |            |   +---+ |
  +---------+            +---------+

Uwaga, w jaki sposób inicjalizacji bze ama nie kopiować trójkąt, lecz przenosi własności trójkąta od acelu b. My również powiedzieć „ ajest przeniesiony do b ” lub „trójkąt jest przeniesiony z a celu b ”. Może się to wydawać mylące, ponieważ sam trójkąt zawsze pozostaje w tym samym miejscu w pamięci.

Przeniesienie obiektu oznacza przeniesienie własności niektórych zasobów, którymi zarządza, na inny obiekt.

Konstruktor kopii auto_ptrprawdopodobnie wygląda mniej więcej tak (nieco uproszczony):

auto_ptr(auto_ptr& source)   // note the missing const
{
    p = source.p;
    source.p = 0;   // now the source no longer owns the object
}

Niebezpieczne i nieszkodliwe ruchy

Niebezpieczne auto_ptrjest to, że to, co syntaktycznie wygląda na kopię, jest w rzeczywistości ruchem. Próba wywołania funkcji członka w przypadku przeniesienia auto_ptrspowoduje wywołanie niezdefiniowanego zachowania, dlatego należy bardzo uważać, aby nie używać tej funkcji auto_ptrpo przeniesieniu z:

auto_ptr<Shape> a(new Triangle);   // create triangle
auto_ptr<Shape> b(a);              // move a into b
double area = a->area();           // undefined behavior

Ale auto_ptrnie zawsze jest niebezpieczne. Funkcje fabryczne są doskonałym rozwiązaniem dla auto_ptr:

auto_ptr<Shape> make_triangle()
{
    return auto_ptr<Shape>(new Triangle);
}

auto_ptr<Shape> c(make_triangle());      // move temporary into c
double area = make_triangle()->area();   // perfectly safe

Zwróć uwagę, że oba przykłady stosują ten sam wzorzec składniowy:

auto_ptr<Shape> variable(expression);
double area = expression->area();

A jednak jeden z nich przywołuje niezdefiniowane zachowanie, podczas gdy drugi nie. Jaka jest różnica między wyrażeniami aa make_triangle()? Czyż nie są tego samego typu? Rzeczywiście są, ale mają różne kategorie wartości .

Kategorie wartości

Oczywiście musi istnieć głęboka różnica między wyrażeniem aoznaczającym auto_ptrzmienną, a wyrażeniem make_triangle()oznaczającym wywołanie funkcji zwracającej auto_ptrwartość, tworząc w ten sposób świeży auto_ptrobiekt tymczasowy przy każdym wywołaniu. ajest przykładem lwartości , natomiast make_triangle()jest przykładem rvalue .

Przejście od wartości lv, takich jak ajest niebezpieczne, ponieważ moglibyśmy później spróbować wywołać funkcję członka przez awywołanie niezdefiniowanego zachowania. Z drugiej strony przejście z wartości rvalu, takich jak make_triangle()jest całkowicie bezpieczne, ponieważ po wykonaniu przez konstruktora kopiowania pracy, nie możemy ponownie użyć tymczasowego. Nie ma wyrażenia oznaczającego powiedzenie tymczasowe; jeśli po prostu napiszemy make_triangle()ponownie, otrzymamy inny tymczasowy. W rzeczywistości przeniesiony z tymczasowego już nie ma w następnym wierszu:

auto_ptr<Shape> c(make_triangle());
                                  ^ the moved-from temporary dies right here

Zwróć uwagę, że litery li rmają historyczne pochodzenie po lewej i prawej stronie zadania. Nie jest to już prawdą w C ++, ponieważ istnieją wartości lv, które nie mogą pojawić się po lewej stronie przypisania (takie jak tablice lub typy zdefiniowane przez użytkownika bez operatora przypisania), i istnieją wartości, które mogą (wszystkie wartości dla typów klas z operatorem przypisania).

Wartość typu klasy jest wyrażeniem, którego ocena tworzy obiekt tymczasowy. W normalnych okolicznościach żadne inne wyrażenie w tym samym zakresie nie oznacza tego samego obiektu tymczasowego.

Referencje wartości

Rozumiemy teraz, że przejście od wartości jest potencjalnie niebezpieczne, ale przejście od wartości jest nieszkodliwe. Gdyby C ++ miał obsługę języka w celu odróżnienia argumentów lvalue od argumentów rvalue, moglibyśmy całkowicie zabronić przejścia od wartości lv, lub przynajmniej uczynić przejście od wartości lvue jawnie w miejscu wywołania, abyśmy nie przemieszczali się przypadkowo.

Odpowiedź C ++ 11 na ten problem to odwołania do wartości . Odwołanie do wartości jest nowym rodzajem odniesienia, które wiąże się tylko z wartościami, a składnia jest następująca X&&. Stare dobre odniesienie X&jest teraz znane jako odwołanie do wartości . (Uwaga: nieX&& jest to odwołanie do odwołania; w C ++ nie ma czegoś takiego).

Jeśli wrzucimy constdo miksu, mamy już cztery różne rodzaje referencji. Z Xjakimi rodzajami wyrażeń typu mogą się wiązać?

            lvalue   const lvalue   rvalue   const rvalue
---------------------------------------------------------              
X&          yes
const X&    yes      yes            yes      yes
X&&                                 yes
const X&&                           yes      yes

W praktyce możesz zapomnieć const X&&. Ograniczenie do czytania z wartości jest mało przydatne.

Odwołanie do wartości X&&jest nowym rodzajem odniesienia, które wiąże się tylko z wartościami.

Niejawne konwersje

Odniesienia do wartości przeszły przez kilka wersji. Od wersji 2.1 odwołanie X&&do wartości wiąże się również ze wszystkimi kategoriami wartości innego typu Y, pod warunkiem, że następuje niejawna konwersja z Yna X. W takim przypadku Xtworzony jest tymczasowy typ , a odwołanie do wartości jest powiązane z tym tymczasowym:

void some_function(std::string&& r);

some_function("hello world");

W powyższym przykładzie "hello world"jest wartością typu const char[12]. Ponieważ istnieje niejawna konwersja od const char[12]do const char*do std::string, std::stringtworzony jest typ tymczasowy , który rjest z nim związany. Jest to jeden z przypadków, w których rozróżnienie między wartościami (wyrażeniami) i tymczasowymi (obiektami) jest nieco rozmyte.

Przenieś konstruktorów

Przydatnym przykładem funkcji z X&&parametrem jest konstruktor ruchu X::X(X&& source) . Jego celem jest przeniesienie własności zarządzanego zasobu ze źródła do bieżącego obiektu.

W C ++ 11 std::auto_ptr<T>został zastąpiony przez, std::unique_ptr<T>który wykorzystuje odwołania do wartości. Opracuję i omówię uproszczoną wersję unique_ptr. Po pierwsze, możemy upakować surowego i wskaźnik przeciążenia operatorów ->i *tak nasza klasa czuje się jak wskaźnik:

template<typename T>
class unique_ptr
{
    T* ptr;

public:

    T* operator->() const
    {
        return ptr;
    }

    T& operator*() const
    {
        return *ptr;
    }

Konstruktor przejmuje własność obiektu, a destruktor usuwa go:

    explicit unique_ptr(T* p = nullptr)
    {
        ptr = p;
    }

    ~unique_ptr()
    {
        delete ptr;
    }

Teraz nadchodzi interesująca część, konstruktor ruchu:

    unique_ptr(unique_ptr&& source)   // note the rvalue reference
    {
        ptr = source.ptr;
        source.ptr = nullptr;
    }

Ten konstruktor ruchu wykonuje dokładnie to samo, co auto_ptrkonstruktor kopiowania, ale może być dostarczony tylko z wartościami rvalues:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);                 // error
unique_ptr<Shape> c(make_triangle());   // okay

Druga linia się nie kompiluje, ponieważ ajest to wartość, ale parametr unique_ptr&& sourcemożna powiązać tylko z wartościami. Właśnie tego chcieliśmy; niebezpieczne ruchy nigdy nie powinny być niejawne. Trzecia linia kompiluje się dobrze, ponieważ make_triangle()jest to wartość. Konstruktor przeniesienia przeniesie własność z tymczasowej na c. Znowu tego właśnie chcieliśmy.

Konstruktor przenoszenia przenosi własność zarządzanego zasobu na bieżący obiekt.

Przenieś operatory przypisania

Ostatnim brakującym elementem jest operator przypisania ruchu. Jego zadaniem jest zwolnienie starego zasobu i pozyskanie nowego zasobu z argumentu:

    unique_ptr& operator=(unique_ptr&& source)   // note the rvalue reference
    {
        if (this != &source)    // beware of self-assignment
        {
            delete ptr;         // release the old resource

            ptr = source.ptr;   // acquire the new resource
            source.ptr = nullptr;
        }
        return *this;
    }
};

Zwróć uwagę, jak ta implementacja operatora przypisania ruchu powiela logikę zarówno destruktora, jak i konstruktora ruchu. Czy znasz idiom kopiowania i zamiany? Może być również zastosowany do semantyki przenoszenia jako idiom ruchu i zamiany:

    unique_ptr& operator=(unique_ptr source)   // note the missing reference
    {
        std::swap(ptr, source.ptr);
        return *this;
    }
};

Teraz sourcejest to zmienna typu unique_ptr, zostanie ona zainicjowana przez konstruktor ruchu; to znaczy argument zostanie przeniesiony do parametru. Argument ten nadal musi być wartością rvalue, ponieważ sam konstruktor przenoszenia ma parametr odniesienia wartości rvalue. Gdy przepływ sterowania osiągnie nawias zamykający operator=, sourcewychodzi poza zakres, automatycznie zwalniając stary zasób.

Operator przypisania przeniesienia przenosi własność zarządzanego zasobu na bieżący obiekt, zwalniając stary zasób. Idiom move-and-swap upraszcza implementację.

Odejście od wartości

Czasami chcemy przejść od wartości lv. Oznacza to, że czasami chcemy, aby kompilator traktował wartość tak, jakby była wartością, aby mógł wywołać konstruktor ruchu, nawet jeśli może być potencjalnie niebezpieczny. W tym celu C ++ 11 oferuje standardowy szablon funkcji biblioteki zwany std::movewewnątrz nagłówka <utility>. Ta nazwa jest trochę niefortunna, ponieważ std::movepo prostu rzuca wartość na wartość; sam niczego nie porusza. To po prostu pozwala się poruszać. Może powinna była zostać nazwana std::cast_to_rvaluelub std::enable_move, ale utknęliśmy już z tym imieniem.

Oto, w jaki sposób wyraźnie przechodzisz od wartości:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);              // still an error
unique_ptr<Shape> c(std::move(a));   // okay

Zauważ, że po trzeciej linii anie ma już trójkąta. To jest w porządku, ponieważ wyraźnie pisząc std::move(a), jasno wyraziliśmy nasze intencje: „Drogi konstruktorze, zrób wszystko, co chcesz a, aby zainicjować c; Już mnie to nie obchodzi a. Nie krępuj się a.”

std::move(some_lvalue) rzuca wartość na wartość, umożliwiając w ten sposób kolejny ruch.

Xvalues

Zauważ, że pomimo tego, że std::move(a)jest to wartość, jej ocena nie tworzy obiektu tymczasowego. Ta zagadka zmusiła komitet do wprowadzenia trzeciej kategorii wartości. Coś, co można powiązać z odwołaniem do wartości, chociaż nie jest to wartość w tradycyjnym znaczeniu, nazywa się wartością x (wartość eXpiring). Tradycyjne wartości zostały przemianowane na wartości (wartości czyste).

Zarówno wartości, jak i wartości x są wartościami. Xvalues ​​i lvalues ​​są zarówno wartościami glvalu (Uogólnione wartości lvalu). Relacje łatwiej zrozumieć za pomocą diagramu:

        expressions
          /     \
         /       \
        /         \
    glvalues   rvalues
      /  \       /  \
     /    \     /    \
    /      \   /      \
lvalues   xvalues   prvalues

Zauważ, że tylko wartości x są naprawdę nowe; reszta wynika właśnie z zmiany nazwy i grupowania.

Wartości C ++ 98 są znane jako prvalues ​​w C ++ 11. Mentalnie zamień wszystkie wystąpienia „wartości” w poprzednich akapitach na „wartość”.

Wyprowadzanie się z funkcji

Do tej pory widzieliśmy ruch do zmiennych lokalnych i parametrów funkcji. Ale ruch jest również możliwy w przeciwnym kierunku. Jeśli funkcja zwraca wartość, jakiś obiekt w miejscu wywołania (prawdopodobnie zmienna lokalna lub tymczasowa, ale może to być dowolny obiekt) jest inicjowany wyrażeniem po returninstrukcji jako argumentem konstruktora przenoszenia:

unique_ptr<Shape> make_triangle()
{
    return unique_ptr<Shape>(new Triangle);
}          \-----------------------------/
                  |
                  | temporary is moved into c
                  |
                  v
unique_ptr<Shape> c(make_triangle());

Być może, co zaskakujące, obiekty automatyczne (zmienne lokalne, które nie są zadeklarowane jako static) mogą być również domyślnie usunięte z funkcji:

unique_ptr<Shape> make_square()
{
    unique_ptr<Shape> result(new Square);
    return result;   // note the missing std::move
}

Dlaczego konstruktor ruchów przyjmuje wartość resultjako argument? Zasięg zbliża resultsię do końca i zostanie on zniszczony podczas rozwijania stosu. Nikt później nie mógł narzekać, że resultto się jakoś zmieniło; gdy przepływ kontrolny powraca do dzwoniącego, resultjuż nie istnieje! Z tego powodu C ++ 11 ma specjalną regułę, która pozwala na zwracanie automatycznych obiektów z funkcji bez konieczności pisania std::move. W rzeczywistości nigdy nie należy używać std::movedo przenoszenia automatycznych obiektów z funkcji, ponieważ hamuje to „nazwaną optymalizację wartości zwrotnej” (NRVO).

Nigdy nie używaj std::movedo przenoszenia automatycznych obiektów z funkcji.

Należy zauważyć, że w obu funkcjach fabrycznych typ zwracany jest wartością, a nie odniesieniem do wartości. Odwołania do wartości są nadal odwołaniami i jak zawsze, nigdy nie powinieneś zwracać odniesienia do obiektu automatycznego; program wywołujący miałby zwisające odniesienie, gdybyś nakłonił kompilator do zaakceptowania kodu, w następujący sposób:

unique_ptr<Shape>&& flawed_attempt()   // DO NOT DO THIS!
{
    unique_ptr<Shape> very_bad_idea(new Square);
    return std::move(very_bad_idea);   // WRONG!
}

Nigdy nie zwracaj automatycznych obiektów przez odwołanie do wartości. Przenoszenie jest wykonywane wyłącznie przez konstruktora ruchów std::move, a nie przez zwykłe powiązanie wartości z odwołaniem do wartości.

Przeprowadzka do członków

Wcześniej czy później napiszesz taki kod:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(parameter)   // error
    {}
};

Zasadniczo kompilator będzie narzekał, że parameterjest to wartość. Jeśli spojrzysz na jego typ, zobaczysz odwołanie do wartości, ale odwołanie do wartości oznacza po prostu „odwołanie powiązane z wartością”; to jednak nie znaczy, że sama odniesienia jest rvalue! Rzeczywiście parameterjest zwykłą zmienną o nazwie. Możesz używać parametertak często, jak chcesz w ciele konstruktora i zawsze oznacza ten sam obiekt. Domniemane odejście od niego byłoby niebezpieczne, dlatego język tego zabrania.

Nazwane odwołanie do wartości jest wartością, podobnie jak każda inna zmienna.

Rozwiązaniem jest ręczne włączenie przenoszenia:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(std::move(parameter))   // note the std::move
    {}
};

Można argumentować, że parameternie jest już używany po inicjalizacji member. Dlaczego nie ma specjalnej reguły, aby wstawiać dyskretnie, std::movepodobnie jak w przypadku zwracanych wartości? Prawdopodobnie dlatego, że byłoby to zbyt duże obciążenie dla implementatorów kompilatora. Na przykład, co jeśli ciało konstruktora znajdowało się w innej jednostce tłumaczeniowej? Natomiast reguła wartości zwracanej musi po prostu sprawdzać tabele symboli, aby ustalić, czy identyfikator po returnsłowie kluczowym oznacza automatyczny obiekt.

Możesz także przekazać parameterwartość. W przypadku typów typu „tylko ruch” unique_ptrwydaje się, że nie ma jeszcze ustalonego idiomu. Osobiście wolę przekazywać według wartości, ponieważ powoduje to mniej bałaganu w interfejsie.

Specjalne funkcje członka

C ++ 98 domyślnie deklaruje trzy specjalne funkcje składowe na żądanie, to znaczy, gdy są one gdzieś potrzebne: konstruktor kopii, operator przypisania kopii i destruktor.

X::X(const X&);              // copy constructor
X& X::operator=(const X&);   // copy assignment operator
X::~X();                     // destructor

Referencje dotyczące wartości przeszły przez kilka wersji. Od wersji 3.0 C ++ 11 deklaruje na żądanie dwie dodatkowe specjalne funkcje składowe: konstruktor ruchu i operator przypisania ruchu. Pamiętaj, że ani VC10, ani VC11 nie są jeszcze zgodne z wersją 3.0, więc będziesz musiał je zaimplementować samodzielnie.

X::X(X&&);                   // move constructor
X& X::operator=(X&&);        // move assignment operator

Te dwie nowe specjalne funkcje składowe są deklarowane domyślnie tylko wtedy, gdy żadna ze specjalnych funkcji składowych nie jest zadeklarowana ręcznie. Ponadto, jeśli zadeklarujesz własnego konstruktora przeniesienia lub operatora przypisania przeniesienia, ani konstruktor kopiowania, ani operator przypisania kopiowania nie zostaną zadeklarowane pośrednio.

Co te zasady oznaczają w praktyce?

Jeśli napiszesz klasę bez niezarządzanych zasobów, nie musisz samodzielnie deklarować żadnej z pięciu specjalnych funkcji składowych, a otrzymasz poprawną semantykę kopiowania i przeniesiesz semantykę za darmo. W przeciwnym razie będziesz musiał samodzielnie wdrożyć specjalne funkcje składowe. Oczywiście, jeśli twoja klasa nie korzysta z semantyki ruchu, nie ma potrzeby implementowania specjalnych operacji przenoszenia.

Zauważ, że operator przypisania kopii i operator przypisania przeniesienia można połączyć w jeden, zunifikowany operator przypisania, biorąc jego argument według wartości:

X& X::operator=(X source)    // unified assignment operator
{
    swap(source);            // see my first answer for an explanation
    return *this;
}

W ten sposób liczba specjalnych funkcji składowych do implementacji spada z pięciu do czterech. Istnieje tutaj kompromis między bezpieczeństwem wyjątkowym a wydajnością, ale nie jestem ekspertem w tej kwestii.

Przekazywanie referencji ( wcześniej znane jako Uniwersalne referencje )

Rozważ następujący szablon funkcji:

template<typename T>
void foo(T&&);

Można się spodziewać T&&wiązania wyłącznie wartości rvalu, ponieważ na pierwszy rzut oka wygląda jak odniesienie do wartości. Jak się jednak okazuje, T&&wiąże się również z wartościami:

foo(make_triangle());   // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a);                 // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&

Jeśli argumentem jest wartość typu X, Tnależy się spodziewać X, że T&&oznacza to X&&. Tego się wszyscy spodziewają. Ale jeśli argumentem jest wartość typu X, ze względu na specjalną regułę, Tnależy się spodziewać X&, a więc T&&oznaczałoby coś takiego X& &&. Ale ponieważ C ++ nadal nie ma pojęcia odniesień do odniesień, typ X& &&jest załamał się X&. Na początku może się to wydawać mylące i bezużyteczne, ale zwijanie referencji jest niezbędne dla idealnego przekazywania (co nie będzie tutaj omawiane).

T&& nie jest referencją do wartości, ale referencją do przekazania. Wiąże się to również z wartościami lvalu, w którym to przypadku Ti T&&są obydwoma wartościami referencyjnymi.

Jeśli chcesz ograniczyć szablon funkcji do wartości, możesz połączyć SFINAE z cechami typu:

#include <type_traits>

template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);

Realizacja ruchu

Teraz, gdy rozumiesz zwijanie referencji, oto jak std::movemożna je zaimplementować:

template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

Jak widać, moveakceptuje dowolny rodzaj parametru dzięki referencji przekazywania T&&i zwraca referencję wartości. std::remove_reference<T>::typeWezwanie meta-funkcja jest konieczne, ponieważ w przeciwnym razie, dla lwartościami typu X, rodzaju powrót byłby X& &&, który załamie się X&. Ponieważ tzawsze jest to wartość (pamiętaj, że nazwane odwołanie do wartości jest wartością), ale chcemy powiązać tz odwołaniem do wartości, musimy jawnie rzutować tna poprawny typ zwracany. Wywołanie funkcji, która zwraca odwołanie do wartości, samo w sobie jest wartością x. Teraz już wiesz, skąd pochodzą wartości x;)

Wywołanie funkcji, która zwraca odwołanie do wartości, takie jak std::move, jest wartością x.

Zwróć uwagę, że w tym przykładzie zwracanie przez odwołanie do wartości jest w porządku, ponieważ tnie oznacza obiektu automatycznego, ale obiekt przekazany przez program wywołujący.



24
Istnieje trzeci powód, dla którego semantyka przenoszenia jest ważna: bezpieczeństwo wyjątkowe. Często tam, gdzie może zostać rzucona operacja kopiowania (ponieważ musi ona przydzielić zasoby, a alokacja może się nie powieść), operacja przenoszenia może nie być wykonywana (ponieważ może przenosić własność istniejących zasobów zamiast przydzielania nowych). Operacje, które nie mogą zakończyć się niepowodzeniem, są zawsze przyjemne i mogą mieć kluczowe znaczenie podczas pisania kodu, który zapewnia wyjątkowe gwarancje.
Brangdon,

8
Byłem z tobą aż do „uniwersalnych referencji”, ale potem jest to zbyt abstrakcyjne, aby je śledzić. Zawalenie się referencji? Idealne przekazywanie? Czy mówisz, że odwołanie do wartości staje się odniesieniem uniwersalnym, jeśli typ jest wzorowany? Chciałbym, aby istniał sposób na wyjaśnienie tego, bym wiedział, czy muszę to zrozumieć, czy nie! :)
Kylotan

8
Proszę napisać książkę teraz ... ta odpowiedź dała mi powód, by sądzić, że jeśli przejrzysz inne zakątki C ++ w tak przejrzysty sposób, jak to, tysiące innych osób to zrozumie.
halivingston

12
@halivingston Dziękuję bardzo za życzliwe opinie, naprawdę to doceniam. Problem z pisaniem książki jest taki, że jest to o wiele więcej pracy, niż można sobie wyobrazić. Jeśli chcesz zagłębić się w C ++ 11 i nie tylko, sugeruję zakup „Effective Modern C ++” Scott Meyers.
fredoverflow

77

Semantyka przenoszenia oparta jest na odwołaniach do wartości .
Wartość jest obiektem tymczasowym, który zostanie zniszczony na końcu wyrażenia. W obecnym C ++ wartości rwiążą się tylko z constreferencjami. C ++ 1x zezwoli na constodniesienia nie będące wartościami, pisane T&&, które są odniesieniami do obiektów wartości.
Ponieważ wartość umrze na końcu wyrażenia, możesz ukraść jej dane . Zamiast kopiować go do innego obiektu, przenosisz do niego dane.

class X {
public: 
  X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
    : data_()
  {
     // since 'x' is an rvalue object, we can steal its data
     this->swap(std::move(rhs));
     // this will leave rhs with the empty data
  }
  void swap(X&& rhs);
  // ... 
};

// ...

X f();

X x = f(); // f() returns result as rvalue, so this calls move-ctor

W powyższym kodzie, ze starymi kompilatorów wynikiem f()jest kopiowany do xkorzystania X„s konstruktor kopiujący. Jeśli twój kompilator obsługuje semantykę ruchu i Xma konstruktor ruchu, wówczas jest to wywoływane. Ponieważ jego rhsargument jest wartością , wiemy, że nie jest już potrzebny i możemy ukraść jego wartość.
Tak więc wartość jest przenoszona z nienazwanego tymczasowego zwracanego z f()do x(podczas gdy dane x, zainicjowane na pusty X, są przenoszone do tymczasowego, który zostanie zniszczony po przypisaniu).


1
zwróć uwagę, że powinno tak być, this->swap(std::move(rhs));ponieważ nazwane referencje wartości są wartościami lv
wmamrak

To jest trochę tak, za @ komentarzu Tacyt jest: rhsjest lwartość w kontekście X::X(X&& rhs). Musisz zadzwonić, std::move(rhs)żeby zdobyć wartość, ale ten rodzaj odpowiedzi jest dyskusyjny.
Asherah,

Co porusza semantykę dla typów bez wskaźników? Przenieś semantykę działa podobnie do kopiowania?
Gusev Slava

@Gusev: Nie mam pojęcia o co pytasz.
sbi

60

Załóżmy, że masz funkcję, która zwraca znaczny obiekt:

Matrix multiply(const Matrix &a, const Matrix &b);

Kiedy piszesz taki kod:

Matrix r = multiply(a, b);

wtedy zwykły kompilator C ++ utworzy tymczasowy obiekt dla wyniku multiply(), wywoła konstruktora kopiowania w celu zainicjowania r, a następnie zniszczy tymczasową wartość zwracaną. Przestaw semantykę w C ++ 0x pozwala na wywołanie „konstruktora ruchu” w celu zainicjowania rpoprzez skopiowanie jego zawartości, a następnie odrzucenie wartości tymczasowej bez konieczności jej niszczenia.

Jest to szczególnie ważne, jeśli (jak być może w Matrixpowyższym przykładzie) kopiowany obiekt przydziela dodatkową pamięć na stercie do przechowywania swojej wewnętrznej reprezentacji. Konstruktor kopii musiałby albo wykonać pełną kopię wewnętrznej reprezentacji, albo wewnętrznie korzystać z liczenia referencji i semantyki kopiowania przy zapisie. Konstruktor ruchu pozostawiłby pamięć stosu w spokoju i po prostu skopiował wskaźnik wewnątrz Matrixobiektu.


2
Czym różnią się konstruktory przenoszenia i konstruktory kopiowania?
dicroce

1
@dicroce: Różnią się składnią, jedna wygląda jak Matrix (const Matrix & src) (konstruktor kopiowania), a druga wygląda jak Matrix (Matrix && src) (konstruktor ruchu), sprawdź moją główną odpowiedź na lepszy przykład.
snk_kid

3
@dicroce: Jeden tworzy pusty obiekt, a drugi tworzy kopię. Jeśli dane przechowywane w obiekcie są duże, kopia może być droga. Na przykład std :: vector.
Billy ONeal

1
@ kunj2aan: Podejrzewam, że to zależy od twojego kompilatora. Kompilator może utworzyć tymczasowy obiekt wewnątrz funkcji, a następnie przenieść go do wartości zwracanej przez program wywołujący. Lub może być w stanie bezpośrednio skonstruować obiekt na podstawie wartości zwracanej, bez konieczności używania konstruktora ruchu.
Greg Hewgill

2
@Jichao: To jest optymalizacja nazywa RVO, patrz na to pytanie, aby uzyskać więcej informacji na temat różnicy: stackoverflow.com/questions/5031778/...
Greg Hewgill

30

Jeśli naprawdę interesuje Cię dobre, dogłębne wyjaśnienie semantyki ruchów, bardzo polecam przeczytanie na nich oryginalnej pracy „Propozycja dodania obsługi semantyki ruchu do języka C ++”.

Jest bardzo dostępny i łatwy do odczytania, co stanowi doskonały argument za oferowanymi przez nie korzyściami. Na stronie WG21 dostępne są inne nowsze i aktualne artykuły na temat semantyki ruchów , ale ta jest prawdopodobnie najprostsza, ponieważ podchodzi do rzeczy z najwyższego poziomu i nie zagłębia się w szczegółowe szczegóły języka.


27

Semantyka Move polega na przesyłaniu zasobów, a nie na ich kopiowaniu, gdy nikt już nie potrzebuje wartości źródłowej.

W C ++ 03 obiekty są często kopiowane, tylko w celu zniszczenia lub przeniesienia, zanim jakikolwiek kod ponownie użyje wartości. Na przykład, gdy zwracasz wartość z funkcji - chyba że RVO włączy się - zwracana wartość jest kopiowana do ramki stosu wywołującego, a następnie wychodzi poza zakres i zostaje zniszczona. Jest to tylko jeden z wielu przykładów: patrz wartość przekazywana, gdy obiekt źródłowy jest tymczasowy, algorytmy takie sortpo prostu przestawiają elementy, realokacja, vectorgdy jego wartość capacity()zostanie przekroczona itp.

Kiedy takie pary kopiuj / niszcz są kosztowne, dzieje się tak zwykle dlatego, że obiekt posiada pewne zasoby ciężkie. Na przykład vector<string>może posiadać dynamicznie przydzielany blok pamięci zawierający tablicę stringobiektów, z których każdy ma własną pamięć dynamiczną. Kopiowanie takiego obiektu jest kosztowne: musisz przydzielić nową pamięć dla każdego dynamicznie przydzielonego bloku w źródle i skopiować wszystkie wartości. Następnie musisz cofnąć przydział całej pamięci, którą właśnie skopiowałeś. Jednak przeniesienie dużego vector<string>oznacza po prostu skopiowanie kilku wskaźników (odnoszących się do dynamicznego bloku pamięci) do miejsca docelowego i wyzerowanie ich w źródle.


23

W prostych (praktycznych) kategoriach:

Kopiowanie obiektu oznacza kopiowanie jego „statycznych” elementów i wywoływanie newoperatora dla jego obiektów dynamicznych. Dobrze?

class A
{
   int i, *p;

public:
   A(const A& a) : i(a.i), p(new int(*a.p)) {}
   ~A() { delete p; }
};

Jednak przesunięcie obiektu (powtarzam, z praktycznego punktu widzenia) oznacza tylko skopiowanie wskaźników obiektów dynamicznych, a nie tworzenie nowych.

Ale czy to nie jest niebezpieczne? Oczywiście można dwukrotnie zniszczyć obiekt dynamiczny (błąd segmentacji). Aby tego uniknąć, należy „unieważnić” wskaźniki źródłowe, aby uniknąć ich dwukrotnego zniszczenia:

class A
{
   int i, *p;

public:
   // Movement of an object inside a copy constructor.
   A(const A& a) : i(a.i), p(a.p)
   {
     a.p = nullptr; // pointer invalidated.
   }

   ~A() { delete p; }
   // Deleting NULL, 0 or nullptr (address 0x0) is safe. 
};

Ok, ale jeśli przesunę obiekt, obiekt źródłowy stanie się bezużyteczny, nie? Oczywiście, ale w niektórych sytuacjach jest to bardzo przydatne. Najbardziej oczywistym jest to, że wywołuję funkcję z obiektem anonimowym (obiekt tymczasowy, obiekt wartości ... możesz wywołać ją pod inną nazwą):

void heavyFunction(HeavyType());

W tej sytuacji anonimowy obiekt jest tworzony, następnie kopiowany do parametru funkcji, a następnie usuwany. Dlatego lepiej jest przenieść obiekt, ponieważ nie potrzebujesz obiektu anonimowego i możesz zaoszczędzić czas i pamięć.

Prowadzi to do koncepcji odniesienia do „wartości”. Istnieją w C ++ 11 tylko w celu wykrycia, czy otrzymany obiekt jest anonimowy, czy nie. Myślę, że już wiesz, że „wartość” jest przypisywalną jednostką (lewa część =operatora), więc potrzebujesz nazwanego odwołania do obiektu, aby móc działać jako wartość. Wartość jest dokładnie odwrotna, obiekt bez nazwanych odniesień. Z tego powodu anonimowy obiekt i wartość są synonimami. Więc:

class A
{
   int i, *p;

public:
   // Copy
   A(const A& a) : i(a.i), p(new int(*a.p)) {}

   // Movement (&& means "rvalue reference to")
   A(A&& a) : i(a.i), p(a.p)
   {
      a.p = nullptr;
   }

   ~A() { delete p; }
};

W takim przypadku, gdy obiekt typu Apowinien zostać „skopiowany”, kompilator tworzy odwołanie do wartości lub odwołanie do wartości w zależności od tego, czy przekazany obiekt ma nazwę, czy nie. Gdy nie, wywoływany jest twój konstruktor ruchów i wiesz, że obiekt jest tymczasowy, i możesz przenosić jego obiekty dynamiczne zamiast kopiować je, oszczędzając miejsce i pamięć.

Należy pamiętać, że obiekty „statyczne” są zawsze kopiowane. Nie ma możliwości „przeniesienia” obiektu statycznego (obiektu na stosie, a nie na stosie). Zatem rozróżnienie „ruch” / „kopia”, gdy obiekt nie ma elementów dynamicznych (bezpośrednio lub pośrednio), jest nieistotne.

Jeśli twój obiekt jest złożony, a destruktor ma inne efekty wtórne, takie jak wywołanie funkcji biblioteki, wywołanie innych funkcji globalnych lub cokolwiek to jest, być może lepiej jest zasygnalizować ruch za pomocą flagi:

class Heavy
{
   bool b_moved;
   // staff

public:
   A(const A& a) { /* definition */ }
   A(A&& a) : // initialization list
   {
      a.b_moved = true;
   }

   ~A() { if (!b_moved) /* destruct object */ }
};

Twój kod jest więc krótszy (nie musisz wykonywać nullptrprzypisania dla każdego członka dynamicznego) i bardziej ogólny.

Inne typowe pytanie: jaka jest różnica między A&&i const A&&? Oczywiście w pierwszym przypadku możesz modyfikować obiekt, aw drugim nie, ale praktyczne znaczenie? W drugim przypadku nie można go zmodyfikować, więc nie ma możliwości unieważnienia obiektu (z wyjątkiem zmiennej flagi lub czegoś podobnego) i nie ma praktycznej różnicy w stosunku do konstruktora kopiowania.

A czym jest idealne przekazywanie ? Ważne jest, aby wiedzieć, że „odwołanie do wartości” to odniesienie do nazwanego obiektu w „zakresie osoby wywołującej”. Ale w rzeczywistym zakresie odwołanie do wartości jest nazwą obiektu, więc działa jak obiekt nazwany. Jeśli przekażesz odwołanie do wartości do innej funkcji, przekazujesz nazwany obiekt, więc obiekt nie jest odbierany jak obiekt tymczasowy.

void some_function(A&& a)
{
   other_function(a);
}

Obiekt azostanie skopiowany do rzeczywistego parametru other_function. Jeśli chcesz, aby obiekt abył nadal traktowany jako obiekt tymczasowy, powinieneś użyć std::movefunkcji:

other_function(std::move(a));

W tym wierszu std::movenastąpi rzut ana wartość i other_functionotrzyma obiekt jako obiekt bez nazwy. Oczywiście, jeśli other_functionnie ma specyficznego przeciążenia do pracy z obiektami nienazwanymi, to rozróżnienie nie jest ważne.

Czy to idealne przekazywanie? Nie, ale jesteśmy bardzo blisko. Idealne przekazywanie jest przydatne tylko do pracy z szablonami, aby powiedzieć: jeśli muszę przekazać obiekt do innej funkcji, potrzebuję tego, że jeśli otrzymam nazwany obiekt, obiekt jest przekazywany jako obiekt nazwany, a gdy nie, Chcę przekazać to jak obiekt bez nazwy:

template<typename T>
void some_function(T&& a)
{
   other_function(std::forward<T>(a));
}

Jest to sygnatura prototypowej funkcji wykorzystującej doskonałe przekazywanie, zaimplementowanej w C ++ 11 za pomocą std::forward. Ta funkcja wykorzystuje niektóre reguły tworzenia szablonów:

 `A& && == A&`
 `A&& && == A&&`

Jeśli więc Tjest odniesieniem do wartości A( T = A i), to atakże ( A i && => A i). Jeśli Tjest to odniesienie do wartości A, arównież (A &&&& => A&&). W obu przypadkach ajest nazwanym obiektem w rzeczywistym zasięgu, ale Tzawiera informacje o jego „typie odniesienia” z punktu widzenia zakresu dzwoniącego. Ta informacja ( T) jest przekazywana jako parametr szablonu do, forwarda „a” jest przenoszone lub nie zgodnie z typem T.


20

To jak kopiowanie semantyki, ale zamiast duplikowania wszystkich danych, możesz ukraść dane z obiektu, z którego się „przenosi”.


13

Wiesz, co oznacza semantyka kopiowania, prawda? oznacza to, że masz typy, które można kopiować, w przypadku typów zdefiniowanych przez użytkownika możesz to zdefiniować albo kupić bezpośrednio, pisząc konstruktor i operator przypisania kopii, lub kompilator generuje je niejawnie. Spowoduje to wykonanie kopii.

Semantyka ruchu jest w zasadzie typem zdefiniowanym przez użytkownika z konstruktorem, który przyjmuje referencję wartości r (nowy typ referencji za pomocą && (tak dwa znaki ampersand)), który nie jest stały, nazywa się to konstruktorem ruchu, to samo dotyczy operatora przypisania. Więc co robi konstruktor ruchu, zamiast kopiować pamięć z argumentu źródłowego, „przenosi” pamięć ze źródła do miejsca docelowego.

Kiedy chcesz to zrobić? well std :: vector jest przykładem, powiedzmy, że utworzyłeś tymczasowy std :: vector i zwracasz go z funkcji:

std::vector<foo> get_foos();

Będziesz mieć narzut od konstruktora kopiowania, gdy funkcja powróci, jeśli (i będzie to w C ++ 0x) std :: vector ma konstruktor ruchu zamiast kopiowania, może po prostu ustawić wskaźniki i „przenieść” dynamicznie przydzielane pamięć do nowej instancji. To coś w rodzaju semantyki przeniesienia własności ze std :: auto_ptr.


1
Nie sądzę, że jest to świetny przykład, ponieważ w tych przykładowych przykładach wartości zwracanych Optymalizacja wartości zwracanej prawdopodobnie już eliminuje operację kopiowania.
Zan Lynx

7

Aby zilustrować potrzebę semantyki przenoszenia , rozważmy ten przykład bez semantyki przenoszenia:

Oto funkcja, która pobiera obiekt typu Ti zwraca obiekt tego samego typu T:

T f(T o) { return o; }
  //^^^ new object constructed

Powyższa funkcja używa wywołania według wartości, co oznacza, że ​​po wywołaniu tej funkcji należy zbudować obiekt, aby mogła być użyta przez funkcję.
Ponieważ funkcja zwraca także wartość , dla zwracanej wartości konstruowany jest kolejny nowy obiekt:

T b = f(a);
  //^ new object constructed

Zbudowano dwa nowe obiekty, z których jeden jest obiektem tymczasowym używanym tylko przez czas trwania funkcji.

Gdy nowy obiekt jest tworzony na podstawie wartości zwracanej, wywoływany jest konstruktor kopiujący, aby skopiować zawartość obiektu tymczasowego do nowego obiektu b. Po zakończeniu funkcji obiekt tymczasowy użyty w funkcji wykracza poza zakres i zostaje zniszczony.


Teraz zastanówmy się, co robi konstruktor kopii .

Najpierw należy zainicjować obiekt, a następnie skopiować wszystkie odpowiednie dane ze starego obiektu do nowego.
W zależności od klasy, być może jest to kontener z bardzo dużą ilością danych, co może oznaczać dużo czasu i zużycia pamięci

// Copy constructor
T::T(T &old) {
    copy_data(m_a, old.m_a);
    copy_data(m_b, old.m_b);
    copy_data(m_c, old.m_c);
}

Dzięki semantyce ruchów można teraz uczynić większość tej pracy mniej nieprzyjemną, po prostu przenosząc dane, a nie kopiując.

// Move constructor
T::T(T &&old) noexcept {
    m_a = std::move(old.m_a);
    m_b = std::move(old.m_b);
    m_c = std::move(old.m_c);
}

Przenoszenie danych wymaga ponownego skojarzenia danych z nowym obiektem. I w ogóle nie ma kopii .

Dokonuje się tego poprzez rvalueodniesienie. Odniesienia działa całkiem dużo jak odniesienia z jedną istotną różnicą: RValue odniesienia może być przemieszczany i lwartość nie może.
rvaluelvalue

Od cppreference.com :

Aby umożliwić silną gwarancję wyjątku, konstruktorzy ruchu zdefiniowani przez użytkownika nie powinni zgłaszać wyjątków. W rzeczywistości standardowe kontenery zwykle polegają na std :: move_if_noexcept, aby wybierać między przenoszeniem i kopiowaniem, gdy elementy kontenera muszą zostać przeniesione. Jeśli podano zarówno konstruktory kopiowania, jak i przenoszenia, rozwiązanie przeciążenia wybiera konstruktor ruchu, jeśli argumentem jest wartość (albo wartość taka jak tymczasowa nazwa bez nazwy lub wartość x, taka jak wynik std :: move), i wybiera konstruktor kopii, jeśli argumentem jest wartość (nazwany obiekt lub funkcja / operator zwracająca odwołanie do wartości). Jeśli dostarczony jest tylko konstruktor kopiowania, wszystkie kategorie argumentów go wybierają (o ile pobiera odwołanie do stałej, ponieważ wartości rvalues ​​mogą się wiązać z stałymi odwołaniami), co powoduje, że kopiowanie zastępcze do przenoszenia jest niemożliwe. W wielu sytuacjach konstruktory ruchów są zoptymalizowane, nawet jeśli wywoływałyby obserwowalne skutki uboczne, patrz opis kopiowania. Konstruktor nazywany jest „konstruktorem ruchu”, gdy przyjmuje jako parametr odwołanie do wartości. Przenoszenie czegokolwiek nie jest obowiązkowe, klasa nie musi mieć zasobu, który ma zostać przeniesiony, a „konstruktor przenoszenia” może nie być w stanie przenieść zasobu, jak w dopuszczalnym (ale może nie sensownym) przypadku, gdy parametr jest parametrem odniesienie do wartości stałej const (const T&&).


7

Piszę to, aby upewnić się, że dobrze to rozumiem.

Semantyka przenoszenia została stworzona, aby uniknąć niepotrzebnego kopiowania dużych obiektów. Bjarne Stroustrup w swojej książce „The C ++ Programming Language” wykorzystuje dwa przykłady, w których domyślnie zachodzi niepotrzebne kopiowanie: jeden, zamiana dwóch dużych obiektów i dwa, zwracanie dużego obiektu z metody.

Zamiana dwóch dużych obiektów zwykle wiąże się z kopiowaniem pierwszego obiektu do obiektu tymczasowego, kopiowaniem drugiego obiektu do pierwszego obiektu i kopiowaniem obiektu tymczasowego do drugiego obiektu. W przypadku typu wbudowanego jest to bardzo szybkie, ale w przypadku dużych obiektów te trzy kopie mogą zająć dużo czasu. „Przypisanie przeniesienia” pozwala programiście zastąpić domyślne zachowanie kopiowania i zamiast tego zamienić odniesienia do obiektów, co oznacza, że ​​w ogóle nie ma kopiowania, a operacja zamiany jest znacznie szybsza. Przypisanie ruchu można wywołać, wywołując metodę std :: move ().

Domyślne zwrócenie obiektu z metody wymaga wykonania kopii obiektu lokalnego i powiązanych z nim danych w miejscu dostępnym dla osoby wywołującej (ponieważ obiekt lokalny nie jest dostępny dla osoby wywołującej i znika po zakończeniu metody). Gdy zwracany jest typ wbudowany, ta operacja jest bardzo szybka, ale jeśli zwracany jest duży obiekt, może to zająć dużo czasu. Konstruktor ruchu pozwala programiście nadpisać to domyślne zachowanie i zamiast tego „ponownie wykorzystać” dane sterty związane z obiektem lokalnym, wskazując obiekt zwracany do obiektu wywołującego, aby stertować dane powiązane z obiektem lokalnym. Dlatego kopiowanie nie jest wymagane.

W językach, które nie zezwalają na tworzenie obiektów lokalnych (tj. Obiektów na stosie), tego rodzaju problemy nie występują, ponieważ wszystkie obiekty są przydzielane na stercie i są zawsze dostępne przez odniesienie.


„Przypisanie przeniesienia” pozwala programiście zastąpić domyślne zachowanie kopiowania i zamiast tego zamienić odniesienia do obiektów, co oznacza, że ​​w ogóle nie ma kopiowania, a operacja zamiany jest znacznie szybsza. ” - twierdzenia te są dwuznaczne i mylące. Aby zamienić dwa przedmioty xi y, nie tylko można „referencje swap do obiektów” ; być może obiekty zawierają wskaźniki odwołujące się do innych danych i wskaźniki te można zamieniać, ale operatorzy ruchu nie są zobowiązani do zamiany czegokolwiek. Mogą wymazać dane z przeniesionego obiektu, zamiast zachowywać w nim dane docelowe.
Tony Delroy

Możesz pisać swap()bez semantyki ruchu. „Przypisanie ruchu można wywołać, wywołując metodę std :: move ().” - czasem konieczne jest użycie std::move()- chociaż to tak naprawdę niczego nie przenosi - po prostu informuje kompilator, że argument jest ruchomy, czasami std::forward<>()(z referencjami przekazywania), a innym razem kompilator wie, że można przenieść wartość.
Tony Delroy

-2

Oto odpowiedź z książki Bjarne Stroustrup „The C ++ Programming Language”. Jeśli nie chcesz oglądać wideo, możesz zobaczyć poniższy tekst:

Rozważ ten fragment kodu. Powrót z operatora + polega na skopiowaniu wyniku ze zmiennej lokalnej do miejsca, resw którym dzwoniący może uzyskać do niej dostęp.

Vector operator+(const Vector& a, const Vector& b)
{
    if (a.size()!=b.size())
        throw Vector_siz e_mismatch{};
    Vector res(a.size());
        for (int i=0; i!=a.size(); ++i)
            res[i]=a[i]+b[i];
    return res;
}

Tak naprawdę nie chcieliśmy kopii; chcieliśmy tylko uzyskać wynik z funkcji. Musimy więc przenieść Vector zamiast go skopiować. Możemy zdefiniować konstruktor ruchu w następujący sposób:

class Vector {
    // ...
    Vector(const Vector& a); // copy constructor
    Vector& operator=(const Vector& a); // copy assignment
    Vector(Vector&& a); // move constructor
    Vector& operator=(Vector&& a); // move assignment
};

Vector::Vector(Vector&& a)
    :elem{a.elem}, // "grab the elements" from a
    sz{a.sz}
{
    a.elem = nullptr; // now a has no elements
    a.sz = 0;
}

&& oznacza „odwołanie do wartości” i jest odniesieniem, z którym możemy powiązać wartość. „wartość” ma na celu uzupełnienie „wartości”, co z grubsza oznacza „coś, co może pojawić się po lewej stronie zadania”. Zatem wartość oznacza w przybliżeniu „wartość, której nie można przypisać”, na przykład liczbę całkowitą zwracaną przez wywołanie funkcji oraz reszmienną lokalną w operatorze + () dla wektorów.

Teraz oświadczenie return res;nie zostanie skopiowane!

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.