Po co kopiujemy?

Widziałem gdzieś kod, w którym ktoś zdecydował się skopiować obiekt, a następnie przenieść go do członka danych klasy. To pozostawiło mnie w zamieszaniu, ponieważ myślałem, że cały sens przeprowadzki polega na unikaniu kopiowania. Oto przykład:

struct S
{
    S(std::string str) : data(std::move(str))
    {}
};

Oto moje pytania:

  • Dlaczego nie bierzemy odniesienia rvalue do str?
  • czy kopia nie będzie droga, zwłaszcza biorąc pod uwagę coś takiego jak std::string?
  • jaki byłby powód, dla którego autor zdecydował się na / align = "left" /
  • Kiedy powinnam to zrobić sama?
Author: user2030677, 2013-05-24

4 answers

Zanim odpowiem na twoje pytania, jedno wydaje się być błędne: przyjmowanie wartości w C++11 nie zawsze oznacza kopiowanie. Jeśli przekazana zostanie wartość r, zostanie ona przeniesiona (pod warunkiem, że istnieje realny konstruktor move) zamiast być kopiowana. I std::string ma konstruktor ruchu.

W Przeciwieństwie Do C++03, W C++11 często idiomatyczne jest przyjmowanie parametrów według wartości, z powodów, które wyjaśnię poniżej. Zobacz też pytania i odpowiedzi na temat StackOverflow na bardziej ogólny zestaw wytycznych dotyczących akceptowania parametrów.

Dlaczego nie bierzemy odniesienia rvalue do str?

Ponieważ uniemożliwiłoby to przejście lvalu, np. w:

std::string s = "Hello";
S obj(s); // s is an lvalue, this won't compile!

Jeśli S miał tylko konstruktor, który akceptuje wartości R, powyższy konstruktor nie kompilowałby się.

Czy kopia nie będzie droga, zwłaszcza biorąc pod uwagę coś takiego jak std::string?

Jeśli zdasz wartość r, zostanie ona przeniesiona do str, i to w końcu zostanie przeniesione do data. Kopiowanie nie będzie wykonywane. Jeśli zdasz lvalue, z drugiej strony, lvalue zostanie skopiowane do str, a następnie przeniesione do data.

Podsumowując, dwa ruchy dla wartości R, jedna kopia i jeden ruch dla wartości LV.

Jaki byłby powód, dla którego autor zdecydował się na wykonanie kopii, a następnie ruch?

Po pierwsze, jak wspomniałem powyżej, pierwsza nie zawsze jest kopią; a to powiedziało: odpowiedź brzmi: "ponieważ jest wydajny (ruchy obiektów {[1] } są tanie) i prosty ".

Przy założeniu, że ruchy są tanie (pomijając tutaj SSO), można je praktycznie pominąć, biorąc pod uwagę ogólną wydajność tego projektu. Jeśli to zrobimy, mamy jedną kopię dla lvalue (tak jak byśmy mieli, gdybyśmy zaakceptowali odniesienie lvalue do const) i żadnych kopii dla rvalue (podczas gdy nadal mielibyśmy kopię, gdybyśmy zaakceptowali odniesienie lvalue do const).

To oznacza, że przyjmowanie przez wartość jest tak dobre, jak przyjmowanie przez odniesienie lvalue do const, gdy podane są wartości lvalue, i lepsze, gdy podane są wartości R.

P. S.: aby podać jakiś kontekst, wierzę to jest Q&a , do którego odnosi się OP.

 92
Author: Andy Prowl,
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-05-23 12:02:27

Aby zrozumieć, dlaczego jest to dobry wzór, powinniśmy zbadać alternatywy, zarówno w C++03, jak i w C++11.

Mamy metodę C++03 z std::string const&:

struct S
{
  std::string data; 
  S(std::string const& str) : data(str)
  {}
};

W tym przypadku, zawsze będzie wykonywana pojedyncza kopia. Jeśli zbudujesz z surowego ciągu C, zostanie skonstruowany std::string, a następnie skopiowany ponownie: dwie alokacje.

Istnieje metoda C++03 polegająca na pobraniu odniesienia do std::string, a następnie zamianie go na lokalny std::string:

struct S
{
  std::string data; 
  S(std::string& str)
  {
    std::swap(data, str);
  }
};

Jest to wersja C++03 "semantyki move", a swap może być często zoptymalizowana, aby była bardzo tania (podobnie jak move). Należy go również analizować w kontekście:

S tmp("foo"); // illegal
std::string s("foo");
S tmp2(s); // legal

I zmusza cię do utworzenia nie-tymczasowego std::string, a następnie odrzucenia go. (Tymczasowe std::string nie może wiązać się z referencją non-const). Jednak tylko jeden przydział jest dokonywany. Wersja C++11 wymagałaby && i wymagałaby wywołania za pomocą std::move, lub tymczasowego: wymaga to wywołujący jawnie tworzy kopię poza wywołaniem i przenosi ją do funkcji lub konstruktora.

struct S
{
  std::string data; 
  S(std::string&& str): data(std::move(str))
  {}
};

Użycie:

S tmp("foo"); // legal
std::string s("foo");
S tmp2(std::move(s)); // legal

Następnie możemy wykonać pełną wersję C++11, która obsługuje zarówno copy jak i move:

struct S
{
  std::string data; 
  S(std::string const& str) : data(str) {} // lvalue const, copy
  S(std::string && str) : data(std::move(str)) {} // rvalue, move
};

Możemy następnie zbadać, jak to jest używane:

S tmp( "foo" ); // a temporary `std::string` is created, then moved into tmp.data

std::string bar("bar"); // bar is created
S tmp2( bar ); // bar is copied into tmp.data

std::string bar2("bar2"); // bar2 is created
S tmp3( std::move(bar2) ); // bar2 is moved into tmp.data

Jest całkiem jasne, że ta technika przeciążania 2 jest co najmniej tak samo skuteczna, jeśli nie bardziej, niż dwa powyższe style C++03. I 'll dub this 2-overload version the" most wersja optymalna.

Teraz zbadamy wersję take-by-copy:

struct S2 {
  std::string data;
  S2( std::string arg ):data(std::move(x)) {}
};

W każdym z tych scenariuszy:

S2 tmp( "foo" ); // a temporary `std::string` is created, moved into arg, then moved into S2::data

std::string bar("bar"); // bar is created
S2 tmp2( bar ); // bar is copied into arg, then moved into S2::data

std::string bar2("bar2"); // bar2 is created
S2 tmp3( std::move(bar2) ); // bar2 is moved into arg, then moved into S2::data

Jeśli porównasz tę wersję side-by-side z "najbardziej optymalną" wersją, zrobimy dokładnie jedną dodatkową move! Ani razu nie robimy dodatkowego copy.

Więc jeśli założymy, że move jest tania, ta wersja daje nam prawie taką samą wydajność jak najbardziej optymalna wersja, ale 2 razy mniej kodu.

A jeśli przyjmujesz powiedzmy 2 do 10 argumentów, redukcja kodu jest wykładnicza-2x razy mniejsza przy 1 argumencie, 4X przy 2, 8x przy 3, 16x przy 4, 1024x przy 10 argumentach.

Teraz możemy obejść to za pomocą perfect forwarding i SFINAE, pozwalając na napisanie pojedynczego konstruktora lub szablonu funkcji, który pobiera 10 argumentów, robi SFINAE, aby upewnić się, że argumenty są odpowiednich typów, a następnie przenosi-lub-kopiuje je do stanu lokalnego zgodnie z wymaganiami. Podczas gdy zapobiega to tysiąckrotnemu wzrostowi problemu wielkości programu, nadal może istnieć cały stos funkcji generowanych z tego szablonu. (instancje funkcji szablonu generują funkcje)

A wiele generowanych funkcji oznacza większy rozmiar kodu wykonywalnego, co samo w sobie może zmniejszyć wydajność.

Za cenę kilku moves otrzymujemy krótszy kod i prawie taką samą wydajność, a często łatwiejszy do zrozumienia kod.

Teraz to działa tylko dlatego, że wiemy, gdy funkcja (w tym przypadku konstruktor) jest wywoływana, że będzie chciał lokalnej kopii tego argumentu. Chodzi o to, że jeśli wiemy, że będziemy robić kopię, powinniśmy poinformować rozmówcę, że robimy kopię, umieszczając ją na naszej liście argumentów. Mogą wtedy zoptymalizować fakt, że dadzą nam kopię (na przykład przechodząc do naszego argumentu).

Kolejną zaletą techniki "take by value" jest to, że często konstruktory move są noexcept. Oznacza to funkcje, które przybierają wartość i wyprowadzają ich argument może być często noexcept, przenosząc dowolne throw s z ich ciała i do zakresu wywołującego (który może go uniknąć poprzez bezpośrednią budowę czasami, lub konstruować elementy i move do argumentu, aby kontrolować, gdzie się dzieje rzucanie). Dokonywanie metod nothrow jest często tego warte.

 51
Author: Yakk - Adam Nevraumont,
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-08-18 14:29:08

Jest to prawdopodobnie celowe i podobne do idiomu copy and swap. Zasadniczo ponieważ łańcuch jest kopiowany przed konstruktorem, sam konstruktor jest bezpieczny dla wyjątków, ponieważ tylko zamienia (przenosi) tymczasowy łańcuch str.

 13
Author: Joe,
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-05-23 22:10:47

Nie chcesz się powtarzać pisząc konstruktor dla ruchu i jeden dla kopii:

S(std::string&& str) : data(std::move(str)) {}
S(const std::string& str) : data(str) {}

Jest to dużo kodu boilerplate, zwłaszcza jeśli masz wiele argumentów. Twoje rozwiązanie pozwala uniknąć powielania kosztów niepotrzebnego ruchu. (Operacja przeprowadzki powinna być jednak dość tania.)

Konkurencyjnym idiomem jest użycie perfect forwarding:

template <typename T>
S(T&& str) : data(std::forward<T>(str)) {}

Magia szablonu wybierze przeniesienie lub skopiowanie w zależności od parametru, który podasz. Informatyka zasadniczo rozszerza się do pierwszej wersji, gdzie oba konstruktory zostały napisane ręcznie. Aby uzyskać podstawowe informacje, Zobacz post Scotta Meyera na Universal references .

Z punktu widzenia wydajności, wersja perfect forwarding jest lepsza od twojej wersji, ponieważ pozwala uniknąć niepotrzebnych ruchów. Można jednak argumentować, że Twoja wersja jest łatwiejsza do czytania i pisania. Ewentualny wpływ na wydajność i tak nie powinien mieć znaczenia w większości sytuacji, więc wydaje się, że jest to kwestia stylu w koniec.

 11
Author: Philipp Claßen,
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-05-25 16:49:50