Czym jest semantyka ruchu?

Właśnie skończyłem słuchać audycji Software Engineering radio wywiad ze Scottem Meyersem dotyczący C++0x. Większość nowych funkcji ma dla mnie sens i jestem teraz podekscytowany C++0x, z wyjątkiem jednej. Nadal nie rozumiem semantyki ruchu ... Co to dokładnie jest?

Author: L. F., 2010-06-23

12 answers

Najłatwiej jest zrozumieć semantykę ruchu za pomocą przykładowego kodu. Zacznijmy od bardzo prostej klasy string, która przechowuje tylko wskaźnik do przydzielonego stercie bloku pamięci:

#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ż zdecydowaliśmy się zarządzać pamięcią sami, musimy przestrzegać zasady trzech . Odłożę pisanie operatora przypisania i na razie zaimplementuję tylko Destruktor i Konstruktor kopiujący: {]}

    ~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 kopiujący definiuje co to znaczy kopiować łańcuch obiektów. Parametr const string& that wiąże się ze wszystkimi wyrażeniami typu string, co pozwala na wykonywanie 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 pierwszej linii, w której kopiujemy x, ta głęboka kopia jest naprawdę potrzebna, ponieważ moglibyśmy chcieć sprawdzić x później i bylibyśmy bardzo zaskoczeni, gdyby x jakoś się zmieniło. Czy zauważyłeś, że właśnie powiedziałem x trzy razy (cztery razy, jeśli włączysz to zdanie) i miałem na myśli Dokładnie ten sam obiekt za każdym razem? Wyrażenia takie jak x nazywamy "lvalues".

Argumenty w liniach 2 i 3 nie są lvalues, ale rvalues, ponieważ leżące u podstaw obiekty łańcuchowe nie mają nazw, więc klient nie ma możliwości sprawdzenia ich ponownie w późniejszym czasie. wartości R oznaczają obiekty tymczasowe, które są niszczone przy następnym średniku(dokładniej: na końcu pełnego wyrażenia, które leksykalnie zawiera wartość R). Jest to ważne, ponieważ podczas inicjalizacja b i c, możemy zrobić co chcemy z łańcuchem źródłowym, a klient nie mógł odróżnić !

C++0x wprowadza nowy mechanizm o nazwie "rvalue reference", który między innymi, pozwala nam wykryć argumenty rvalue poprzez przeciążenie funkcji. Wystarczy, że napiszemy konstruktor z parametrem referencyjnym rvalue. Wewnątrz tego konstruktora możemy zrobić wszystko, co chcemy ze źródłem, o ile zostawimy je w some valid state:

    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }
Co my tu zrobiliśmy? Zamiast głęboko kopiować dane sterty, po prostu skopiowaliśmy wskaźnik, a następnie ustawiliśmy oryginalny wskaźnik na null(aby zapobiec zwolnieniu' delete [] 'z destruktora obiektu źródłowego 'just stolen data'). W efekcie "wykradliśmy" dane, które pierwotnie należały do łańcucha źródłowego. Ponownie, kluczowy wgląd jest taki, że pod żadnym pozorem klient nie mógł wykryć, że źródło zostało zmodyfikowane. Ponieważ nie naprawdę zrób kopię tutaj, nazywamy ten konstruktor "konstruktorem ruchu". Jego zadaniem jest przenoszenie zasobów z jednego obiektu do drugiego zamiast ich kopiowania.

Gratulacje, teraz rozumiesz podstawy semantyki ruchu! Kontynuujmy implementację operatora przyporządkowania. Jeśli nie znasz idiomu copy and swap , 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;
    }
};
To wszystko? "Gdzie jest rvalue referencje?"możesz zapytać. "Nie potrzebujemy go tutaj!"to moja odpowiedź:)

Zauważ, że przekazujemy parametr that przez wartość , więc that musi być zainicjalizowany tak jak każdy inny obiekt string. Jak dokładnie zostanie zainicjowana that? W dawnych czasach C++98, odpowiedź brzmiałaby "przez Konstruktor kopiujący". W C++0x kompilator wybiera pomiędzy konstruktorem kopiującym a konstruktorem move na podstawie tego, czy argument do operatora przypisania jest lvalue lub rvalue.

Więc jeśli powiesz a = b, Konstruktor kopiujący zainicjalizuje that (ponieważ wyrażenie b jest lvalue), a operator przypisania zamieni zawartość na świeżo utworzoną, głęboką kopię. Taka jest sama definicja idiomu copy and swap-zrób kopię, zamień zawartość z kopią, a następnie pozbądź się kopii, opuszczając zakres. Nic nowego.

Ale jeśli powiesz a = x + y, konstruktor ruchu zainicjalizuje that (ponieważ wyrażenie x + y jest wartością R), więc nie ma głębokiej kopii, tylko efektywny ruch. that jest nadal obiektem niezależnym od argumentu, ale jego budowa była trywialna, ponieważ dane sterty nie musiały być kopiowane, po prostu przenoszone. Nie było potrzeby jej kopiowania, ponieważ x + y jest wartością rvalue, i znowu, można przenosić się z obiektów łańcuchowych oznaczonych wartościami r.

Podsumowując, Konstruktor kopiujący tworzy głęboką kopię, ponieważ źródło musi pozostać nietknięte. Z drugiej strony konstruktor move może po prostu skopiować wskaźnik, a następnie ustawić wskaźnik w źródle NA null. W ten sposób można "anulować" obiekt źródłowy, ponieważ klient nie ma możliwości ponownego sprawdzenia obiektu.

Mam nadzieję, że ten przykład ma główny punkt. Jest o wiele więcej do referencji rvalue i przenieść semantyki, które celowo pominąłem, aby utrzymać to proste. Jeśli chcesz więcej szczegółów, zobacz moja dodatkowa odpowiedź.

 2615
Author: fredoverflow,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-07-22 10:46:00

Moją pierwszą odpowiedzią było niezwykle uproszczone wprowadzenie do poruszania semantyki, a wiele szczegółów zostało celowo pominiętych, aby było proste. Jest jednak o wiele więcej do poruszenia semantyki i pomyślałem, że nadszedł czas na drugą odpowiedź, aby wypełnić luki. Pierwsza odpowiedź jest już dość stara i nie czułem się dobrze, po prostu zastępując ją zupełnie innym tekstem. Myślę, że nadal służy jako pierwsze wprowadzenie. Ale jeśli chcesz kopać głębiej, Czytaj dalej:)

Stephan T. Lavavej poświęcił czas, aby dostarczyć cennych informacji zwrotnych. Dziękuję bardzo, Stephan!

Wprowadzenie

Semantyka Move pozwala obiektowi, pod pewnymi warunkami, przejąć własność zewnętrznych zasobów innego obiektu. Jest to ważne na dwa sposoby: [161]}

  1. Zamienianie drogich kopii w tanie posunięcia. Zobacz moją pierwszą odpowiedź na przykład. Zauważ, że jeśli obiekt nie zarządza co najmniej jednym zewnętrznym zasobem (bezpośrednio lub pośrednio poprzez jego obiekty członkowskie), semantyka move nie będzie oferować żadnych korzyści w stosunku do semantyki kopiowania. W tym przypadku kopiowanie 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. Implementacja bezpiecznych typów "move-only", czyli typów, dla których kopiowanie nie ma sensu, ale przenoszenie ma. Przykłady obejmują blokady, uchwyty plików i inteligentne wskaźniki z unikalną semantyką własności. Uwaga: Ta odpowiedź omawia std::auto_ptr, przestarzały szablon biblioteki standardowej C++98, który został zastąpiony przez std::unique_ptr W C++11. [30]}, a ze względu na" semantykę move", którą wyświetla, wydaje się dobrym punktem wyjścia do omówienia semantyki move W C++11. YMMV.

Co to jest ruch?

Biblioteka standardowa C++98 oferuje inteligentny wskaźnik z unikalną semantyką własności o nazwie std::auto_ptr<T>. W przypadku, gdy nie jesteś zaznajomiony z auto_ptr, jego celem jest zagwarantowanie, że dynamicznie przydzielany obiekt jest zawsze zwolniony, 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łą rzeczą w auto_ptr jest jego" kopiowanie " zachowania:

auto_ptr<Shape> a(new Triangle);

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

auto_ptr<Shape> b(a);

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

Zauważ, że inicjalizacja b Z a sprawia, że nie kopiuje trójkąta, ale zamiast tego przenosi własność trójkąta z a na b. Mówimy również " ajest przeniesiony do b" lub " trójkąt jest przesunięty z a na b". Może to zabrzmieć myląco, ponieważ sam trójkąt zawsze pozostaje w tym samym miejscu w pamięci.

Przeniesienie obiektu oznacza przeniesienie własności jakiegoś zasobu, którym zarządza, na inny obiekt.

Konstruktor kopiujący auto_ptr prawdopodobnie 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

Niebezpieczną rzeczą w auto_ptr jest to, że to, co składniowo wygląda jak kopia, jest w rzeczywistości ruchem. Próba wywołania funkcji członka na moved-from auto_ptr wywoła niezdefiniowane zachowanie, więc musisz być bardzo ostrożny, aby nie używać auto_ptr Po 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_ptr nie jest zawsze niebezpieczny. Funkcje fabryczne są doskonałym przykładem użycia 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

Zauważ, że oba przykłady mają ten sam wzór składniowy:

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

A jednak jeden z nich wywołuje nieokreślone zachowanie, podczas gdy drugi nie. Jaka jest więc różnica między wyrażeniami a i 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ć pewna głęboka różnica między wyrażeniem a, które oznacza zmienną auto_ptr, a wyrażeniem make_triangle(), które oznacza wywołanie funkcji, która zwraca auto_ptr przez wartość, tworząc w ten sposób nowy tymczasowy obiekt {34]} za każdym razem, gdy jest wywołana. a jest przykładem lvalue , natomiast make_triangle() jest przykładem rvalue .

Poruszanie się z lvalues, takich jak a jest niebezpieczne, ponieważ możemy później spróbować wywołać funkcję member poprzez a, wywołując niezdefiniowane zachowanie. Z drugiej strony, przejście z wartości R, takich jak make_triangle() jest całkowicie bezpieczne, ponieważ po wykonaniu przez Konstruktor kopiujący swojej pracy, nie możemy ponownie użyć tymczasowych wartości. Nie ma wyrażenia, które oznaczałoby wypowiedź tymczasową; jeśli po prostu napiszemy make_triangle() ponownie, otrzymamy inny tymczasowy. W rzeczywistości przesunięty-z tymczasowy jest już w następnej linii:

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

Zauważ, że litery l i r mają 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( jak tablice lub typy zdefiniowane przez Użytkownika bez operatora przypisania), i istnieją wartości R, które mogą (wszystkie wartości R typów klas z operator przypisania).

Rvalue typu klasy jest wyrażeniem, którego ocena tworzy obiekt tymczasowy. W normalnych okolicznościach żadne inne wyrażenie wewnątrz tego samego zakresu nie oznacza tego samego obiektu tymczasowego.

Rvalue references

Teraz rozumiemy, że przejście z wartości LV jest potencjalnie niebezpieczne, ale przejście z wartości R jest nieszkodliwe. Gdyby C++ miał obsługę języka do odróżniania argumentów lvalue od argumentów rvalue, moglibyśmy albo całkowicie zabronić przeprowadzki z lvalues, albo przynajmniej zrobić przeprowadzkę z lvalues explicit na miejscu wywołania, abyśmy nie poruszali się już przez przypadek.

Odpowiedzią C++11 na ten problem jest rvalue references. Odniesienie rvalue jest nowym rodzajem odniesienia, które wiąże się tylko z wartościami r, a składnia to X&&. Stare, dobre odniesienie X& jest teraz znane jako odniesienie lvalue. (Zauważ, że X&&jest nie odniesieniem do odniesienia; jest nie ma czegoś takiego w C++.)

Jeśli wrzucimy const do miksu, mamy już cztery różne rodzaje odniesień. Z jakimi rodzajami wyrażeń typu X 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żna zapomnieć o const X&&. Ograniczenie odczytu z wartości R nie jest zbyt użyteczne.

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

Implicit conversions

Rvalue reference went przez kilka wersji. Od wersji 2.1, odniesienie rvalue X&& wiąże się również ze wszystkimi kategoriami wartości innego typu Y, pod warunkiem, że istnieje Zamiana domyślna z Y na X. W tym przypadku tworzony jest tymczasowy Typ X, a odniesienie do rvalue jest powiązane z tym tymczasowym:

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

some_function("hello world");

W powyższym przykładzie, "hello world" jest lvalue typu const char[12]. Ponieważ istnieje niejawna konwersja z const char[12] przez const char* do std::string, tymczasowy Typ std::string jest stworzony i r jest związany z tym tymczasowym. Jest to jeden z przypadków, w których rozróżnienie między wartościami r (wyrażeniami) i czasownikami (obiektami) jest nieco rozmyte.

Konstruktory ruchu

Użytecznym przykładem funkcji z parametrem X&& jest konstruktor move 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>, co zajmuje zaleta referencji rvalue. Opracuję i omówię uproszczoną wersję unique_ptr. Po pierwsze, zamykamy surowy wskaźnik i przeciążamy operatory -> i *, więc nasza klasa czuje się jak wskaźnik: {161]}

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 robi dokładnie to, co zrobił Konstruktor kopiujący, ale może tylko być dostarczane z wartościami r:

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

Druga linia nie jest kompilowana, ponieważ a jest lvalue, ale parametr unique_ptr&& source może być przypisany tylko do wartości R. To jest dokładnie to, czego chcieliśmy; niebezpieczne ruchy nigdy nie powinny być ukryte. Trzecia linia kompiluje się dobrze, ponieważ make_triangle() jest wartością r. Konstruktor move przeniesie własność z tymczasowego na c. Znowu, to jest dokładnie to, czego chcieliśmy.

Konstruktor move przenosi własność zarządzanego zasób do bieżącego obiektu.

Operatory przypisania ruchu

Ostatni brakujący element to operator przypisania ruchu. Jego zadaniem jest uwolnienie starego zasobu i pozyskanie nowego zasobu z jego 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;
    }
};

Zauważ, jak ta implementacja operatora przypisania ruchu powiela logikę zarówno destruktora, jak i konstruktora ruchu. Znasz idiom copy-and-swap? Może być również stosowany do przenoszenia semantyki jako idiom move-and-swap:

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

Teraz, gdy source jest zmienną typu unique_ptr, zostanie ona zainicjowana przez konstruktor move, czyli argument zostanie przeniesiony do parametru. Argument jest nadal wymagany jako rvalue, ponieważ konstruktor move posiada parametr referencyjny rvalue. Gdy przepływ sterowania osiągnie klamrę zamykającąoperator=, source wychodzi poza zakres, zwalniając Stary zasób automatycznie.

Operator przypisania ruchu przenosi własność zarządzanego zasobu do bieżącego obiektu, zwalniając Stary zasób. Idiom move-and-swap upraszcza implementację.

Przeprowadzka z lvalues

Czasami, chcemy przenieść się z lvalues. Oznacza to, że czasami chcemy, aby kompilator traktował lvalue tak, jakby był rvalue, aby mógł wywołać konstruktor move, nawet jeśli może być potencjalnie niebezpieczny. W tym celu C++11 oferuje standardowy szablon funkcji biblioteki o nazwie std::move wewnątrz nagłówka <utility>. Ta nazwa jest trochę niefortunna, ponieważ std::move po prostu rzuca lvalue na rvalue; nie przenosi niczego samodzielnie. To tylko umożliwia poruszanie się. Może powinna być nazwana std::cast_to_rvalue lub std::enable_move, ale utknęliśmy z tą nazwą.

Oto jak przejdziesz od lvalue:

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 a nie posiada już trójkąta. W porządku, bo przez jawnie pisząc std::move(a), jasno wyraziliśmy nasze intencje: "drogi konstruktorze, rób co chcesz z {37]}, aby zainicjować {95]}; Nie dbam już o {37]}. Zapraszam do współpracy a."

std::move(some_lvalue) rzuca lvalue do rvalue, umożliwiając w ten sposób kolejny ruch.

Xvalues

Zauważ, że chociaż std::move(a) jest wartością r, jej ocena nie tworzy obiektu tymczasowego. Ta zagadka zmusił Komitet do wprowadzenia trzeciej kategorii wartości. Coś, co może być powiązane z odniesieniem rvalue, nawet jeśli nie jest to wartość rvalue w tradycyjnym znaczeniu, nazywa się xvalue (wartość wygasająca). Tradycyjne wartości R zostały przemianowane na prvalues (czyste wartości R).

Zarówno wartości pr, jak i X są wartościami r. Xvalues i lvalues są zarówno glvalues (uogólnione lvalues). Relacje są łatwiejsze do uchwycenia z schemat:

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

Zauważ, że tylko wartości X są naprawdę nowe; reszta wynika tylko ze zmiany nazwy i grupowania.

Wartości R++98 są znane jako wartości pr W C++11. Mentalnie Zamień wszystkie wystąpienia "rvalue "w poprzednich akapitach na"prvalue".

Wyprowadzanie z funkcji

Do tej pory widzieliśmy ruch na zmienne lokalne i na parametry funkcji. Ale poruszanie się jest również możliwe w przeciwnym kierunku. Jeśli funkcja zwraca według wartości, jakiś obiekt w miejscu wywołania (prawdopodobnie zmienna lokalna lub tymczasowa, ale może być dowolnym obiektem) jest inicjowany wyrażeniem po instrukcji return jako argument konstruktora move:

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 zaskakujące, automatyczne obiekty (zmienne lokalne, które nie są zadeklarowane jako static) mogą być również domyślnie przeniesione z funkcji:

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

Dlaczego konstruktor ruchu akceptuje lvalue result jako argument? Zakres result wkrótce się skończy i zostanie zniszczony podczas rozwijania stosu. Nikt nie mógł później narzekać, że result jakoś się zmienił; kiedy kontrola przepływu powraca do rozmówcy, result już nie istnieje! Z tego powodu C++11 posiada specjalną regułę, która pozwala zwracać automatyczne obiekty z funkcji bez konieczności pisania std::move. W rzeczywistości, nie należy nigdy używać std::move do przenoszenia automatycznych obiektów poza funkcje, ponieważ blokuje to " nazwaną wartość zwracaną optymalizacja " (NRVO).

Nigdy nie używaj std::move do przenoszenia automatycznych obiektów poza funkcje.

Zauważ, że w obu funkcjach fabrycznych typ zwracany jest wartością, a nie referencją rvalue. Odwołania do Rvalue są nadal odwołaniami i jak zawsze, nigdy nie powinieneś zwracać odwołań do automatycznego obiektu; wywołujący skończyłby z zwisającym odwołaniem, jeśli oszukałeś kompilator, aby zaakceptował Twój kod, Jak to:

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

Never return automatyczne obiekty przy pomocy referencji rvalue. Przenoszenie jest wykonywane wyłącznie przez konstruktor move, a nie przez std::move, a nie tylko przez powiązanie wartości R z referencją rvalue.

Przejście do członków

Prędzej czy później napiszesz kod w ten sposób:

class Foo
{
    unique_ptr<Shape> member;

public:

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

W zasadzie kompilator będzie narzekał, że parameter jest lvalue. Jeśli spojrzysz na jego typ, zobaczysz odniesienie rvalue, ale odniesienie rvalue oznacza po prostu " odniesienie, które jest związane z rvalue"; to nie oznacza, że samo odniesienie jest rvalue! W rzeczy samej, parameter jest zwykłą zmienną o nazwie. Możesz używać parameter tak często, jak chcesz wewnątrz konstruktora i zawsze oznacza ten sam obiekt. Dorozumiane odejście od niego byłoby niebezpieczne, stąd język tego zabrania.

Nazwane odniesienie rvalue jest lvalue, podobnie jak każda inna zmienna.

Rozwiązaniem jest ręczne włączenie przenieść:

class Foo
{
    unique_ptr<Shape> member;

public:

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

Można argumentować, że parameter nie jest już używany po inicjalizacji member. Dlaczego nie ma specjalnej reguły do cichego wstawiania std::move tak jak przy wartościach zwracanych? Prawdopodobnie dlatego, że byłoby to zbyt dużym obciążeniem dla implementatorów kompilatora. Na przykład, co jeśli ciało konstruktora znajdowało się w innej jednostce tłumaczeniowej? Natomiast reguła zwracanej wartości musi po prostu sprawdzić tabele symboli, aby określić, czy identyfikator po słowie kluczowym return oznacza obiekt automatyczny.

Można również przekazać parameter przez wartość. W przypadku typów poruszających się tylko, takich jak unique_ptr, wydaje się, że nie ma jeszcze ustalonego idiomu. Osobiście wolę przekazać wartość, ponieważ powoduje mniej bałaganu w interfejsie.

Specjalne funkcje członków

C++98 domyślnie deklaruje trzy specjalne funkcje Członkowskie 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 Rvalue były dostępne w kilku wersjach. Od wersji 3.0, C++11 deklaruje na żądanie dwie dodatkowe Funkcje specjalne: konstruktor move i operator move assignment. Zauważ, ż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 funkcje specjalne są deklarowane domyślnie tylko wtedy, gdy żadna z funkcji specjalnych nie jest deklarowana ręcznie. Ponadto, jeśli zadeklaruj swój własny konstruktor move lub operator przypisania move, ani Konstruktor kopiujący, ani operator przypisania kopiowania nie będą deklarowane w sposób niejawny.

Co te zasady oznaczają w praktyce?

Jeśli piszesz klasę bez niezarządzanych zasobów, nie ma potrzeby deklarowania żadnej z pięciu specjalnych funkcji Członkowskich samodzielnie, a otrzymasz poprawną semantykę kopiowania i przenosisz semantykę za darmo. W przeciwnym razie będziesz musiał wdrożyć specjalne funkcje członka siebie. Oczywiście, jeśli twoja klasa nie korzysta z semantyki move, nie ma potrzeby implementowania specjalnych operacji move.

Operator przypisania kopiowania i operator przeniesienia mogą być połączone 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 Członkowskich do wdrożenia spada z pięciu do czterech. Jest kompromis między wyjątkiem-Bezpieczeństwo i wydajność tutaj, ale jestem nie jest ekspertem w tej sprawie.

Przekazywanie referencji (poprzednio znane jako uniwersalne referencje )

Rozważ następujący szablon funkcji:

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

Można oczekiwać, że T&& będzie wiązać tylko wartości r, ponieważ na pierwszy rzut oka wygląda to jak odniesienie do wartości r. Jak się jednak okazuje, T&& wiąże się również z lvalues:

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 argument jest wartością r typu X, T jest wydedukować X, stąd T&& oznacza X&&. Tego każdy by się spodziewał. Jeśli jednak argument jest lvalue typu X, ze względu na specjalną regułę, T jest wydedukowane jako X&, stąd T&& oznaczałoby coś w rodzaju X& &&. Ale ponieważ C++ nadal nie ma pojęcia o odwołaniach do referencji, Typ X& && jest zwinięty do X&. Na początku może to wydawać się mylące i bezużyteczne, ale zwijanie referencji jest niezbędne dla perfect forwarding (co nie będzie omawiane tutaj).

T & & nie jest referencją rvalue, ale referencją przekierowującą. Wiąże się również z lvalue, w którym to przypadku T i T&& są zarówno odniesieniami lvalue.

Jeśli chcesz ograniczyć szablon funkcji do wartości r, 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&&);

Implementacja move

Teraz, gdy rozumiesz zwijanie referencji, oto jak std::move jest zaimplementowane:

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

Jak widzisz, move akceptuje każdy rodzaj parametru dzięki referencji przekierowania T&& i zwraca referencję rvalue. Wywołanie meta-funkcji std::remove_reference<T>::type jest konieczne, ponieważ w przeciwnym razie, dla lvalues typu X, zwracanym typem będzie X& &&, który spadnie do X&. Ponieważ t jest zawsze lvalue (pamiętaj, że nazwane odniesienie rvalue jest lvalue), ale chcemy powiązać {[155] } z odniesieniem rvalue, musimy jawnie oddać t do właściwego typu zwrotnego. Wywołanie funkcji, która zwraca Referencja rvalue sama w sobie jest wartością xvalue. Teraz już wiesz skąd się biorą xvalues;)

Wywołanie funkcji, która zwraca referencję rvalue, np. std::move, jest wartością xvalue.

Zauważ, że zwracanie przez odniesienie rvalue jest dobre w tym przykładzie, ponieważ t nie oznacza automatycznego obiektu, ale obiekt, który został przekazany przez wywołującego.

 1107
Author: fredoverflow,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-04-28 11:40:57

Semantyka ruchu opiera się na rvalue.
Rvalue jest obiektem tymczasowym, który zostanie zniszczony na końcu wyrażenia. W bieżącym C++ wartości R wiążą się tylko z referencjami const. C++1x zezwala na odwołania nie - const rvalue, pisane T&&, które są odniesieniami do obiektów rvalue.
Ponieważ wartość R umrze na końcu wyrażenia, możesz wykraść jego dane. Zamiast skopiować do innego obiektu, ty przenosisz jego dane do niego.

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 kompilatorami wynik f() jest kopiowane do x za pomocą konstruktora kopiującego X. Jeśli twój kompilator obsługuje semantykę move i X ma konstruktor move, to jest on wywoływany. Ponieważ jego argument rhs jest rvalue, wiemy, że nie jest już potrzebny i możemy ukraść jego wartość.
Więc wartość jest przeniesiony z nienazwanych tymczasowych zwracane z f() do x (podczas gdy DANE x, zainicjalizowane na pusty X, są przenoszone do tymczasowego, który zostanie zniszczony po przypisaniu).

 79
Author: sbi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-24 15:23:54

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

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

Kiedy piszesz kod w ten sposób:

Matrix r = multiply(a, b);

Wtedy zwykły kompilator C++ utworzy tymczasowy obiekt dla wyniku multiply(), wywoła Konstruktor kopiujący, aby zainicjował r, a następnie zniszczy tymczasową wartość zwracaną. Semantyka Move W C++0x pozwala na wywołanie" konstruktora move " w celu zainicjalizowania r przez skopiowanie jego zawartości, a następnie odrzucenie tymczasowej wartości bez konieczności destrukcji to.

Jest to szczególnie ważne, jeśli (Jak przykład Matrix powyżej), kopiowany obiekt przydziela dodatkową pamięć na stercie, aby przechowywać jej wewnętrzną reprezentację. Konstruktor kopiujący musiałby albo wykonać pełną kopię reprezentacji wewnętrznej, albo użyć semantyki zliczania referencji i semantyki kopiowania przy zapisie. Konstruktor move zostawiłby samą pamięć sterty i po prostu skopiowałby wskaźnik wewnątrz obiektu Matrix.

 61
Author: Greg Hewgill,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-06-23 22:53:31

Jeśli naprawdę jesteś zainteresowany dobrym, dogłębnym wyjaśnieniem semantyki move, Gorąco polecam przeczytanie oryginalnego artykułu na ich temat, " propozycja dodania obsługi semantyki Move do języka C++."

Jest bardzo przystępny i łatwy do odczytania i stanowi doskonały przykład korzyści, które oferują. Istnieją inne nowsze i aktualne artykuły na temat semantyki move dostępne na stronie WG21 , ale ta jest prawdopodobnie najprostsza ponieważ podchodzi do rzeczy z widoku najwyższego poziomu i nie wchodzi zbytnio w szczegóły języka.

 30
Author: James McNellis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-06-23 23:32:29

Semantyka Move {[8] } polega naprzekazywaniu zasobów zamiast ich kopiowania , gdy nikt już nie potrzebuje wartości źródłowej.

W C++03, obiekty są często kopiowane, tylko do zniszczenia lub przypisane-over zanim jakikolwiek kod ponownie użyje wartości. Na przykład, gdy zwracasz wartość z funkcji-o ile nie włączy się RVO-zwracana wartość jest kopiowana do ramki stosu wywołującego, a następnie wychodzi poza zakres i jest niszczona. To tylko jeden z wielu przykładów: zobacz pass-by-value, gdy obiekt źródłowy jest tymczasowym, algorytmem takim jak sort, który po prostu zmienia kolejność elementów, realokuje w vector, gdy jego capacity() jest przekroczony, itd.

Kiedy takie pary Kopiuj/niszcz są drogie, zazwyczaj dzieje się tak dlatego, że obiekt posiada jakiś ciężki surowiec. Na przykład vector<string> może posiadać dynamicznie przydzielany blok pamięci zawierający tablicę obiektów string, każdy z własną pamięcią dynamiczną. Kopiowanie takiego obiektu jest kosztowne: trzeba przydzielić nową pamięć dla każdego dynamicznie przydzielane bloki w źródle i kopiować wszystkie wartości w poprzek. następnie musisz usunąć wszystkie wspomnienia, które właśnie skopiowałeś. Jednak przeniesienie dużego vector<string> oznacza po prostu skopiowanie kilku wskaźników (które odnoszą się do dynamicznego bloku pamięci) do miejsca docelowego i zerowanie ich w źródle.

 28
Author: Dave Abrahams,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-04-08 19:47:56

W prostych (praktycznych) terminach:

Kopiowanie obiektu oznacza kopiowanie jego" statycznych " elementów i wywołanie operatora new dla jego dynamicznych obiektów. Prawda?

class A
{
   int i, *p;

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

Jednakże, aby przenieść obiekt (powtarzam, z praktycznego punktu widzenia) oznacza tylko kopiowanie wskaźników dynamicznych obiektów, a nie tworzenie nowych.

Ale czy to nie jest niebezpieczne? Oczywiście można dwukrotnie zniszczyć dynamiczny obiekt(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 poruszę obiekt, obiekt źródłowy stanie się bezużyteczny, prawda? Oczywiście, ale w pewnych sytuacjach jest to bardzo przydatne. Najbardziej oczywistym jest wywołanie funkcji z anonimowym obiektem (temporal, rvalue object, ..., można go nazywać różnymi nazwami):

void heavyFunction(HeavyType());

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

Prowadzi to do koncepcji odniesienia "rvalue". Istnieją one w C++11 tylko po to, aby wykryć, czy otrzymany obiekt jest anonimowy czy nie. Myślę, że już wiesz, że" lvalue " jest przypisywalną jednostką( lewą częścią operatora =), więc potrzebujesz nazwanego odniesienia do obiektu, aby móc działać jako lvalue. Wartość R jest dokładnie odwrotna, obiekt bez nazwy referencje. Z tego powodu anonymous object i rvalue 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 tym przypadku, gdy obiekt typu A powinien być "skopiowany", kompilator tworzy referencję lvalue lub rvalue zgodnie z tym, czy przekazany obiekt jest nazwany, czy nie. Jeśli nie, wywoływany jest konstruktor move-constructor i wiesz, że obiekt jest czasowy i możesz przenosić jego dynamiczne obiekty zamiast je kopiować, oszczędzając miejsce i pamięć.

Ważne jest, aby pamiętać, że " statyczne" obiekty są zawsze kopiowane. Nie ma możliwości "przeniesienia" obiektu statycznego(obiektu w stosie, a nie na stosie). Tak więc rozróżnienie "Przenieś" / "Kopiuj", gdy obiekt nie ma dynamicznych elementów (bezpośrednio lub pośrednio) jest nieistotne.

Jeśli obiekt jest złożony, a Destruktor ma inne efekty uboczne, 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 */ }
};

Więc Twój kod jest krótszy (ty nie trzeba robić nullptr przypisania dla każdego dynamicznego członka) i bardziej ogólne.

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

I co to jest idealne przekierowanie ? Ważne jest, aby wiedzieć, że " rvalue reference "jest odniesieniem do nazwanego obiektu w"obszarze wywołującego". Ale w rzeczywistym zakresie, odniesienie rvalue jest nazwą obiektu, więc działa jak obiekt nazwany. Jeśli przekazujesz odniesienie rvalue do innej funkcji, przekazujesz nazwany obiekt, więc obiekt nie jest odbierany tak jak obiekt czasowy.

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

Obiekt a zostanie skopiowany do rzeczywistego parametru other_function. Jeśli chcesz obiekt a kontynuacja jest traktowana jako obiekt tymczasowy, należy użyć funkcji std::move:

other_function(std::move(a));

W tej linii std::move rzuci a do wartości R, a other_function otrzyma obiekt jako obiekt nienazwany. Oczywiście, jeśli other_function nie ma specyficznego przeciążenia do pracy z obiektami nienazwanymi, rozróżnienie to nie jest ważne.

Czy to idealne przekierowanie? Nie, ale jesteśmy bardzo blisko. Perfect forwarding jest przydatny tylko do pracy z szablonami, w celu powiedzenia: jeśli muszę przejść obiekt do innej funkcji, potrzebuję, że jeśli otrzymam obiekt o nazwie, obiekt zostanie przekazany jako obiekt o nazwie, a jeśli nie, chcę przekazać go jak obiekt nienazwany:
template<typename T>
void some_function(T&& a)
{
   other_function(std::forward<T>(a));
}

To podpis prototypowej funkcji wykorzystującej doskonałe przekazywanie, zaimplementowanej w C++11 za pomocą std::forward. Ta funkcja wykorzystuje niektóre zasady tworzenia szablonu:

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

Więc jeśli T jest odniesieniem doA (T = a&), a also ( A& && => A&). Jeśli T jest odniesieniem do wartości RA, a również (A&& & & = > A&&). W obu przypadkach a jest obiektem nazwanym w rzeczywistym zakresie, ale T zawiera informacje swojego "typu odniesienia" z punktu widzenia zakresu wywołującego. Ta informacja (T) jest przekazywana jako parametr szablonu do forward, A " a " jest przenoszona lub nie zgodnie z typem T.

 25
Author: Peregring-lk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-02-20 23:09:00

To jak semantyka kopiowania, ale zamiast powielać wszystkie dane, można ukraść dane z obiektu, z którego jest "przeniesiony".

 21
Author: Terry Mahaffey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-09-27 07:48:25

Wiesz, co oznacza semantyka kopii? oznacza to, że masz typy, które można kopiować, dla typów zdefiniowanych przez użytkownika definiujesz to albo kup jawnie pisząc Konstruktor kopiujący i operator przypisania lub kompilator generuje je domyślnie. To zrobi kopię.

Semantyka Move jest w zasadzie zdefiniowanym przez użytkownika typem z konstruktorem, który pobiera referencję o wartości r (nowy typ odniesienia za pomocą & & (tak dwa ampersandy)), która nie jest const, nazywa się to konstruktorem move, tak samo dla operatora przydziału. Co robi konstruktor move, zamiast kopiować pamięć z argumentu źródłowego "przenosi" pamięć ze źródła do miejsca docelowego.

Kiedy chcesz to zrobić? dobrze std::vector jest przykładem, powiedzmy, że stworzyłeś tymczasowy std::vector i zwracasz go z funkcji powiedzmy:
std::vector<foo> get_foos();

Będziesz miał narzut z konstruktora kopiującego, gdy funkcja powróci, jeśli (i będzie to w C++0x) std::vector ma konstruktor ruchu zamiast kopiowanie go może po prostu ustawić jego wskaźniki i "przenieść" dynamicznie przydzieloną pamięć do nowej instancji. To coś w rodzaju semantyki przeniesienia własności z std:: auto_ptr.

 13
Author: snk_kid,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-06-23 23:03:36

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

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

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

Powyższa funkcja wykorzystuje wywołanie przez wartość , co oznacza, że gdy ta funkcja jest wywoływana, obiekt musi być skonstruowany , aby mógł być używany przez funkcję.
Ponieważ funkcja Zwraca wartość , kolejnym nowym obiektem jest skonstruowane dla wartości zwracanej:

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

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

Gdy nowy obiekt zostanie wytworzony z zwracanej wartości, Konstruktor kopiujący jest wywoływany do skopiuj zawartość tymczasowego obiektu do nowego obiektu b. po zakończeniu funkcji tymczasowy obiekt użyty w funkcji wychodzi poza zakres i jest niszczony.


Teraz, zastanówmy się, co robi Konstruktor kopiujący.

Musi najpierw zainicjować obiekt, a następnie skopiować wszystkie istotne dane ze starego obiektu do nowego.
W zależności od klasy, może jest to kontener z bardzo dużą ilością danych, wtedy może reprezentować wiele czasu i uż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);
}

Z semantyką move jest teraz możliwe, aby większość tej pracy była mniej nieprzyjemna, po prostu przenoszenie danych, a nie kopiuję.

// 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 polega na ponownym powiązaniu danych z nowym obiektem. I Żadna Kopia nie ma miejsca w ogóle.

Osiąga się to za pomocą odniesienia rvalue.
Odsyłacz rvalue działa podobnie jak odsyłacz lvalue z jedną ważną różnicą:
nie można przenieść referencji rvalue, a lvalue.

Z cppreference.com :

Aby umożliwić silną gwarancję WYJĄTKÓW, konstruktory ruchu zdefiniowane przez użytkownika nie powinny wyrzucać WYJĄTKÓW. W rzeczywistości standardowe kontenery zazwyczaj opierają się na std::move_if_noexcept, aby wybrać pomiędzy move a copy, gdy elementy kontenera muszą zostać przeniesione. Jeśli podano konstruktory copy I move, rozdzielczość przeciążenia wybiera konstruktor move, jeśli argument jest wartością r (albo wartością PR, taką jak bezimienna wartość tymczasowa lub wartością x, taką jak wynik STD:: move) i wybiera Konstruktor kopiujący, jeśli argument jest wartością lvalue (o nazwie obiekt lub funkcja / operator zwracający referencję lvalue). Jeśli podany jest tylko Konstruktor kopiujący, wybierają go Wszystkie kategorie argumentów (tak długo, jak wymaga referencji do const, ponieważ wartości r mogą wiązać się z referencjami const), co sprawia, że kopiowanie zapasowe do przenoszenia, gdy przenoszenie jest niedostępne. W wielu sytuacjach konstruktory ruchu są zoptymalizowane, nawet jeśli spowodowałyby zauważalne efekty uboczne, patrz copy elision. Konstruktor jest nazywany "konstruktorem ruchu", gdy przyjmuje wartość R odniesienie jako parametr. Nie jest zobowiązany do przenoszenia czegokolwiek, klasa nie musi mieć zasobu do przeniesienia i 'konstruktor move' może nie być w stanie przenieść zasobu, jak w dopuszczalnym (ale może nie rozsądnym) przypadku, gdy parametr jest referencją const rvalue (const T&&).

 7
Author: Andreas DM,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-02-25 00:12:28

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

Semantyka Move została stworzona, aby uniknąć niepotrzebnego kopiowania dużych obiektów. Bjarne Stroustrup w swojej książce "The C++ Programming Language" używa dwóch przykładów, w których domyślnie występuje niepotrzebne kopiowanie: jeden, Zamiana dwóch dużych obiektów i dwa, zwracanie dużych obiektów z metody.

Zamiana dwóch dużych obiektów zwykle polega na skopiowaniu pierwszego obiektu na obiekt tymczasowy, skopiowaniu drugiego obiekt do pierwszego obiektu i skopiowanie obiektu tymczasowego do drugiego obiektu. Dla typu wbudowanego jest to bardzo szybkie, ale dla dużych obiektów te trzy kopie mogą zająć dużą ilość czasu. "Przeniesienie" pozwala programiście nadpisać domyślne zachowanie kopiowania i zamiast tego zamienić odniesienia do obiektów, co oznacza, że nie ma kopiowania w ogóle, a operacja wymiany jest znacznie szybsza. Przypisanie move może być wywołane przez wywołanie STD:: move() metoda.

Zwracanie obiektu z metody domyślnie polega na wykonaniu kopii obiektu lokalnego i powiązanych z nim danych w miejscu dostępnym dla wywołującego (ponieważ obiekt lokalny nie jest dostępny dla wywołującego i znika po zakończeniu metody). Gdy zwracany jest typ wbudowany, operacja ta jest bardzo szybka, ale jeśli zwracany jest duży obiekt, może to zająć dużo czasu. Konstruktor move pozwala programiście nadpisać to domyślne zachowanie zamiast tego "wykorzystaj ponownie" DANE sterty związane z obiektem lokalnym, wskazując zwracany obiekt do wywołującego na dane sterty związane z obiektem lokalnym. W związku z tym nie jest wymagane kopiowanie.

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

 7
Author: Chris B,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-11-22 05:23:14

Oto ODPOWIEDŹ z książki "język programowania C++" Bjarne Stroustrupa. Jeśli nie chcesz oglądać filmu, możesz zobaczyć poniższy tekst:

Rozważ ten fragment. Zwracanie z operatora + polega na skopiowaniu wyniku ze zmiennej lokalnej res i w miejsce, gdzie wywołujący może uzyskać do niego 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. Więc musimy przenieść Wektor zamiast go skopiować. Możemy zdefiniuj 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 "rvalue reference" i jest odniesieniem, do którego możemy powiązać rvalue. "rvalue "'ma na celu uzupełnienie" lvalue", co w przybliżeniu oznacza " coś, co może pojawić się po lewej stronie przypisania."Zatem rvalue oznacza mniej więcej" wartość, której nie można przypisać", taką jak liczba całkowita zwracana przez wywołanie funkcji i zmienna lokalna res w operatorze + () dla wektorów.

Teraz oświadczenie return res; nie będzie kopiowane!

 -2
Author: Rob Pei,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-04-25 06:14:56