Почему вывод аргумента шаблона отключен с помощью 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.