C ++ 11 значения и путаница в семантике перемещения (оператор return) - PullRequest
402 голосов
/ 13 февраля 2011

Я пытаюсь понять 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();

Ответы [ 6 ]

527 голосов
/ 13 февраля 2011

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

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

41 голосов
/ 13 февраля 2011

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

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

16 голосов
/ 10 июля 2011

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

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

2 голосов
/ 13 февраля 2011

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

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

0 голосов
/ 06 марта 2019

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...