C++11 rvalues and move semantyka confusion (return statement)

Próbuję zrozumieć referencje rvalue i przenieść semantykę C++11.

Jaka jest różnica między tymi przykładami, a który z nich nie zrobi żadnej kopii wektorowej?

Pierwszy przykład

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

Drugi przykład

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

Trzeci przykład

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();
Author: artm, 2011-02-13

5 answers

Pierwszy przykład

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

Pierwszy przykład zwraca tymczasowy, który jest przechwytywany przez rval_ref. Ten czasowy będzie miał swoje życie przedłużone poza definicję rval_ref i możesz go używać tak, jakbyś złapał go przez wartość. Jest to bardzo podobne do następujących:

const std::vector<int>& rval_ref = return_vector();

Z wyjątkiem tego, że w moim przepisaniu oczywiście nie możesz użyć rval_ref w sposób niekonst.

Drugi przykład

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

W drugim przykładzie został utworzony błąd czasu wykonania. rval_ref Teraz posiada odniesienie do destrukcji tmp wewnątrz funkcji. Przy odrobinie szczęścia ten kod natychmiast by się rozbił.

Trzeci przykład

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

Twój trzeci przykład jest mniej więcej odpowiednikiem Twojego pierwszego. std::move on tmp jest zbędny i w rzeczywistości może być pesymizacją wydajności, ponieważ hamuje optymalizację wartości zwrotnej.

Najlepszym sposobem na zakodowanie tego, co robisz, jest:

Najlepsza praktyka

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

Czyli tak jak w C++03. tmp jest implicite traktowana jako wartość R w deklaracji return. Zostanie zwrócony poprzez optymalizację return-value (bez kopiowania, bez ruchu), lub jeśli kompilator zdecyduje, że nie może wykonać RVO, wtedy użyje konstruktora ruchu Vectora do wykonania tego zwrotu. Tylko jeśli RVO nie jest wykonywane i jeśli zwracany typ nie miał konstruktora move, Konstruktor kopiujący zostanie użyty do zwrotu.

 484
Author: Howard Hinnant,
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
2018-09-13 14:09:43

Żaden z nich nie będzie kopiował, ale drugi będzie odnosił się do zniszczonego wektora. Nazwane odniesienia rvalue prawie nigdy nie istnieją w zwykłym kodzie. Piszesz tak jak napisałbyś kopię w C++03.

std::vector<int> return_vector()
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

Z tym, że wektor jest przesunięty. użytkownik klasy nie zajmuje się referencjami rvalue w zdecydowanej większości przypadków.

 40
Author: Puppy,
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
2014-07-21 16:06:43

Prosta odpowiedź jest taka, że powinieneś pisać kod dla referencji rvalue tak, jak byś robił zwykłe referencje, i powinieneś traktować je tak samo mentalnie przez 99% czasu. Obejmuje to wszystkie stare zasady zwracania referencji (tzn. nigdy nie zwracaj referencji do zmiennej lokalnej).

O ile nie piszesz klasy kontenera szablonu, która musi skorzystać ze STD:: forward I być w stanie napisać ogólną funkcję, która przyjmuje odniesienia lvalue lub rvalue, jest to bardziej lub mniej prawdziwe.

Jedną z dużych zalet konstruktora move i przypisania move jest to, że jeśli je zdefiniujesz, kompilator może ich użyć w przypadkach, gdy RVO (return value optimization) i nrvo (named return value optimization) nie zostaną wywołane. Jest to dość duże w przypadku zwracania drogich obiektów, takich jak kontenery i ciągi znaków według wartości, efektywnie z metod.

Teraz, gdy sprawy stają się interesujące z referencjami rvalue, jest to, że możesz również użyć ich jako argumentów do normalnego funkcje. Pozwala to na zapisanie kontenerów, które mają przeciążenia zarówno dla referencji const (const foo& other), jak i dla referencji rvalue (foo&& other). Nawet jeśli argument jest zbyt nieporęczny, aby przekazać go zwykłym wywołaniem konstruktora, nadal można to zrobić:

std::vector vec;
for(int x=0; x<10; ++x)
{
    // automatically uses rvalue reference constructor if available
    // because MyCheapType is an unamed temporary variable
    vec.push_back(MyCheapType(0.f));
}


std::vector vec;
for(int x=0; x<10; ++x)
{
    MyExpensiveType temp(1.0, 3.0);
    temp.initSomeOtherFields(malloc(5000));

    // old way, passed via const reference, expensive copy
    vec.push_back(temp);

    // new way, passed via rvalue reference, cheap move
    // just don't use temp again,  not difficult in a loop like this though . . .
    vec.push_back(std::move(temp));
}

Kontenery STL zostały zaktualizowane tak, aby miały przeciążenia dla prawie wszystkiego (klawisz skrótu i wartości, wstawianie wektorów itp.).

Można również używać ich do normalnych funkcji, a jeśli podaj tylko argument odniesienia rvalue, możesz zmusić wywołującego do utworzenia obiektu i pozwolić funkcji wykonać ruch. Jest to bardziej przykład niż naprawdę dobre użycie, ale w mojej bibliotece renderowania przypisałem ciąg znaków do wszystkich załadowanych zasobów, dzięki czemu łatwiej jest zobaczyć, co każdy obiekt reprezentuje w debuggerze. Interfejs jest podobny do tego:

TextureHandle CreateTexture(int width, int height, ETextureFormat fmt, string&& friendlyName)
{
    std::unique_ptr<TextureObject> tex = D3DCreateTexture(width, height, fmt);
    tex->friendlyName = std::move(friendlyName);
    return tex;
}

Jest to forma "nieszczelnej abstrakcji", ale pozwala mi wykorzystać fakt, że musiałem stworzyć ciąg już przez większość czasu i unikaj robienia kolejnego jej kopiowania. To nie jest dokładnie kod wysokiej wydajności, ale jest dobrym przykładem możliwości, jak ludzie dostać powiesić tej funkcji. Kod ten wymaga, aby zmienna była tymczasowa dla wywołania lub wywołana przez STD:: move:

// move from temporary
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard"));

Lub

// explicit move (not going to use the variable 'str' after the create call)
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, std::move(str));

Lub

// explicitly make a copy and pass the temporary of the copy down
// since we need to use str again for some reason
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string(str));

Ale to się nie skompiluje!

string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);
 14
Author: Zoner,
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
2018-06-27 21:14:11

Żaden z nich nie zrobi dodatkowego kopiowania. Nawet jeśli RVO nie jest używany, nowy standard mówi, że konstrukcja move jest preferowana do kopiowania podczas robienia zwrotów.

Wierzę, że twój drugi przykład powoduje nieokreślone zachowanie, ponieważ zwracasz odniesienie do zmiennej lokalnej.

 2
Author: Crazy Eddie,
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
2011-02-13 20:40:33

Nie ODPOWIEDŹ per se , ale wytyczne. Przez większość czasu nie ma większego sensu deklarowanie lokalnej zmiennej T&& (tak jak to zrobiłeś z std::vector<int>&& rval_ref). Nadal będziesz musiał std::move() używać ich w metodach typu foo(T&&). Jest też problem, o którym już wspomniano, że gdy spróbujesz zwrócić taką rval_ref z funkcji, otrzymasz standardowe odniesienie-do-destroyed-temporary-fiasco.

Przez większość czasu używałem następującego wzoru:

// Declarations
A a(B&&, C&&);
B b();
C c();

auto ret = a(b(), c());

You don ' t hold any refs do zwracanych obiektów tymczasowych, dzięki czemu unikasz (niedoświadczonego) błędu programisty, który chce użyć przeniesionego obiektu.

auto bRet = b();
auto cRet = c();
auto aRet = a(std::move(b), std::move(c));

// Either these just fail (assert/exception), or you won't get 
// your expected results due to their clean state.
bRet.foo();
cRet.bar();

Oczywiście istnieją (choć raczej rzadkie) przypadki, w których funkcja naprawdę zwraca T&&, który jest odniesieniem do nie-tymczasowego obiektu, który można przenieść do swojego obiektu.

W odniesieniu do RVO: mechanizmy te generalnie działają i kompilator może ładnie uniknąć kopiowania, ale w przypadkach, gdy ścieżka powrotna nie jest oczywista (wyjątki, if warunki określające nazwany obiekt, który zwrócisz, i prawdopodobnie kilka innych) rref są twoimi wybawcami (nawet jeśli potencjalnie droższe).

 2
Author: Red XIII,
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-02-26 07:46:40