C ++ 11 значения и путаница в семантике перемещения (оператор return)

435

Я пытаюсь понять 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();
тарантул
источник
51
Пожалуйста, никогда не возвращайте локальные переменные по ссылке. Ссылка rvalue по-прежнему является ссылкой.
fredoverflow
63
Это было очевидно намеренно, чтобы понять смысловые различия между примерами. LOL
Тарантул
@FredOverflow Старый вопрос, но мне потребовалась секунда, чтобы понять ваш комментарий. Я думаю, что вопрос с № 2 заключался в том, была ли std::move()создана постоянная «копия».
3Dave
5
@DavidLively std::move(expression)ничего не создает, просто переводит выражение в xvalue. Никакие объекты не копируются или перемещаются в процессе оценки std::move(expression).
fredoverflow

Ответы:

564

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

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ненужно и может быть на самом деле производительность pessimization , как это будет препятствовать оптимизации возвращаемого значения.

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

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

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

Говард Хиннант
источник
65
Компиляторы будут возвращать RVO, когда вы возвращаете локальный объект по значению, а тип local и возврат функции совпадают, и ни один из них не является cv-квалифицированным (не возвращает const-типы). Держитесь подальше от возврата с условием (:?), Так как это может помешать RVO. Не включайте local в какую-то другую функцию, которая возвращает ссылку на local. Просто return my_local;. Многократные операторы возврата в порядке и не будут препятствовать RVO.
Говард Хиннант
27
Есть предостережение: при возврате члена локального объекта перемещение должно быть явным.
мальчик
5
@NoSenseEtAl: В обратной строке не создано временных данных. moveне создает временный. Он переводит lvalue в xvalue, не делая копий, ничего не создавая, ничего не разрушая. Этот пример точно такой же, как если бы вы вернулись с помощью lvalue-reference и удалили его moveиз строки возврата: в любом случае у вас есть свисающая ссылка на локальную переменную внутри функции, которая была разрушена.
Говард Хиннант,
15
«Несколько операторов возврата в порядке и не будут препятствовать RVO»: только если они возвращают одну и ту же переменную.
дедупликатор
5
@ Дедупликатор: Вы правы. Я говорил не так точно, как хотел. Я имел в виду, что множественные операторы возврата не запрещают компилятору RVO (даже если это делает невозможным его реализацию), и поэтому выражение возврата по-прежнему считается значением.
Говард Хиннант,
42

Ни один из них не скопирует, но второй будет ссылаться на уничтоженный вектор. Именованные 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 в подавляющем большинстве случаев.

щенок
источник
Вы действительно уверены, что третий пример собирается сделать векторное копирование?
Тарантул
@Tarantula: это разрушит ваш вектор. На самом деле, не имеет значения, копировала ли она или не копировала до взлома.
Щенок
4
Я не вижу причин, по которым вы предлагаете разорение. Прекрасно связать локальную ссылочную переменную rvalue с rvalue. В этом случае время жизни временного объекта увеличивается до времени жизни ссылочной переменной rvalue.
fredoverflow
1
Просто пояснение, так как я учусь этому. В этом новом примере, вектор tmpне перемещается в rval_ref, но записываются непосредственно в rval_refиспользовании РВО (т.е. скопировать Пропуска). Существует различие между std::moveкопией elision. А std::moveможет все еще включать некоторые данные, которые будут скопированы; в случае вектора новый конструктор фактически создается в конструкторе копирования, и данные выделяются, но основная часть массива данных копируется только путем копирования указателя (по существу). Исключение копий позволяет избежать 100% всех копий.
Марк Лаката
@MarkLakata Это NRVO, а не RVO. NRVO является необязательным, даже в C ++ 17. Если он не применяется, возвращаемое значение и rval_refпеременные создаются с помощью конструктора перемещения std::vector. Нет никакого конструктора копирования, связанного как с / без std::move. tmpрассматривается как RValue в returnзаявлении в этом случае.
Даниэль Лэнгр
16

Простой ответ заключается в том, что вы должны написать код для 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);
Zoner
источник
3

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

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

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

Красный XIII
источник
2

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

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

Эдвард Стрендж
источник
1

Как уже упоминалось в комментариях к первому ответу, 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не является постоянным.

Андрей Подзимек
источник