C ++ 11 переоцінює та змішує зміщення семантики (виписка із повернення)


435

Я намагаюся зрозуміти рецензії на значення і перемістити семантику 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();

50
Будь ласка, не повертайте локальні змінні ніколи за посиланням. Посилання на оцінку все ще є посиланням.
fredoverflow

63
Це, очевидно, було навмисно, щоб зрозуміти смислові відмінності між прикладами lol
Тарантула,

@FredOverflow Старе запитання, але мені знадобилося секунду, щоб зрозуміти ваш коментар. Я думаю, що питання №2 полягало в тому, чи std::move()створено стійку "копію"
3Dave

5
@DavidLively std::move(expression)нічого не створює, він просто кидає вираз xvalue. Жодні об'єкти не копіюються та не переміщуються в процесі оцінювання std::move(expression).
fredoverflow

Відповіді:


562

Перший приклад

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


64
Компілятори будуть RVO, коли ви повернете локальний об'єкт за значенням, а тип локальної та повернення функції однакові, і жоден з них не має кваліфікації cv (не повертайте типи const). Тримайтеся подалі від повернення з умовою (:?), Оскільки це може пригнічувати RVO. Не загортайте локальну в якусь іншу функцію, яка повертає посилання на локальну. Просто return my_local;. Одноразові заяви про повернення в порядку і не гальмують RVO.
Говард Хінант

27
Існує застереження: при поверненні члена локального об’єкта переміщення повинно бути явним.
хлопець

5
@NoSenseEtAl: на зворотному рядку тимчасово не створено. moveне створює тимчасовий. Він кидає значення на xvalue, не створюючи копій, нічого не створюючи, нічого не руйнуючи. Цей приклад є точно такою ж ситуацією, як якщо б ви повернулися за допомогою lvalue-reference та видалили moveз лінії повернення: У будь-якому випадку ви отримали звисаючу посилання на локальну змінну всередині функції, яка була знищена.
Говард Хінант

15
"Оператори з декількома поверненнями в порядку і не стримують RVO": Тільки якщо вони повертають ту саму змінну.
Дедуплікатор

5
@Deduplicator: Ви маєте рацію. Я говорив не так точно, як мав намір. Я мав на увазі, що багаторазові оператори повернення не забороняють компілятор від RVO (навіть якщо це робить неможливим реалізацію), і тому вираз return все ще вважається значущим.
Говард Хіннант

42

Жодна з них не копіює, а друга стосуватиметься знищеного вектора. Іменовані посилання на оцінку майже ніколи не існують у звичайному коді. Ви пишете так, як ви написали б копію на 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і копіюють елізію. А 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 (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

Не відповідь сама по собі , а настанова. Більшу частину часу немає великого сенсу в оголошенні локальної 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 - це ваші рятівники (навіть якщо потенційно більше дорого).


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не є постійним.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.