Почему вывод аргумента шаблона отключен с помощью std:: forward?
В VS2010 std:: forward определяется как таковой:
template<class _Ty> inline
_Ty&& forward(typename identity<_Ty>::type& _Arg)
{ // forward _Arg, given explicitly specified type parameter
return ((_Ty&&)_Arg);
}
identity
, по-видимому, используется исключительно для отключения вывода аргумента шаблона. Какой смысл целенаправленно отключить его в этом случае?
Ответы
Ответ 1
Если вы передаете ссылку rvalue объекту типа X
на функцию шаблона, которая принимает в качестве параметра тип T&&
, вывод аргумента шаблона выводит T
как X
. Поэтому параметр имеет тип X&&
. Если аргумент функции является значением lvalue или const lvalue, компилятор выводит его тип как ссылочную ссылку lvalue или const lvalue этого типа.
Если std::forward
используется вывод аргумента шаблона:
Так как objects with names are lvalues
единственное время std::forward
будет корректно применено к T&&
, это будет когда входной аргумент был неназванным rvalue (например, 7
или func()
). В случае идеальной пересылки arg
вы переходите на std::forward
, это значение lvalue, потому что оно имеет имя. Тип std::forward
будет выводиться как ссылка lvalue или const lvalue. Правила сворачивания ссылок приведут к тому, что T&&
in static_cast<T&&>(arg)
в std:: forward всегда будет использоваться как ссылка lvalue или const lvalue.
Пример:
template<typename T>
T&& forward_with_deduction(T&& obj)
{
return static_cast<T&&>(obj);
}
void test(int&){}
void test(const int&){}
void test(int&&){}
template<typename T>
void perfect_forwarder(T&& obj)
{
test(forward_with_deduction(obj));
}
int main()
{
int x;
const int& y(x);
int&& z = std::move(x);
test(forward_with_deduction(7)); // 7 is an int&&, correctly calls test(int&&)
test(forward_with_deduction(z)); // z is treated as an int&, calls test(int&)
// All the below call test(int&) or test(const int&) because in perfect_forwarder 'obj' is treated as
// an int& or const int& (because it is named) so T in forward_with_deduction is deduced as int&
// or const int&. The T&& in static_cast<T&&>(obj) then collapses to int& or const int& - which is not what
// we want in the bottom two cases.
perfect_forwarder(x);
perfect_forwarder(y);
perfect_forwarder(std::move(x));
perfect_forwarder(std::move(y));
}
Ответ 2
Потому что std::forward(expr)
не полезен. Единственное, что он может сделать, - это запретить операции, т.е. Безошибочно передавать свой аргумент и действовать как функция тождества. Альтернативой будет то, что он такой же, как std::move
, но у нас это уже есть. Другими словами, предполагая, что это возможно, в
template<typename Arg>
void generic_program(Arg&& arg)
{
std::forward(arg);
}
std::forward(arg)
семантически эквивалентен arg
. С другой стороны, std::forward<Arg>(arg)
не является запретом в общем случае.
Таким образом, запрет std::forward(arg)
помогает отлавливать ошибки программиста, и мы ничего не теряем, поскольку любое возможное использование std::forward(arg)
тривиально заменяется на arg
.
Я думаю, вы бы лучше поняли, если бы мы сосредоточились на том, что именно делает std::forward<Arg>(arg)
, а не на том, что сделал бы std::forward(arg)
(поскольку это неинтересный запрет). Давайте попробуем написать шаблон функции no-op, который отлично передает свой аргумент.
template<typename NoopArg>
NoopArg&& noop(NoopArg&& arg)
{ return arg; }
Эта наивная первая попытка не совсем верна. Если мы вызываем noop(0)
то NoopArg
выводится как int
. Это означает, что возвращаемый тип - int&&
и мы не можем связать такую ссылку на rvalue из выражения arg
, которое является lvalue (это имя параметра). Если мы тогда попытаемся:
template<typename NoopArg>
NoopArg&& noop(NoopArg&& arg)
{ return std::move(arg); }
тогда int я = 0; noop(i);
int я = 0; noop(i);
выходит из строя. На этот раз NoopArg
выводится как int&
(правила свертывания ссылок гарантируют, что int& &&
сворачивается в int&
), поэтому возвращаемый тип - int&
, и на этот раз мы не можем связать такую ссылку lvalue из выражения std::move(arg)
который является xvalue.
В контексте функции совершенной пересылки, такой как noop
, иногда мы хотим двигаться, а в других - нет. Правило знать, следует ли нам двигаться, зависит от Arg
: если это не ссылочный тип lvalue, это означает, что noop
был передан rvalue. Если это ссылочный тип lvalue, это означает, что noop
был передан lvalue. Таким образом, в std::forward<NoopArg>(arg)
NoopArg
является необходимым аргументом для std::forward
чтобы шаблон функции делал правильные вещи. Без этого там мало информации. Этот NoopArg
не того типа, который был бы выведен в общем случае для параметра T
std::forward
.
Ответ 3
Изменение: следующее показывает корень моей путаницы. Пожалуйста, объясните, почему он вызывает somefunС# 1 вместо somefunС# 2.
template<typename T> T&& forward(T& x) {
return static_cast<T&&>(x);
}
void somefunc( int& ){} // #1
void somefunc( int&& ){} // #2
template<typename T> void ForwardingFunc(T&& x) {
somefunc(forward(x));
}
int main() {
ForwardingFunc(5);
}
В ForwardingFunc x
всегда считается lvalue при каждом использовании:
somefunc(forward(x));
Объекты с именами всегда являются lvalues.
Это ключевая функция безопасности. Если у вас есть имя для него, вы не хотите неявно переходить от него: было бы слишком легко перейти от него дважды:
foo(x); // no implicit move
bar(x); // else this would probably not do what you intend
При вызове forward
T
выводится как int&
поскольку аргумент x
является lvalue. Итак, вы называете эту специализацию:
template<>
int& forward(int& x)
{
return static_cast<int&>(x);
}
Поскольку T
выводит как int&
и int& &&
сворачивается обратно в int&
.
Поскольку forward(x)
возвращает int&
, вызов somefunc
полностью соответствует перегрузке # 1.