Возможно ли использовать std::move для локальных стековых переменных?
Описание проблемы:
У меня возникла проблема с пониманием работы перемещения объектов в C++. Рассмотрим следующий код:
struct MyStruct
{
int iInteger;
string strString;
};
void MyFunc(vector<MyStruct>& vecStructs)
{
MyStruct NewStruct = { 8, "Hello" };
vecStructs.push_back(std::move(NewStruct));
}
int main()
{
vector<MyStruct> vecStructs;
MyFunc(vecStructs);
}
Вопрос:
Почему это работает?
Когда вызывается функция MyFunc
, адрес возврата помещается в стек текущего потока. Затем создается объект NewStruct
, который также должен быть размещен в стеке. При использовании std::move
я сообщаю компилятору, что больше не планирую использовать ссылку на NewStruct
. Компилятор может "украсть" эту память (так как функция push_back
поддерживает семантику перемещения).
Однако, когда функция возвращается и NewStruct
выходит из области видимости, компилятор должен удалить как минимум ранее сохраненный адрес возврата. Это может привести к фрагментации стека, и будущие allocations могут перезаписать "перемещенную" память.
Кто-то может пояснить это?
РЕДАКТИРОВАНИЕ:
Во-первых, спасибо большое за ваши ответы.
Однако, то, что я узнал, все еще не помогает мне понять, почему следующий код не работает так, как я ожидаю:
struct MyStruct
{
int iInteger;
string strString;
string strString2;
};
void MyFunc(vector<MyStruct>& vecStructs)
{
MyStruct oNewStruct = { 8, "Hello", "Определенно больше 16 символов" };
vecStructs.push_back(std::move(oNewStruct));
// На этом этапе oNewStruct.strString2 должен быть "", поскольку его память была "украдена".
// Но это действительно так только тогда, когда я явно создаю конструктор перемещения,
// как это было указано Yakk.
}
void main()
{
vector<MyStruct> vecStructs;
MyFunc(vecStructs);
}
Почему strString2
не оказывается пустым?
2 ответ(ов)
Во-первых, std::move
не перемещает, а std::forward
не пересылает.
std::move
— это приведение к rvalue-ссылке. По соглашению, rvalue-ссылки рассматриваются как «ссылки, из которых вам разрешено перемещать данные, поскольку вызывающая сторона обещает, что ей эти данные больше не нужны».
С другой стороны, rvalue-ссылки неявно связываются с возвращаемым значением от std::move
(и иногда от std::forward
), с временными объектами, в определенных случаях, когда возвращается локальный объект из функции, и при использовании члена временного или переезда объекта.
То, что происходит внутри функции, принимающей rvalue-ссылку, — это не магия. Она не может напрямую получить доступ к памяти внутри данного объекта. Однако она может «вытащить его внутренности»; ей позволено (по соглашению) вмешиваться во внутреннее состояние своих аргументов, если это может быть выполнено быстрее.
Теперь в C++ автоматически создаются некоторые конструкторы перемещения для вас.
struct MyStruct
{
int iInteger;
std::string strString;
};
В этом случае компилятор создаст примерно такой код:
MyStruct::MyStruct( MyStruct&& other ) noexcept(true) :
iInteger( std::move(other.iInteger) ),
strString( std::move(other.strString) )
{}
Иными словами, будет произведено перемещение поэлементно.
Когда вы перемещаете целое число, ничего интересного не происходит. Нет смысла менять состояние исходного целого числа.
Когда вы перемещаете std::string
, это приносит определенные преимущества. Стандарт C++ описывает, что происходит, когда вы перемещаете из одного std::string
в другой. В основном, если исходный std::string
использует кучу, память кучи передается в целевой std::string
.
Это общая схема для контейнеров C++: когда вы перемещаетесь из них, они «крадут» выделенную в куче память исходного контейнера и повторно используют ее в целевом.
Заметьте, что исходный std::string
остается std::string
, просто он «остался без внутренностей». Большинство контейнероподобных объектов остаются пустыми, не припомню, гарантирует ли std::string
это (возможно, нет из-за оптимизации небольших буферов), и это сейчас не важно.
Вкратце, когда вы перемещаете из чего-то, его память не «повторно используется», но память, которой он владеет, может быть повторно использована.
В вашем случае MyStruct
содержит std::string
, который может использовать выделенную в куче память. Эта память может быть перемещена в MyStruct
, хранящийся в std::vector
.
Продолжая углубляться в тему, "Hello"
может быть настолько коротким, что произойдет оптимизация небольших буферов (SBO), и std::string
вообще не будет использовать кучу. В этом конкретном случае преимущества от move
могут быть минимальными.
Ваш пример можно сократить до:
vector<string> vec;
string str; // заполните длинной строкой
vec.push_back(std::move(str));
Это всё ещё поднимает вопрос: "Можно ли перемещать локальные переменные из стека?" Просто убирает лишний код, чтобы сделать его понятнее.
Ответ — да. Код типа выше может использовать преимущества std::move
, поскольку std::string
— по крайней мере, если содержимое достаточно велико — хранит фактические данные на куче, даже если переменная находится на стеке.
Если вы не используете std::move()
, вы можете ожидать, что код, подобный приведённому выше, будет копировать содержимое str
, которое может быть произвольно большим. Если же вы используете std::move()
, будут скопированы только прямые члены строки (так как move
не требует "обнуления" старых мест), а данные будут использованы без модификации или копирования.
Это в основном разница между следующим:
char* str; // заполните длинной строкой
char* other = new char[strlen(str)+1];
strcpy(other, str);
и
char* str; // заполните длинной строкой
char* other = str;
В обоих случаях переменные находятся на стеке. Но данные — нет.
Если у вас есть случай, когда все данные действительно находятся на стеке, например, std::string
с эффектом "оптимизации малой строки" или структура, содержащая целые числа, тогда std::move()
не принесёт никакой пользы.
Что такое std::move() и когда его следует использовать?
push_back против emplace_back: в чем разница?
Когда действительно стоит использовать noexcept?
Возможно ли вывести тип переменной в стандартном C++?
Стоит ли игнорировать предупреждение "-Wmissing-braces" от gcc/clang?