Если вы читаете код, как
auto&& var = foo();
где foo
любая функция, возвращаемая по значению типа T
, затем var
является lvalue типа rvalue ссылки на T
, Но что это значит для var
? Означает ли это, что мы можем украсть ресурсы var
? Есть ли разумные ситуации, когда вы должны использовать auto&&
сказать читателю вашего кода что-то подобное, когда вы возвращаете unique_ptr<>
сказать, что у вас есть эксклюзивная собственность? А как насчет например T&&
когда T
имеет тип класса?
Я просто хочу понять, есть ли другие случаи использования auto&&
чем те, в шаблонном программировании; как те, которые обсуждались в примерах в этой статье Универсальные ссылки Скотт Мейерс.
Используя 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
вырвать его внутренности ради производительности (избегая копий), когда исходный инициализатор был изменяемым значением.
Что это означает относительно того, можем ли мы или когда мы можем украсть ресурсы у var
? Ну так как auto&&
будет привязан к чему-либо, мы не можем попытаться вырвать var
s s внутренности — это вполне может быть lvalue или даже const. Мы можем однако std::forward
это к другим функциям, которые могут полностью разрушить его внутренности. Как только мы сделаем это, мы должны рассмотреть var
находиться в недопустимом состоянии.
Теперь давайте применим это к случаю auto&& var = foo();
, как указано в вашем вопросе, где foo возвращает T
по значению. В этом случае мы точно знаем, что тип 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>()
это то прекрасное выражение, которое может быть 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
}
Во-первых, я рекомендую прочитать этот мой ответ как подробное описание пошагового объяснения того, как работает вывод аргумента шаблона для универсальных ссылок.
Означает ли это, что мы можем украсть ресурсы
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&
Вы получите обратно lvalue.
Итак, вернемся к теме: что делать auto&& v = f();
а также std::forward<decltype(v)>(v)
в кодовой базе скажите нам? Они говорят нам, что v
будут приобретены и переданы наиболее эффективным способом. Помните, однако, что после пересылки такой переменной, возможно, что она перемещена, поэтому было бы неправильно использовать ее дальше без сброса.
Лично я пользуюсь auto&&
в общем коде, когда мне нужно modifyable переменная. Совершенная пересылка значения r является изменяющей, так как операция перемещения потенциально может похитить его. Если я просто хочу быть ленивым (то есть не пишу имя типа, даже если я его знаю) и не нуждаюсь в изменении (например, при печати только элементов диапазона), я буду придерживаться auto const&
,
† auto
настолько отличается, что auto v = {1,2,3};
сделаю v
std::initializer_list
, в то время как f({1,2,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 &&)
Конструктор перемещения все еще доступен.
И чтобы ответить на ваши другие вопросы:
… Есть ли разумные ситуации, когда вы должны использовать авто&& рассказать читателю вашего кода что-то …
Первое, что, как говорит Xeo, это Я прохожу X максимально эффективно, какой бы тип X не был. Итак, увидев код, который использует auto&&
внутренне следует сообщить, что она будет использовать семантику перемещения внутри, где это уместно.
… как вы делаете, когда вы возвращаете unique_ptr<> Сказать, что у вас есть исключительная собственность …
Когда шаблон функции принимает аргумент типа T&&
, он говорит, что может переместить объект, который вы передаете. Возвращение unique_ptr
явно дает право собственности вызывающей стороне; принимающий T&&
может Удалить владение от вызывающего абонента (если cort существует и т. д.).
auto &&
синтаксис использует две новые функции C ++ 11:
auto
part позволяет компилятору определять тип на основе контекста (в данном случае возвращаемого значения). Это без каких-либо справочных квалификаций (позволяя вам указать, хотите ли вы T
, T &
или же T &&
для выведенного типа T
).
&&
это новый ход семантики. Тип, поддерживающий семантику перемещения, реализует конструктор T(T && other)
это оптимально перемещает контент в новом типе. Это позволяет объекту поменять местами внутреннее представление вместо глубокого копирования.
Это позволяет вам иметь что-то вроде:
std::vector<std::string> foo();
Так:
auto var = foo();
выполнит копию возвращенного вектора (дорого), но:
auto &&var = foo();
поменяет внутреннее представление вектора (вектор из foo
и пустой вектор из var
), так будет быстрее.
Это используется в новом синтаксисе цикла for:
for (auto &item : foo())
std::cout << item << std::endl;
Где цикл for держит auto &&
на возвращаемое значение из foo
а также item
ссылка на каждое значение в foo
,