Przenieś semantykę w C ++ - Move-return zmiennych lokalnych

11

Rozumiem, że w C ++ 11, gdy zwracasz zmienną lokalną z funkcji według wartości, kompilator może traktować tę zmienną jako odwołanie do wartości r i „przenosić” ją z funkcji, aby ją zwrócić (jeśli Oczywiście RVO / NRVO się nie zdarza).

Moje pytanie brzmi: czy to nie może złamać istniejącego kodu?

Rozważ następujący kod:

#include <iostream>
#include <string>

struct bar
{
  bar(const std::string& str) : _str(str) {}
  bar(const bar&) = delete;
  bar(bar&& other) : _str(std::move(other._str)) {other._str = "Stolen";}
  void print() {std::cout << _str << std::endl;}

  std::string _str;
};

struct foo
{
  foo(bar& b) : _b(b) {}
  ~foo() {_b.print();}

  bar& _b;
};

bar foobar()
{
  bar b("Hello, World!");
  foo f(b);

  return std::move(b);
}

int main()
{
  foobar();
  return EXIT_SUCCESS;
}

Myślałem, że możliwe byłoby, aby destruktor obiektu lokalnego odwoływał się do obiektu, który zostaje niejawnie poruszony, a zatem nieoczekiwanie zobaczyłby „pusty” obiekt. Próbowałem to sprawdzić (patrz http://ideone.com/ZURoeT ), ale mam „prawidłowy” wynik bez wyraźnej std::movein foobar(). Zgaduję, że było to spowodowane NRVO, ale nie próbowałem zmienić kodu, aby to wyłączyć.

Czy mam rację, że ta transformacja (powodująca wyjście z funkcji) zachodzi niejawnie i może uszkodzić istniejący kod?

AKTUALIZACJA Oto przykład, który ilustruje to, o czym mówię. Poniższe dwa łącza dotyczą tego samego kodu. http://ideone.com/4GFIRu - C ++ 03 http://ideone.com/FcL2Xj - C ++ 11

Jeśli spojrzysz na wynik, jest inny.

Sądzę więc, że to pytanie się teraz stało, czy zostało to wzięte pod uwagę przy dodawaniu niejawnego przeniesienia do standardu, i zdecydowano, że dodanie tej przełomowej zmiany jest w porządku, ponieważ ten rodzaj kodu jest dość rzadki? Zastanawiam się także, czy jakieś kompilatory będą ostrzegać w takich przypadkach ...

Bwmat
źródło
Ruch musi zawsze pozostawić obiekt w stanie do zniszczenia.
Zan Lynx,
Tak, ale to nie jest pytanie. Kod w wersji wcześniejszej niż c ++ 11 mógł zakładać, że wartość zmiennej lokalnej nie zmieniłaby się po prostu ze względu na jej zwrócenie, więc ten ukryty ruch mógłby złamać to założenie.
Bwmat
Właśnie to starałem się wyjaśnić w moim przykładzie; za pomocą destruktorów można sprawdzić stan (podzbiór) lokalnych zmiennych funkcji „po” wykonaniu instrukcji return, ale zanim funkcja faktycznie powróci.
Bwmat
To doskonałe pytanie w dodanym przykładzie. Mam nadzieję, że otrzyma więcej odpowiedzi od profesjonalistów, którzy mogą to wyjaśnić. Jedyne prawdziwe informacje zwrotne, które mogę udzielić, to: dlatego obiekty zwykle nie powinny mieć widoków nie posiadających danych. W rzeczywistości istnieje wiele sposobów pisania niewinnie wyglądającego kodu, który segreguje błędy, gdy dajesz obiektom widoki, które nie są właścicielami (surowe wskaźniki lub referencje). Mogę rozwinąć tę kwestię, udzielając właściwej odpowiedzi, jeśli chcesz, ale zgaduję, że nie o tym tak naprawdę chcesz usłyszeć. A przy okazji wiadomo już, że 11 może złamać istniejący kod, np. Poprzez zdefiniowanie nowych słów kluczowych.
Nir Friedman
Tak, wiem, że C ++ 11 nigdy nie twierdził, że nie łamie żadnego starego kodu, ale jest to dość subtelne i bardzo łatwo byłoby go pominąć (bez błędów kompilatora, ostrzeżeń, segfaultów)
Bwmat

Odpowiedzi:

8

Scott Meyers opublikował na comp.lang.c ++ (sierpień 2010 r.) O problemie polegającym na tym, że niejawne generowanie konstruktorów ruchu mogło złamać niezmienniki klasy C ++ 03:

struct X
{
  // invariant: v.size() == 5
  X() : v(5) {}

  ~X() { std::cout << v[0] << std::endl; }

private:    
  std::vector<int> v;
};

int main()
{
    std::vector<X> y;
    y.push_back(X()); // X() rvalue: copied in C++03, moved in C++0x
}

Problem polega na tym, że w C ++ 03 Xmiał niezmiennik, że jego vczłonek zawsze miał 5 elementów. X::~X()liczył na ten niezmiennik, ale nowo wprowadzony konstruktor ruchu przeniósł się v, ustawiając w ten sposób jego długość na zero.

Jest to związane z twoim przykładem, ponieważ zepsuty niezmiennik jest wykrywany tylko w Xdestruktorze (jak mówisz, możliwe jest, że destruktor obiektu lokalnego odwołuje się do obiektu, który zostaje niejawnie przeniesiony, a zatem nieoczekiwanie widzi pusty obiekt).

C ++ 11 stara się osiągnąć równowagę między zerwaniem części istniejącego kodu a dostarczeniem użytecznych optymalizacji opartych na konstruktorach move.

Komitet początkowo zdecydował, że konstruktory ruchów i operatory przydziału ruchów powinny być generowane przez kompilator, o ile użytkownik tego nie zapewni.

Następnie zdecydował, że to rzeczywiście był powód do alarmu i ograniczył automatyczne generowanie konstruktorów ruchu i operatorów przypisania ruchu w taki sposób, że jest znacznie mniejsze, choć nie niemożliwe, uszkodzenie istniejącego kodu (np. Jawnie zdefiniowany destruktor).

Kuszące jest myślenie, że zapobieganie generowaniu niejawnych konstruktorów ruchu, gdy obecny jest destruktor zdefiniowany przez użytkownika, jest wystarczające, ale nie jest to prawdą ( N3153 - Implicit Move Must Go for more details).

W N3174 - Aby przenieść lub nie, aby przenieść Stroupstrup mówi:

Uważam to za problem projektowania języka, a nie zwykły problem kompatybilności wstecznej. Łatwo jest uniknąć zerwania starego kodu (np. Po prostu usunąć operacje przenoszenia z C ++ 0x), ale widzę, że C ++ 0x jest lepszym językiem, ponieważ operacje przenoszenia są wszechobecne, głównym celem, dla którego warto złamać część C + Kod +98.

manlio
źródło