Що нам говорить авто &&?


168

Якщо ви читаєте код, як

auto&& var = foo();

де fooбудь-яка функція, що повертається за значенням типу T. Тоді varє значення посилання на значення rvalue типу T. Але що це означає var? Це означає, що нам дозволено красти ресурси var? Чи є якісь обґрунтовані ситуації, коли вам слід, auto&&щоб повернути читачеві свого коду щось подібне, коли ви робите, коли ви повертаєтесь, unique_ptr<>щоб сказати, що у вас є ексклюзивне право власності? А що, наприклад , T&&коли Tмає тип класу?

Я просто хочу зрозуміти, чи є інші випадки використання, auto&&ніж ті, що є у програмуванні шаблонів; як ті, що обговорювалися в прикладах цієї статті Універсальні посилання Скотта Майєрса.


1
Мені було цікаво те саме. Я розумію , як працює тип вирахування, але що мій код кажучи , коли я використовую auto&&? Я думав розглянути, чому діапазон для циклу розширюється, щоб використовувати його auto&&як приклад, але не обійшов його. Можливо, хто відповість, може пояснити це.
Джозеф Менсфілд

1
Це навіть законно? Я маю на увазі, що instnce T знищується негайно після fooповернення, зберігаючи rvalue ref, це звучить як UB to ne.

1
@aleguna Це абсолютно законно. Я не хочу повертати посилання чи покажчик на локальну змінну, а значення. Функція fooможе, наприклад , виглядати наступним чином : int foo(){return 1;}.
MWid

9
Посилання @aleguna на тимчасові компанії продовжують тривалість життя, як і в C ++ 98.
ecatmur

5
@aleguna продовження терміну служби працює лише з місцевими часописами, не функціонує повернення посилань. Див stackoverflow.com/a/2784304/567292
ecatmur

Відповіді:


232

Використовуючи, auto&& var = <initializer>ви говорите: я прийму будь-який ініціалізатор незалежно від того, чи це вираз lvalue чи rvalue, і я збережу його чіткість . Зазвичай це використовується для переадресації (як правило, з T&&). Причина цього працює в тому, що «універсальна посилання», auto&&або T&&, буде прив’язана до чого завгодно .

Ви можете сказати: ну чому б не просто використовувати a, const auto&тому що це також буде пов'язане з чим-небудь? Проблема використання constпосилання полягає в тому, що це const! Пізніше ви не зможете прив’язати його до будь-яких посилань, що не стосуються const, або викликати будь-які функції учасника, які не позначені const.

Як приклад, уявіть, що ви хочете отримати std::vector, візьміть ітератор до його першого елемента та змініть значення, на яке вказує цей ітератор, певним чином:

auto&& vec = some_expression_that_may_be_rvalue_or_lvalue;
auto i = std::begin(vec);
(*i)++;

Цей код складеться чудово незалежно від виразу ініціалізатора. Альтернативи auto&&відмовити наступними способами:

auto         => will copy the vector, but we wanted a reference
auto&        => will only bind to modifiable lvalues
const auto&  => will bind to anything but make it const, giving us const_iterator
const auto&& => will bind only to rvalues

Тож для цього auto&&відмінно працює! Приклад такого використання auto&&- це forцикл на основі діапазону . Дивіться моє інше питання для більш детальної інформації.

Якщо потім ви використовуєте std::forwardу своєму auto&&посиланні, щоб зберегти той факт, що він спочатку був або значенням, або рівнем, ваш код говорить: Тепер, коли я отримав ваш об'єкт або з вираження львалю, або з rvalue, я хочу зберегти будь-яку цінність його спочатку було, тому я можу використовувати його найбільш ефективно - це може визнати його недійсним. А саме:

auto&& var = some_expression_that_may_be_rvalue_or_lvalue;
// var was initialized with either an lvalue or rvalue, but var itself
// is an lvalue because named rvalues are lvalues
use_it_elsewhere(std::forward<decltype(var)>(var));

Це дозволяє use_it_elsewhereвирвати свої нутри заради продуктивності (уникаючи копій), коли оригінальний ініціалізатор міняв оцінку.

Що це означає, чи можемо ми, або коли можемо вкрасти ресурси var? Отже, оскільки auto&&воля пов'язуватиме з чим завгодно, ми не можемо самі спробувати вирвати varкишки - це може бути цілком вагомим чи навіть const. Однак ми можемо std::forwardце зробити для інших функцій, які можуть повністю розрушити його нутрощі. Як тільки ми це зробимо, ми повинні вважати varсебе недійсним.

Тепер давайте застосуємо це до випадку auto&& var = foo();, як зазначено у вашому запитанні, де foo повертає Tзначення a . У цьому випадку ми точно знаємо, що тип varбуде виведено як T&&. Оскільки ми точно знаємо, що це реальне значення, нам не потрібен std::forwardдозвіл на крадіжку його ресурсів. У цьому конкретному випадку, знаючи, що fooповертається за значенням , читач повинен просто читати це як: Я беру рецензуючу посилання на тимчасове повернене з foo, щоб я міг із задоволенням перейти з нього.


Як додаток, я думаю, що варто згадати, коли some_expression_that_may_be_rvalue_or_lvalueможе з'явитися такий вираз, як "ситуація, що може змінити ваш код". Ось ось надуманий приклад:

std::vector<int> global_vec{1, 2, 3, 4};

template <typename T>
T get_vector()
{
  return global_vec;
}

template <typename T>
void foo()
{
  auto&& vec = get_vector<T>();
  auto i = std::begin(vec);
  (*i)++;
  std::cout << vec[0] << std::endl;
}

Ось, get_vector<T>()це прекрасний вираз, який може бути або значенням, або рівнем, залежно від родового типу T. Ми по суті змінюємо тип повернення get_vectorчерез параметр шаблону foo.

Коли ми зателефонуємо foo<std::vector<int>>, get_vectorповернеться global_vecза значенням, що дає вираження rvalue. Крім того, коли ми зателефонуємо foo<std::vector<int>&>, get_vectorповернемося global_vecза посиланням, що призведе до виразу lvalue.

Якщо ми робимо:

foo<std::vector<int>>();
std::cout << global_vec[0] << std::endl;
foo<std::vector<int>&>();
std::cout << global_vec[0] << std::endl;

Як очікується, ми отримуємо такий результат:

2
1
2
2

Якщо ви повинні були змінити auto&&в коді будь-який з auto, auto&, const auto&або const auto&&ми не отримаємо результат , який ми хочемо.


Альтернативний спосіб зміни логіки програми, заснований на тому, чи auto&&ініціюється ваша посилання виразом lvalue або rvalue, - використовувати риси типу:

if (std::is_lvalue_reference<decltype(var)>::value) {
  // var was initialised with an lvalue expression
} else if (std::is_rvalue_reference<decltype(var)>::value) {
  // var was initialised with an rvalue expression
}

2
Чи не можемо ми просто сказати, що T vec = get_vector<T>();всередині функції foo? Або я спрощую це до абсурдного рівня :)
Зірочка

@Asterisk No bcoz T vec може бути призначений lvalue лише у випадку std :: vector <int &> і якщо T std :: vector <int>, тоді ми будемо використовувати виклик за значенням, яке є неефективним
Kapil

1
auto & дає мені такий же результат. Я використовую MSVC 2015. І GCC видає помилку.
Сергій Подобрий

Тут я використовую MSVC 2015, автоматичне & дає ті ж результати, що й у програмі auto &&.
Kehe CAI

Чому int i; auto && j = i; дозволено, але int i; int && j = i; не ?
SeventhSon84

14

По-перше, я рекомендую прочитати цю мою відповідь як сторонне читання для покрокового пояснення того, як працює виведення аргументів шаблону для універсальних посилань.

Це означає, що нам дозволено красти ресурси var?

Не обов'язково. Що робити, якщо foo()раптом повернулася посилання, або ви змінили дзвінок, але забули оновити використання var? Або якщо ви маєте загальний код і тип повернення foo()може змінюватися залежно від ваших параметрів?

Подумайте, auto&&що це точно так само, як і T&&в template<class T> void f(T&& v);, тому що це (майже ) саме це. Що ви робите з універсальними посиланнями у функціях, коли вам потрібно передавати їх чи будь-яким чином використовувати їх? Ви використовуєте std::forward<T>(v)для повернення початкової категорії цінностей. Якщо це було рівне значення перед тим, як перейти до вашої функції, воно залишається значенням після його проходження std::forward. Якщо це було rvalue, воно знову стане rvalue (пам’ятайте, названа посилання rvalue - це lvalue).

Отже, як ви varправильно користуєтесь загальним способом? Використовуйте std::forward<decltype(var)>(var). Це буде працювати точно так само, як std::forward<T>(v)у шаблоні функцій вище. Якщо varце a T&&, ви отримаєте реванш назад, а якщо він є T&, ви отримаєте назад значення.

Отже, повернемось до теми: Що нам auto&& v = f();і std::forward<decltype(v)>(v)в кодовій базі говорять? Вони кажуть нам, що vбуде придбано та передано найефективнішим чином. Пам’ятайте, проте, що після перенаправлення такої змінної, можливо, вона перенесена, тому невірно використовувати її далі, не скидаючи її.

Особисто я використовую auto&&в загальному коді, коли мені потрібна змінна змінна. Ідеальне переадресація rvalue є модифікуючим, оскільки операція переміщення потенційно вкрадає його кишки. Якщо я просто хочу бути лінивим (тобто не писати ім'я типу, навіть якщо я це знаю) і не потрібно змінювати (наприклад, коли просто друкують елементи діапазону), я буду дотримуватися цього auto const&.


autoв настільки сильно відрізняється , що auto v = {1,2,3};зробить , в той час як буде провал відрахування.vstd::initializer_listf({1,2,3})


У першій частині вашої відповіді: я маю на увазі, що якщо foo()поверне значення типу T, то var(цей вираз) буде рівнем значення, а його тип (цього виразу) буде посиланням на rvalue T(тобто T&&).
MWid

@MWid: Має сенс, вилучив першу частину.
Xeo

3

Розглянемо якийсь тип, Tякий має конструктор переміщення, і припустимо

T t( foo() );

використовує цей конструктор переміщення.

Тепер давайте скористаємося проміжною посиланням для отримання повернення від foo:

auto const &ref = foo();

це виключає використання конструктора переміщення, тому повернене значення доведеться скопіювати замість переміщеного (навіть якщо ми використовуємо std::moveтут, ми фактично не можемо переміститися через const ref)

T t(std::move(ref));   // invokes T::T(T const&)

Однак, якщо ми використовуємо

auto &&rvref = foo();
// ...
T t(std::move(rvref)); // invokes T::T(T &&)

конструктор переміщення все ще доступний.


А для вирішення інших ваших питань:

... Чи існують обґрунтовані ситуації, коли вам слід скористатися функцією auto &&, щоб щось повідомити читачеві про свій код ...

Перше, як каже Xeo, - це, по суті, я проходжу X якомога ефективніше , незалежно від типу X. Отже, бачачи код, який використовує auto&&внутрішньо, слід повідомляти, що він використовує внутрішню семантику переміщення, де це доречно.

... як ви робите, коли повертаєте унікальний_ptr <>, щоб сказати, що ви маєте ексклюзивне право власності ...

Коли шаблон функції приймає аргумент типу T&&, він говорить, що він може переміщувати об'єкт, який ви передаєте . Повернення unique_ptrявно надає право власності абоненту; прийняття T&&може видалити право власності з абонента (якщо існує переїзд ctor тощо).


2
Я не впевнений, що ваш другий приклад є дійсним. Чи не потрібно вам ідеального переадресації, щоб викликати конструктор переміщення?

3
Це неправильно. В обох випадках конструктор копіювання називається, так refі rvrefобидва lvalues. Якщо ви хочете конструктор переміщення, то вам доведеться писати T t(std::move(rvref)).
MWid

Ви мали на увазі const ref у своєму першому прикладі auto const &:?
PiotrNycz

@aleguna - ви і MWid праві, дякую. Я виправив свою відповідь.
Марно

1
@Useless Ви маєте рацію. Але це не відповідає моєму питанню. Коли ви використовуєте auto&&і що ви повідомляєте читачеві свого коду, використовуючи auto&&?
MWid

-3

У auto &&синтаксисі використовуються дві нові функції C ++ 11:

  1. autoЧастина дозволяє компілятор виводить типу на основі контексту (значення, що повертається в даному випадку). Це не має жодної довідкової кваліфікації (дозволяє вказати, чи ви хочете T, T &чи T &&для виведеного типу T).

  2. Це &&нова семантика ходу. Тип семантики, що підтримує рух, реалізує конструктор, T(T && other)який оптимально переміщує вміст у новий тип. Це дозволяє об'єкту міняти внутрішнє представлення замість виконання глибокої копії.

Це дозволяє мати щось на кшталт:

std::vector<std::string> foo();

Так:

auto var = foo();

виконає копію повернутого вектора (дорого), але:

auto &&var = foo();

зміниться внутрішнім поданням вектора (вектор з fooі порожній вектор з var), так буде швидше.

Це використовується в новому синтаксисі for-loop:

for (auto &item : foo())
    std::cout << item << std::endl;

Якщо цикл for-loop містить auto &&значення повернення від fooі itemє посиланням на кожне значення в foo.


Це неправильно. auto&&не рухатиметься нічого, це просто зробить посилання. Будь то посилання на значення або rvalue залежить від виразу, який використовується для його ініціалізації.
Джозеф Менсфілд

В обох випадках конструктор переміщення буде викликаний, оскільки std::vectorі std::stringє рухомконструктивним. Це не має нічого спільного з типом var.
MWid

1
@MWid: Насправді, виклик конструктора копіювання / переміщення також може бути повністю відхилений RVO.
Матьє М.

@MatthieuM. Ти правий. Але я думаю, що у наведеному вище прикладі копіювальний інструмент копіювання ніколи не зателефонує, оскільки все є рухомим.
MWid

1
@MWid: моя думка полягала в тому, що навіть конструктор переміщення може бути відхилений. Елісіон козирі рухаються (це дешевше).
Матьє М.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.