Что говорит auto &&?

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

auto&& var = foo();

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

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

Ответ 1

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

Вы могли бы сказать, ну почему бы не просто использовать const auto&, потому что это также будет привязано ко всему? Проблема с использованием ссылки 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&& ссылке, чтобы сохранить тот факт, что изначально это значение было lvalue или rvalue, ваш код говорит: Теперь, когда у меня есть ваш объект из lvalue или выражение 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 разорвать свои кишки из-за производительности (избегая копий), когда исходный инициализатор был модифицируемым rvalue.

Что это означает, можем ли мы или когда мы можем украсть ресурсы из var? Ну, так как auto&& будет привязан ко всему, мы не сможем попытаться вырвать var кишки сами по себе - это может быть lvalue или даже const. Тем не менее мы можем std::forward использовать его для других функций, которые могут полностью разрушить его внутренности. Как только мы это сделаем, мы должны рассмотреть var в недопустимом состоянии.

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

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

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

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

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

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

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

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


auto по-прежнему отличается тем, что auto v = {1,2,3}; сделает v a std::initializer_list, а f({1,2,3}) будет дедукцией.

Ответ 3

Рассмотрим некоторый тип T, который имеет конструктор перемещения, и предположим, что

T t( foo() );

использует этот конструктор move.

Теперь позвольте использовать промежуточную ссылку для захвата возврата из 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&& может удалить право собственности от вызывающего (если существует перемещение ctor и т.д.).

Ответ 4

Синтаксис auto && использует две новые возможности С++ 11:

  • Часть auto позволяет компилятору выводить тип на основе контекста (в этом случае возвращаемое значение). Это без каких-либо ссылочных квалификаций (позволяет указать, хотите ли вы T, T & или T && для выведенного типа T).

  • && - новая семантика перемещения. Семантика, поддерживающая тип, реализует конструктор 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.