Почему вывод аргументов шаблона отключен с помощью std :: forward? - PullRequest
22 голосов
/ 15 октября 2011

В 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, по-видимому, используется исключительно для отключения вывода аргумента шаблона.Какой смысл целенаправленно отключать его в этом случае?

Ответы [ 3 ]

21 голосов
/ 14 января 2012

Если вы передаете rvalue-ссылку на объект типа X в шаблонную функцию, которая принимает тип T&& в качестве своего параметра, вычет аргумента шаблона выводит T в X. Следовательно, параметр имеет тип X&&. Если аргумент функции является lvalue или const lvalue, компилятор определяет его тип как ссылку lvalue или ссылку const lvalue этого типа.

Если std::forward используется вычет аргумента шаблона:

Поскольку objects with names are lvalues единственный раз, когда std::forward будет правильно приведен к T&&, будет, когда входной аргумент будет безымянным значением (например, 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));
}
17 голосов
/ 15 октября 2011

Потому что 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 i = 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, который будет выведен в общем случае.

7 голосов
/ 16 октября 2011

Редактировать: следующее показывает корень моей путаницы.Пожалуйста, объясните, почему он вызывает somefunc # 1 вместо somefunc # 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);
}

In 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.

...