0

Возможно ли использовать std::move для локальных стековых переменных?

13

Описание проблемы:

У меня возникла проблема с пониманием работы перемещения объектов в 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 ответ(ов)

0

Во-первых, 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 могут быть минимальными.

0

Ваш пример можно сократить до:

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() не принесёт никакой пользы.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь