Я где-то видел код, в котором кто-то решил скопировать объект и впоследствии переместить его в член данных класса. Это привело меня в замешательство, поскольку я думал, что весь смысл перемещения заключается в том, чтобы избежать копирования. Вот пример:
struct S
{
S(std::string str) : data(std::move(str))
{}
};
Вот мои вопросы:
- Почему мы не берем ссылку на rvalue
str
? - Разве копия не будет дорогой, особенно учитывая что-то вроде
std::string
? - По какой причине автор решил сделать копию, а затем переехать?
- Когда мне самому это сделать?
c++
c++11
move-semantics
user2030677
источник
источник
Ответы:
Прежде чем я отвечу на ваши вопросы, вы, кажется, ошибаетесь в одном: принятие по значению в C ++ 11 не всегда означает копирование. Если передано rvalue, оно будет перемещено (при наличии жизнеспособного конструктора перемещения), а не скопировано. И у
std::string
него есть конструктор перемещения.В отличие от C ++ 03, в C ++ 11 часто бывает идиоматично принимать параметры по значению по причинам, которые я объясню ниже. Также смотрите эти вопросы и ответы на StackOverflow, чтобы получить более общий набор рекомендаций о том, как принимать параметры.
Потому что это сделало бы невозможным передачу lvalue, например:
Если бы был
S
только конструктор, принимающий rvalue, вышеперечисленное не компилировалось бы.Если вы передадите rvalue, оно будет перемещено в
str
, и в конечном итоге будет перемещено вdata
. Копирование не производится. Если передать именующее выражение, с другой стороны, что - значение будет скопировано вstr
, а затем переехал вdata
.Подводя итог, два шага для rvalues, одна копия и один ход для lvalues.
Во-первых, как я упоминал выше, первый не всегда является копией; и при этом ответ таков: « Потому что это эффективно (перемещение
std::string
объектов дешево) и просто ».Если предположить, что перемещения являются дешевыми (без учета SSO), их можно практически не учитывать при рассмотрении общей эффективности этой конструкции. Если мы это сделаем, у нас будет одна копия для lvalues (как у нас было бы, если бы мы приняли ссылку lvalue на
const
) и никаких копий для rvalues (хотя у нас все еще была бы копия, если бы мы приняли ссылку lvalue наconst
).Это означает, что принимать по значению так же хорошо, как по ссылке lvalue,
const
когда предоставляются lvalue , и лучше, когда предоставляются rvalue.PS: Чтобы обеспечить некоторый контекст, я считаю, что это вопросы и ответы, о которых говорит OP.
источник
const T&
передачу аргументов: в худшем случае (lvalue) это то же самое, но в случае временного вам нужно только переместить временное. Беспроигрышный вариант.data
члене)? У вас будет копия, даже если вы возьмете ссылку lvalue наconst
data
!Чтобы понять, почему это хороший шаблон, мы должны изучить альтернативы как в C ++ 03, так и в C ++ 11.
У нас есть метод C ++ 03 для получения
std::string const&
:в этом случае всегда будет выполняться одна копия. Если вы строите из необработанной строки C,
std::string
будет построено, а затем снова скопировано: два распределения.Существует метод C ++ 03, позволяющий взять ссылку на a
std::string
, а затем заменить ее на локальнуюstd::string
:это версия C ++ 03 «семантики перемещения», и
swap
ее часто можно оптимизировать, чтобы сделать ее очень дешевой (во многом как amove
). Это также следует анализировать в контексте:и заставляет вас сформировать невременное
std::string
, а затем отбросить его. (Временноеstd::string
не может быть привязано к неконстантной ссылке). Однако выполняется только одно распределение. Версия C ++ 11 примет a&&
и потребует, чтобы вы вызывали ее с помощьюstd::move
или с помощью временного: для этого требуется, чтобы вызывающий объект явно создал копию вне вызова и переместил эту копию в функцию или конструктор.Использование:
Затем мы можем сделать полную версию C ++ 11, которая поддерживает как копирование, так и
move
:Затем мы можем изучить, как это используется:
Совершенно очевидно, что этот метод двух перегрузок по крайней мере так же эффективен, если не больше, чем два вышеуказанных стиля C ++ 03. Я назову эту версию с двумя перегрузками «самой оптимальной» версией.
Теперь мы рассмотрим версию с дублированием:
в каждом из этих сценариев:
Если сравнить эту параллельную версию с «самой оптимальной» версией, мы сделаем ровно одну дополнительную
move
! Ни разу не делаем лишнегоcopy
.Так что, если предположить, что
move
это дешево, эта версия дает нам почти такую же производительность, что и наиболее оптимальная версия, но в 2 раза меньше кода.И если вы берете, скажем, от 2 до 10 аргументов, сокращение кода будет экспоненциальным - в 2 раза меньше с 1 аргументом, в 4 раза с 2, 8x с 3, 16x с 4, 1024x с 10 аргументами.
Теперь мы можем обойти это с помощью идеальной пересылки и SFINAE, позволяя вам написать единственный конструктор или шаблон функции, который принимает 10 аргументов, выполняет SFINAE, чтобы гарантировать, что аргументы имеют соответствующие типы, а затем перемещает или копирует их в местное государство по мере необходимости. Хотя это предотвращает тысячукратное увеличение размера программы, из этого шаблона все же может быть сгенерирована целая куча функций. (экземпляры шаблонных функций генерируют функции)
А большое количество сгенерированных функций означает больший размер исполняемого кода, что само по себе может снизить производительность.
За несколько
move
секунд мы получаем более короткий код и почти такую же производительность, а зачастую и более простой для понимания код.Теперь это работает только потому, что мы знаем, что при вызове функции (в данном случае конструктора) нам потребуется локальная копия этого аргумента. Идея в том, что если мы знаем, что собираемся делать копию, мы должны сообщить вызывающей стороне, что мы делаем копию, поместив ее в наш список аргументов. Затем они могут оптимизировать тот факт, что они собираются дать нам копию (например, перейдя к нашему аргументу).
Еще одно преимущество техники «принимать по значению» состоит в том, что конструкторы перемещения часто не являются исключениями. Это означает, что функции, которые принимают значение по значению и выходят из своего аргумента, часто могут быть без исключения, перемещая любые
throw
s из своего тела в вызывающую область. (кто может избежать этого через прямое построение иногда или создавать элементы иmove
в аргумент, чтобы контролировать, где происходит выброс). Часто стоит сделать методы nothrow.источник
noexcept
. Принимая данные за копией, вы можете сделать свою функциюnoexcept
, и любая конструкция копии, вызывающая потенциальные выбросы (например, из памяти), происходит вне вызова вашей функции.Вероятно, это сделано намеренно и похоже на идиому копирования и обмена . В основном, поскольку строка копируется перед конструктором, сам конструктор безопасен в отношении исключений, поскольку он только меняет местами (перемещает) временную строку str.
источник
Вы же не хотите повторяться, написав конструктор для перемещения и один для копии:
Это очень шаблонный код, особенно если у вас несколько аргументов. Ваше решение позволяет избежать этого дублирования за счет ненужного переезда. (Однако операция перемещения должна быть довольно дешевой.)
Конкурирующая идиома - использовать идеальную переадресацию:
Магия шаблона будет перемещать или копировать в зависимости от переданного вами параметра. По сути, она расширяется до первой версии, где оба конструктора были написаны вручную. Для получения дополнительной информации см. Сообщение Скотта Мейера об универсальных ссылках .
С точки зрения производительности идеальная версия для пересылки превосходит вашу версию, поскольку позволяет избежать ненужных ходов. Однако можно возразить, что вашу версию легче читать и писать. В любом случае возможное влияние на производительность не должно иметь значения в большинстве ситуаций, так что в конечном итоге это, похоже, вопрос стиля.
источник