Wartości C ++ 11 i zamieszanie semantyki ruchu (instrukcja return)

435

Próbuję zrozumieć odwołania do wartości i przenieść semantykę C ++ 11.

Jaka jest różnica między tymi przykładami i który z nich nie wykona 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();
Tarantula
źródło
50
Nigdy nie zwracaj zmiennych lokalnych przez odniesienie. Odwołanie do wartości jest nadal odniesieniem.
fredoverflow
63
Było to oczywiście zamierzone, aby zrozumieć różnice semantyczne między przykładami lol
Tarantula
@FredOverflow Stare pytanie, ale zrozumienie twojego komentarza zajęło mi sekundę. Myślę, że pytanie nr 2 brzmiało, czy std::move()utworzono trwałą „kopię”.
3Dave
5
@DavidLively std::move(expression)niczego nie tworzy, po prostu rzutuje wyrażenie na wartość x. Żadne obiekty nie są kopiowane ani przenoszone w trakcie oceny std::move(expression).
fredoverflow

Odpowiedzi:

562

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 wartość tymczasową, która jest przechwytywana przez rval_ref. Tymczasowe życie wydłuży się poza rval_refdefinicję i możesz go używać tak, jakbyś złapał go pod względem wartości. Jest to bardzo podobne do następującego:

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

poza tym, że w moim przepisywaniu nie można oczywiście używać rval_refw sposób niezmienny.

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 utworzono błąd czasu wykonywania. rval_refteraz zawiera odniesienie do zniszczonej tmpfunkcji. Przy odrobinie szczęścia ten kod natychmiast się zawiesi.

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 taki sam jak twój pierwszy. std::moveNa tmpto niepotrzebne i może być pessimization wydajność jak to będzie hamować optymalizacji zwracanej wartości.

Najlepszym sposobem na kodowanie tego, co robisz, jest:

Najlepsze praktyki

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

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

Tj. Tak jak w C ++ 03. tmpjest domyślnie traktowany jako wartość w instrukcji return. Zostanie on zwrócony poprzez optymalizację wartości zwracanej (bez kopiowania, bez przenoszenia), lub jeśli kompilator zdecyduje, że nie może wykonać RVO, użyje konstruktora ruchu wektora, aby dokonać zwrotu . Tylko jeśli RVO nie zostanie wykonane i jeśli zwracany typ nie miał konstruktora przenoszenia, do zwrotu zostanie użyty konstruktor kopiowania.

Howard Hinnant
źródło
64
Kompilatory wykonają RVO, gdy zwrócisz obiekt lokalny według wartości, a typ lokalny i zwrot funkcji są takie same i żaden z nich nie jest kwalifikowany do cv (nie zwracaj stałych typów). Unikaj zwracania z instrukcją warunek (:?), Ponieważ może to zahamować RVO. Nie zawijaj lokalnego w jakąś inną funkcję, która zwraca odwołanie do lokalnego. Właśnie return my_local;. Wiele instrukcji zwrotu jest w porządku i nie będzie hamować RVO.
Howard Hinnant,
27
Istnieje zastrzeżenie: podczas zwracania członka obiektu lokalnego ruch musi być jawny.
boycy
5
@NoSenseEtAl: Nie ma tymczasowego utworzenia w linii powrotnej. movenie tworzy tymczasowego. Rzuca wartość na wartość x, nie robi kopii, nie tworzy niczego, niczego nie niszczy. Ten przykład jest dokładnie taką samą sytuacją, jak w przypadku powrotu przez odwołanie do wartości i usunięcie movewiersza zwrotnego: Tak czy inaczej, masz wiszące odwołanie do zmiennej lokalnej wewnątrz funkcji i która została zniszczona.
Howard Hinnant
15
„Wiele instrukcji return jest w porządku i nie hamuje RVO”: Tylko jeśli zwracają tę samą zmienną.
Deduplicator
5
@Deduplicator: Masz rację. Nie mówiłem tak dokładnie, jak zamierzałem. Miałem na myśli to, że wiele instrukcji return nie zabrania kompilatorowi RVO (chociaż uniemożliwia to implementację), dlatego wyrażenie zwrotne jest nadal uważane za wartość.
Howard Hinnant,
42

Żadne z nich nie zostanie skopiowane, ale drugie odniesie się do zniszczonego wektora. Nazwane referencje wartości prawie nigdy nie istnieją w zwykłym kodzie. Piszesz to tak, jakbyś napisał 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 wyjątkiem teraz wektor jest przenoszony. Użytkownik klasy nie zajmuje się ona w odnośnikach rvalue w zdecydowanej większości przypadków.

Szczeniak
źródło
Czy naprawdę jesteś pewien, że trzeci przykład to kopiowanie wektorowe?
Tarantula,
@Tarantula: Zniszczy twój wektor. Nieważne, czy to zrobiło, czy nie skopiowało przed zniszczeniem.
Szczeniak
4
Nie widzę żadnego powodu, dla którego zaproponujesz obalenie. Zupełnie dobrze jest powiązać lokalną zmienną odniesienia do wartości z wartością. W takim przypadku czas życia obiektu tymczasowego jest przedłużany do czasu życia zmiennej referencyjnej wartości.
fredoverflow
1
Tylko kwestia wyjaśnienia, ponieważ uczę się tego. W tym nowym przykładzie wektor tmpnie jest przenoszony do rval_ref, lecz zapisywany bezpośrednio rval_refprzy użyciu RVO (tzn. Elision copy). Istnieje rozróżnienie między std::moveelision a copy. A std::movemoże nadal wymagać kopiowania niektórych danych; w przypadku wektora nowy konstruktor jest tak naprawdę konstruowany w kreatorze kopiowania i dane są przydzielane, ale większość tablicy danych jest kopiowana tylko przez skopiowanie wskaźnika (zasadniczo). Eliminacja kopii pozwala uniknąć 100% wszystkich kopii.
Mark Lakata,
@MarkLakata To jest NRVO, a nie RVO. NRVO jest opcjonalny, nawet w C ++ 17. Jeśli nie zostanie zastosowane, zarówno wartość zwracana, jak i rval_refzmienne są konstruowane przy użyciu konstruktora przenoszenia z std::vector. Nie ma konstruktora kopii zaangażowanego zarówno z / bez std::move. w tym przypadku tmpjest traktowany jako wartość w returninstrukcji.
Daniel Langr
16

Prosta odpowiedź brzmi: powinieneś pisać kod dla wartości referencyjnych, tak jak normalny kod referencyjny, i powinieneś traktować je tak samo mentalnie przez 99% czasu. Obejmuje to wszystkie stare reguły dotyczące zwracania referencji (tj. Nigdy nie zwracaj referencji do zmiennej lokalnej).

O ile nie piszesz szablonowej klasy kontenera, która musi skorzystać ze std :: forward i być w stanie napisać ogólną funkcję, która przyjmuje odwołania do wartości lub wartości, jest to mniej więcej prawda.

Jedną z dużych zalet konstruktora przenoszenia i przypisania przeniesienia jest to, że jeśli je zdefiniujesz, kompilator może ich użyć w przypadkach, gdy RVO (optymalizacja wartości zwracanej) i NRVO (optymalizacja nazwanej wartości zwrotnej) nie zostaną wywołane. Jest to dość duże, jeśli chodzi o wydajne zwracanie drogich obiektów, takich jak kontenery i łańcuchy, pod względem wartości z metod.

Teraz, gdy sprawy stają się interesujące w odniesieniu do odwołań do wartości, można również użyć ich jako argumentów normalnych funkcji. Pozwala to na pisanie kontenerów, które mają przeciążenia zarówno dla stałej referencji (const foo i inne), jak i wartości referencyjnej (foo i& inne). Nawet jeśli argument jest zbyt niewygodny, 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 w taki sposób, że mają przeciążenia związane z przenoszeniem prawie wszystkiego (klucz skrótu i ​​wartości, wstawianie wektorów itp.) I tam, gdzie będzie ich najwięcej.

Możesz także użyć ich do normalnych funkcji, a jeśli podasz tylko argument odwołania do wartości, możesz zmusić program wywołujący do utworzenia obiektu i pozwolić funkcji wykonać ruch. Jest to raczej przykład niż naprawdę dobre zastosowanie, ale w mojej bibliotece renderowania przypisałem ciąg do wszystkich załadowanych zasobów, aby łatwiej było zobaczyć, co każdy obiekt reprezentuje w debuggerze. Interfejs wygląda mniej więcej tak:

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 skorzystać z faktu, że musiałem tworzyć ciąg już przez większość czasu i uniknąć tworzenia kolejnego kopiowania. Nie jest to dokładnie kod o wysokiej wydajności, ale jest dobrym przykładem możliwości, gdy ludzie znają tę funkcję. Ten kod faktycznie wymaga, aby zmienna była tymczasowa dla wywołania lub wywołana 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);
Strefa
źródło
3

Nie odpowiedź sama w sobie , ale wskazówka. W większości przypadków deklarowanie T&&zmiennej lokalnej nie ma większego sensu (jak w przypadku std::vector<int>&& rval_ref). Nadal będziesz musiał std::move()je użyć w foo(T&&)metodach typu. Istnieje również problem, o którym już wspomniano, że gdy spróbujesz powrócić rval_refz funkcji, otrzymasz standardowe odwołanie do zniszczonego tymczasowego fiasku.

Najczęściej wybieram następujący wzór:

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

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

Nie przechowuje się żadnych referencji do zwróconych obiektów tymczasowych, dzięki czemu unika się (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 są (choć raczej rzadkie) przypadki, w których funkcja naprawdę zwraca wartość, T&&która jest odniesieniem do nietrwałego obiektu, który można przenieść do swojego obiektu.

Odnośnie RVO: mechanizmy te ogólnie działają, a kompilator może ładnie uniknąć kopiowania, ale w przypadkach, gdy ścieżka zwrotna nie jest oczywista (wyjątki, ifwarunki określające nazwany obiekt, który zwrócisz i prawdopodobnie kilka innych), odnośniki są twoimi wybawcami (nawet jeśli potencjalnie więcej kosztowny).

Czerwony XIII
źródło
2

Żadne z nich nie wykona żadnego dodatkowego kopiowania. Nawet jeśli RVO nie jest używane, nowy standard mówi, że konstrukcja przenoszenia jest preferowana do kopiowania podczas dokonywania zwrotów.

Wierzę, że twój drugi przykład powoduje niezdefiniowane zachowanie, ponieważ zwracasz odwołanie do zmiennej lokalnej.

Edward Strange
źródło
1

Jak już wspomniano w komentarzach do pierwszej odpowiedzi, return std::move(...);konstrukcja może mieć znaczenie w przypadkach innych niż zwracanie zmiennych lokalnych. Oto działający przykład, który dokumentuje, co dzieje się, gdy zwracasz obiekt członkowski zi bez std::move():

#include <iostream>
#include <utility>

struct A {
  A() = default;
  A(const A&) { std::cout << "A copied\n"; }
  A(A&&) { std::cout << "A moved\n"; }
};

class B {
  A a;
 public:
  operator A() const & { std::cout << "B C-value: "; return a; }
  operator A() & { std::cout << "B L-value: "; return a; }
  operator A() && { std::cout << "B R-value: "; return a; }
};

class C {
  A a;
 public:
  operator A() const & { std::cout << "C C-value: "; return std::move(a); }
  operator A() & { std::cout << "C L-value: "; return std::move(a); }
  operator A() && { std::cout << "C R-value: "; return std::move(a); }
};

int main() {
  // Non-constant L-values
  B b;
  C c;
  A{b};    // B L-value: A copied
  A{c};    // C L-value: A moved

  // R-values
  A{B{}};  // B R-value: A copied
  A{C{}};  // C R-value: A moved

  // Constant L-values
  const B bc;
  const C cc;
  A{bc};   // B C-value: A copied
  A{cc};   // C C-value: A copied

  return 0;
}

Przypuszczalnie return std::move(some_member);ma to sens tylko wtedy, gdy rzeczywiście chcesz przenieść konkretnego członka klasy, np. W przypadku, gdy class Creprezentuje on krótkotrwałe obiekty adaptera wyłącznie w celu utworzenia instancji klasystruct A .

Wskazówki jak struct Azawsze zostanie skopiowane z class B, nawet wtedy, gdy class Bobiekt jest wartość R. Wynika to z faktu, że kompilator nie ma sposobu na stwierdzenie, że class Bjego instancja struct Anie będzie już używana. W class Ckompilator ma tę informację std::move(), dlatego struct Azostaje przeniesiony , chyba że instancja class Cjest stała.

Andrej Podzimek
źródło