Что говорит 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
.