Почему мы копируем, а затем двигаемся?
Я где-то видел код, в котором кто-то решил скопировать объект, а затем переместите его на член данных класса. Это оставило меня в замешательстве, поскольку я думал, что весь смысл перемещения состоял в том, чтобы избежать копирования. Вот пример:
struct S
{
S(std::string str) : data(std::move(str))
{}
};
вот мои вопросы:
- почему мы не берем rvalue-ссылку на
str
? - не будет ли копия дорогой, особенно учитывая что-то вроде
std::string
? - что было бы причиной чтобы автор решил сделать копию, а затем двигаться?
- когда я должен сделать это сам?
4 ответа:
прежде чем я отвечу на ваши вопросы, одна вещь, которую вы, кажется, ошибаетесь: взятие по значению в C++11 не всегда означает копирование. Если rvalue передается, это будет двигался (при условии, что жизнеспособный конструктор перемещения существует), а не копируется. И
std::string
имеет конструктор перемещения.в отличие от C++03, В C++11 часто идиоматично принимать параметры по значению, по причинам, которые я объясню ниже. Также смотрите этот Q&A на StackOverflow для более общего набора рекомендаций о том, как принимать параметры.
почему мы не берем rvalue-ссылку на
str
?потому что это сделало бы невозможным пройти lvalues, например, в:
std::string s = "Hello"; S obj(s); // s is an lvalue, this won't compile!
если
S
только был конструктор, который принимает rvalues, выше не будет компилироваться.не будет ли копия дорогой, особенно учитывая что-то вроде
std::string
?если вы передадите rvalue, это будет двигался на
str
, и это в конечном итоге будет перемещено вdata
. Копирование не производится. Если вы передадите lvalue, с другой стороны, это lvalue будет скопировал наstr
, а потом переехал вdata
.Итак, чтобы подвести итог, два хода для rvalues, одна копия и один ход для lvalues.
какова была бы причина для автора решите сделать копию, а затем двигаться?
прежде всего, как я уже упоминал выше, первый не всегда является копией; и это говорит, ответ: "потому что это эффективно (движения
std::string
объекты дешевы) и просты".в предположении, что ходы дешевы (игнорируя SSO здесь), их можно практически игнорировать при рассмотрении общей эффективности этой конструкции. Если мы это сделаем, у нас есть одна копия для lvalues (как и если бы мы приняли ссылку lvalue в
const
) и никаких копий для rvalues (хотя у нас все равно будет копия, если мы примем ссылку lvalue наconst
).это означает, что взятие по значению так же хорошо, как взятие по ссылке lvalue на
const
когда lvalues предоставляются, и лучше, когда rvalues предоставляются.С. П.: Чтобы обеспечить некоторый контекст, я считаю это вопрос и ответ ОП имеет в виду.
чтобы понять, почему это хорошая модель, мы должны изучить альтернативы, как в C++03, так и в C++11.
у нас есть C++03 метод принятия
std::string const&
:struct S { std::string data; S(std::string const& str) : data(str) {} };
в этом случае всегда будет выполнена одна копия. Если вы строите из необработанной строки C, a
std::string
будет построен, а затем скопирован снова: два распределения.существует метод C++03 для получения ссылки на A
std::string
, затем замена его в местныйstd::string
:struct S { std::string data; S(std::string& str) { std::swap(data, str); } };
это версия C++03 "семантика перемещения", и
swap
часто может быть оптимизирован, чтобы быть очень дешевым (подобноmove
). Он также должен быть проанализирован в контексте:S tmp("foo"); // illegal std::string s("foo"); S tmp2(s); // legal
и заставляет вас формировать нестационарный
std::string
, то откажитесь от нее. (Временныйstd::string
невозможно привязать к неконстантной ссылке). Однако выполняется только одно распределение. Версия C++11 будет принимать&&
и требуют, чтобы вы позвонили ему сstd::move
, или с временным: это требует, что абонент явно создает копию вне вызова и перемещает эту копию в функцию или конструктор.struct S { std::string data; S(std::string&& str): data(std::move(str)) {} };
использование:
S tmp("foo"); // legal std::string s("foo"); S tmp2(std::move(s)); // legal
Далее, мы можем сделать полную версию C++11, которая поддерживает как копирование, так и
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 };
мы можем изучить, как это использовать:
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
довольно ясно, что этот метод перегрузки 2 по крайней мере так же эффективен, если не более того,чем выше два стиля C++03. Я назову эту версию с 2 перегрузками "самой оптимальной" версией.
теперь мы рассмотрим версию take-by-copy:
struct S2 { std::string data; S2( std::string arg ):data(std::move(x)) {} };
в каждом из этих сценариев:
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
если вы сравните это бок о бок с" наиболее оптимальной " версией, мы сделаем ровно один дополнительный
move
! Не один раз мы делаем дополнительныйcopy
.Итак, если мы предположим, что
move
дешево, эта версия получает нас почти та же производительность, что и у самой оптимальной версии, но в 2 раза меньше кода.и если вы принимаете, скажем, от 2 до 10 аргументов, сокращение кода экспоненциально - в 2 раза меньше с 1 аргументом, 4x с 2, 8x с 3, 16x с 4, 1024x с 10 аргументами.
теперь мы можем обойти это с помощью perfect forwarding и SFINAE, позволяя вам написать один конструктор или шаблон функции, который принимает 10 аргументов, делает SFINAE для обеспечения того, чтобы аргументы были подходящими типы, а затем перемещает или копирует их в локальное состояние по мере необходимости. Хотя это предотвращает тысячекратное увеличение проблемы размера программы, все еще может быть целая куча функций, созданных из этого шаблона. (экземпляры функции шаблона генерируют функции)
и множество сгенерированных функций означает больший размер исполняемого кода, что само по себе может снизить производительность.
за счет нескольких
move
s, мы получаем более короткий код и почти то же самое производительность, и часто легче понять код.теперь это работает только потому, что мы знаем, когда вызывается функция (в данном случае конструктор), что нам понадобится локальная копия этого аргумента. Идея заключается в том, что если мы знаем, что мы собираемся сделать копию, мы должны сообщить вызывающему, что мы делаем копию, поместив ее в наш список аргументов. Затем они могут оптимизировать вокруг того факта, что они собираются дать нам копию (перейдя в наш аргумент, например образец.)
еще одно преимущество метода "take by value" заключается в том, что часто конструкторы перемещения не являются исключением. Это означает, что функции, которые принимают по-значению и выходят из их аргумента, часто могут быть noexcept, перемещая любой
throw
s из их тела и в область вызова (кто может избежать его через прямое строительство иногда, или построить элементы иmove
в аргумент, чтобы контролировать, где происходит бросание). Создание методов nothrow часто стоит того.
Это, вероятно, намеренно и похоже на скопировать и поменять идиоматическое выражение. В основном, поскольку строка копируется перед конструктором, сам конструктор является безопасным исключением, поскольку он только меняет местами (перемещает) временную строку str.
вы не хотите повторяться, написав конструктор для перемещения и один для копирования:
S(std::string&& str) : data(std::move(str)) {} S(const std::string& str) : data(str) {}
это много boilerplate кода, особенно если у вас есть несколько аргументов. Ваше решение позволяет избежать дублирования стоимости ненужного перемещения. (Однако операция перемещения должна быть довольно дешевой.)
конкурирующая идиома заключается в использовании perfect forwarding:
template <typename T> S(T&& str) : data(std::forward<T>(str)) {}
волшебство шаблона выберет двинуть или скопировать в зависимости от параметр, который вы передаете. Он в основном расширяется до первой версии, где оба конструктора были написаны вручную. Для получения справочной информации см. сообщение Скотта Мейера на универсальный ссылок.
С точки зрения производительности, идеальная версия пересылки превосходит вашу версию, поскольку она позволяет избежать ненужных движений. Однако можно утверждать, что ваша версия легче читать и писать. Возможное влияние на производительность не должно иметь значения в большинстве ситуаций, так или иначе, так что в конце концов, это вопрос стиля.