Что auto && говорит нам?


168

Если вы читаете код, как

auto&& var = foo();

где fooлюбая функция, возвращаемая по значению типа T. Тогда varlvalue типа rvalue ссылается на T. Но для чего это нужно var? Значит ли это, что нам разрешено воровать ресурсы var? Существуют ли разумные ситуации, когда вы должны использовать, auto&&чтобы сообщить читателю своего кода что-то подобное, когда вы возвращаете a, unique_ptr<>чтобы сообщить, что у вас есть исключительное право собственности? А что, например, T&&когда Tкласс?

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


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

1
Это даже законно? Я имею в виду, что экземпляр T уничтожается сразу же после fooвозврата, сохраняя значение r в нем, звучит как UB для ne.

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

9
Ссылки @aleguna к временным файлам выполняют продление жизни, как в C ++ 98.
Ecatmur

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

Ответы:


233

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

Вы можете сказать, ну почему бы просто не использовать a, const auto&потому что это также будет связывать что-либо? Проблема с использованием constссылки в том, что это const! Вы не сможете позже связать его с любыми неконстантными ссылками или вызывать любые функции-члены, которые не отмечены const.

В качестве примера представьте, что вы хотите получить a 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, ваш код говорит: Теперь, когда я получил свой объект либо от Lvalue или RValue выражения, я хочу , чтобы сохранить зависимости от того , valueness его первоначально так что я могу использовать его наиболее эффективно - это может сделать его недействительным. Как в:

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внутренности - это вполне может быть lvalue или даже const. Мы можем однако std::forwardэто к другим функциям, которые могут полностью разрушить его внутренности. Как только мы сделаем это, мы должны varбыть в недопустимом состоянии.

Теперь давайте применим это к случаю auto&& var = foo();, как указано в вашем вопросе, где foo возвращает Tзначение по. В этом случае мы точно знаем, что тип varбудет выведен как T&&. Так как мы точно знаем, что это ценность, нам не нужно std::forwardразрешение на кражу его ресурсов. В этом конкретном случае, зная, что fooвозвращается по значению , читатель должен просто прочитать его как: Я беру ссылку на rvalue на возвращаемое временное значение 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>()то прекрасное выражение, которое может быть lvalue или rvalue в зависимости от универсального типа 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

@Asterisk No bcoz T vec можно присвоить lvalue только в случае std :: vector <int &>, и если T - это std :: vector <int>, тогда мы будем использовать вызов по значению, который неэффективен
Kapil

1
Авто & дает мне тот же результат. Я использую MSVC 2015. И GCC выдает ошибку.
Сергей Подобрый

Здесь я использую MSVC 2015, auto & дает те же результаты, что и auto &&.
Kehe CAI

Почему int i; auto && j = i; разрешено, но int i; int && j = i; не является ?
седьмое апреля84

14

Во-первых, я рекомендую прочитать этот мой ответ в качестве дополнительного руководства для пошагового объяснения того, как работает вывод аргумента шаблона для универсальных ссылок.

Значит ли это, что нам разрешено воровать ресурсы var?

Не обязательно. Что, если foo()вдруг вернется ссылка, или вы изменили вызов, но забыли обновить использование var? Или, если вы используете общий код и тип возвращаемого значения foo()может меняться в зависимости от ваших параметров?

Думайте о том, auto&&чтобы быть точно таким же, как T&&в template<class T> void f(T&& v);, потому что это (почти ) именно это. Что вы делаете с универсальными ссылками в функциях, когда вам нужно передать их или использовать каким-либо образом? Вы используете, std::forward<T>(v)чтобы вернуть исходную категорию значения. Если это было lvalue до того, как оно было передано вашей функции, оно остается lvalue после прохождения через него std::forward. Если это было значение rvalue, оно снова станет значением rvalue (помните, что именованная ссылка на rvalue является lvalue).

Итак, как вы используете varправильно в общем виде? Использование std::forward<decltype(var)>(var). Это будет работать точно так же, как std::forward<T>(v)в шаблоне функции выше. Если varэто T&&, вы получите обратно значение, а если это так T&, вы получите обратно значение.

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

Лично я использую auto&&в общем коде, когда мне нужна изменяемая переменная. Идеальная переадресация значения r является модифицирующей, поскольку операция перемещения потенциально крадет у нее внутренности. Если я просто хочу быть ленивым (т. Е. Не записывать имя типа, даже если я его знаю) и мне не нужно изменять (например, при печати элементов диапазона), я буду придерживаться auto const&.


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


В первой части вашего ответа: я имею в виду, что если foo()возвращает тип значения T, то var(это выражение) будет lvalue, а его тип (этого выражения) будет ссылкой на 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&&внутренне, следует сообщить, что он будет использовать семантику перемещения внутри, где это необходимо.

... как вы делаете, когда вы возвращаете unique_ptr <>, чтобы сказать, что у вас есть исключительное право собственности ...

Когда шаблон функции принимает аргумент типа T&&, он говорит, что может переместить объект, который вы передаете. Возвращение unique_ptrявно дает право собственности вызывающей стороне; Принятие T&&может отменить право собственности у вызывающего абонента (если cort существует и т. д.).


2
Я не уверен, что ваш второй пример верен. Разве вам не нужно совершенное перенаправление, чтобы вызвать конструктор перемещения?

3
Это не верно. В обоих случаях конструктор копирования называется, так refи rvrefоба lvalues. Если вам нужен конструктор перемещения, тогда вы должны написать T t(std::move(rvref)).
MWid

Вы имели в виду const ref в своем первом примере auto const &:?
PiotrNycz

@aleguna - вы и MWid правы, спасибо. Я исправил свой ответ.
бесполезно

1
@ Бесполезно Ты прав. Но это не отвечает на мой вопрос. Когда вы используете 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();

поменяет местами внутреннее представление вектора (вектор from fooи пустой вектор from var), поэтому будет быстрее.

Это используется в новом синтаксисе цикла for:

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

Где цикл for хранит auto &&возвращаемое значение fooи itemявляется ссылкой на каждое значение в foo.


Это неверно auto&&не будет ничего перемещать, он просто сделает ссылку. Является ли это ссылкой на lvalue или rvalue, зависит от выражения, использованного для его инициализации.
Джозеф Мэнсфилд

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

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

@MatthieuM. Ты прав. Но я думаю, что в приведенном выше примере копирующий cnstructor никогда не будет вызван, так как все можно переместить.
MWid

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