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

Я пытаюсь понять ссылки rvalue и переместить семантику C++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();

5 ответов


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

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 теперь содержит ссылку на destructed 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();

ваш третий пример примерно эквивалентен вашему первому. The 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();

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


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

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

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

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


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

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

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

теперь, когда вещи становятся интересными с ссылками 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 invoked:

// 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);

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

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


не ответ per se, но руководящее положение. В большинстве случаев нет смысла объявлять local 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-ваши спасители (даже если потенциально дороже).