Как вернуть объект из функции с учетом значений C++11 и переместить семантику?

Я пытаюсь понять 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 теперь содержит ссылку на разрушенный 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 на 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, тогда он будет использовать конструктор перемещения вектора для выполнения возврата. Только если 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();

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

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

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

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

Теперь, когда вещи становятся интересными со ссылками на rvalue, вы можете также использовать их в качестве аргументов обычных функций. Это позволяет вам писать контейнеры с перегрузками как для константной ссылки (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);

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

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

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

Как уже упоминалось в комментариях к первому ответу, return std::move(...);конструкция может иметь значение в других случаях, кроме возврата локальных переменных. Вот рабочий пример, который документирует, что происходит, когда вы возвращаете объект-член с и безstd::move():

#include <iostream>
#include <utility>

struct A {
  A() = default;
  A(const A&) { std::cout << "A copied\n"; }
  A(A&&) { std::cout << "A moved\n"; }
};

class B {
  A a;
 public:
  operator A() const & { std::cout << "B C-value: "; return a; }
  operator A() & { std::cout << "B L-value: "; return a; }
  operator A() && { std::cout << "B R-value: "; return a; }
};

class C {
  A a;
 public:
  operator A() const & { std::cout << "C C-value: "; return std::move(a); }
  operator A() & { std::cout << "C L-value: "; return std::move(a); }
  operator A() && { std::cout << "C R-value: "; return std::move(a); }
};

int main() {
  // Non-constant L-values
  B b;
  C c;
  A{b};    // B L-value: A copied
  A{c};    // C L-value: A moved

  // R-values
  A{B{}};  // B R-value: A copied
  A{C{}};  // C R-value: A moved

  // Constant L-values
  const B bc;
  const C cc;
  A{bc};   // B C-value: A copied
  A{cc};   // C C-value: A copied

  return 0;
}

Предположительно, return std::move(some_member); имеет смысл только в том случае, если вы действительно хотите переместить конкретный член класса, например, в случае, когда class C представляет недолговечные объекты адаптера с единственной целью создания экземпляров struct A.

Обратите внимание, как struct Aвсегда копируется изclass B, даже когда class Bобъект является значением R. Это потому, что компилятор не может сказать, чтоclass Bэкземпляр struct Aбольше не будет использоваться. Вclass C, компилятор получает эту информацию из std::move(), вот почему struct Aне получает перемещен, если экземплярclass C постоянно.

Другие вопросы по тегам