С++ 11 rvalues ​​и перемещение семантики путаницы (return statement)

Я пытаюсь понять ссылки rvalue и перенести семантику С++ 11.

В чем разница между этими примерами, а какая из них не будет делать копию вектора?

Первый пример

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

Второй пример

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

Третий пример

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

Ответ 1

Первый пример

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

Первый пример возвращает временное, которое попадает на rval_ref. Это временное время будет продлеваться за пределами определения rval_ref, и вы можете использовать его так, как если бы вы его поймали по значению. Это очень похоже на следующее:

const std::vector<int>& rval_ref = return_vector();

за исключением того, что в моем переписании вы явно не можете использовать rval_ref неконстантным образом.

Второй пример

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

Во втором примере вы создали ошибку времени выполнения. rval_ref теперь содержит ссылку на разрушенный tmp внутри функции. Если повезет, этот код немедленно сработает.

Третий пример

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

Ваш третий пример примерно эквивалентен вашему первому. Std:: move on tmp не требуется и может фактически быть пессимизацией производительности, поскольку это будет препятствовать оптимизации возвращаемого значения.

Лучший способ кодировать то, что вы делаете:

Лучшая практика

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

т.е. как и в С++ 03. tmp неявно рассматривается как rvalue в операторе return. Он будет либо возвращен с помощью оптимизации возвращаемого значения (без копирования, без перемещения), либо если компилятор решит, что он не может выполнить RVO, тогда он будет использовать конструктор перемещения вектора для выполнения возврата. Только если RVO не выполняется, и если возвращаемый тип не имел конструктора перемещения, для его использования использовался бы конструктор копирования.

Ответ 2

Ни один из них не скопирует, а второй будет ссылаться на уничтоженный вектор. Именованные ссылки rvalue почти никогда не встречаются в обычном коде. Вы пишете это, как бы вы написали копию на С++ 03.

std::vector<int> return_vector()
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

За исключением теперь, вектор перемещается. Пользователь класса не имеет дело с его rvalue-ссылками в подавляющем большинстве случаев.

Ответ 3

Простым ответом является то, что вы должны написать код для ссылок rvalue, как и обычный код ссылок, и вы должны относиться к ним одинаково мысленно в 99% случаев. Это включает в себя все старые правила о возврате ссылок (т.е. Никогда не возвращать ссылку на локальную переменную).

Если вы не пишете класс контейнера шаблонов, который должен использовать std:: forward и иметь возможность писать общую функцию, которая принимает ссылки lvalue или rvalue, это более или менее верно.

Одним из больших преимуществ для конструктора перемещения и назначения переноса является то, что если вы их определяете, то компилятор может их использовать в случаях, когда RVO (оптимизация возвращаемого значения) и NRVO (с именем return value optimization) не могут быть вызваны, Это довольно огромно для эффективного возврата дорогостоящих объектов, таких как контейнеры и строки, из методов.

Теперь, когда вещи становятся интересными с ссылками rvalue, вы также можете использовать их в качестве аргументов для обычных функций. Это позволяет писать контейнеры с перегрузками как для ссылки const (const foo & other), так и для ссылки rvalue (foo & other). Даже если аргумент слишком громоздкий, чтобы пройти с помощью простого вызова конструктора, он все равно может быть выполнен:

std::vector vec;
for(int x=0; x<10; ++x)
{
    // automatically uses rvalue reference constructor if available
    // because MyCheapType is an unamed temporary variable
    vec.push_back(MyCheapType(0.f));
}


std::vector vec;
for(int x=0; x<10; ++x)
{
    MyExpensiveType temp(1.0, 3.0);
    temp.initSomeOtherFields(malloc(5000));

    // old way, passed via const reference, expensive copy
    vec.push_back(temp);

    // new way, passed via rvalue reference, cheap move
    // just don't use temp again,  not difficult in a loop like this though . . .
    vec.push_back(std::move(temp));
}

Контейнеры STL обновлены, чтобы иметь перегруженные перемещения почти для любого (хэш-ключ и значения, вставка вектора и т.д.), и именно там вы увидите их больше всего.

Вы также можете использовать их для обычных функций, и если вы предоставляете только аргумент ссылки rvalue, вы можете заставить вызывающего объекта создать объект и позволить функции выполнять перемещение. Это скорее пример, чем реальное использование, но в моей библиотеке рендеринга я назначил строку всем загруженным ресурсам, чтобы было легче увидеть, что каждый объект представляет в отладчике. Интерфейс выглядит примерно так:

TextureHandle CreateTexture(int width, int height, ETextureFormat fmt, string&& friendlyName)
{
    std::unique_ptr<TextureObject> tex = D3DCreateTexture(width, height, fmt);
    tex->friendlyName = std::move(friendlyName);
    return tex;
}

Это форма "негерметичной абстракции", но позволяет мне воспользоваться тем фактом, что я должен был создать строку уже большую часть времени, и не пытаюсь сделать еще одно копирование. Это не совсем высокопроизводительный код, но является хорошим примером возможностей, так как люди получают эту функцию. Этот код фактически требует, чтобы переменная либо была временной для вызова, либо вызвана std:: move:

// move from temporary
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard"));

или

// explicit move (not going to use the variable 'str' after the create call)
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, std::move(str));

или

// explicitly make a copy and pass the temporary of the copy down
// since we need to use str again for some reason
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string(str));

но это не скомпилируется!

string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);

Ответ 4

Не ответ как таковой, а рекомендация. В большинстве случаев нет смысла указывать локальную переменную T&& (как это было в случае с std::vector<int>&& rval_ref). Вам все равно придется std::move() использовать их в методах типа foo(T&&). Существует также проблема, о которой уже упоминалось, что, когда вы пытаетесь вернуть такую ​​функцию rval_ref из функции, вы получите стандартное обращение к разрушенному-временному фиаско.

В большинстве случаев я бы пошел со следующим шаблоном:

// Declarations
A a(B&&, C&&);
B b();
C c();

auto ret = a(b(), c());

У вас нет ссылок на возвращаемые временные объекты, поэтому вы избегаете (неопытных) ошибок программиста, которые хотят использовать перемещенный объект.

auto bRet = b();
auto cRet = c();
auto aRet = a(std::move(b), std::move(c));

// Either these just fail (assert/exception), or you won't get 
// your expected results due to their clean state.
bRet.foo();
cRet.bar();

Очевидно, что существуют (хотя и довольно редкие) случаи, когда функция действительно возвращает T&&, которая является ссылкой на не временный объект, который вы можете перемещать в свой объект.

Относительно RVO: эти механизмы, как правило, работают, и компилятор может прекрасно избегать копирования, но в случаях, когда путь возврата не является очевидным (исключения, if условные выражения, определяющие именованный объект, который вы вернете, и, возможно, пару других) rrefs являются вашими спасителей (даже если они потенциально дороже).

Ответ 5

Ни один из них не выполнит никакого дополнительного копирования. Даже если RVO не используется, новый стандарт говорит, что перемещение конструкции предпочтительнее копировать при выполнении возвратов, которые я считаю.

Я полагаю, что ваш второй пример вызывает поведение undefined, хотя, потому что вы возвращаете ссылку на локальную переменную.