Почему для пересылки ссылок необходим std :: forward

В таком шаблоне функции

template <typename T>
void foo(T&& x) {
  bar(std::forward<T>(x));
}

Разве x не является ссылкой на rvalue внутри foo, если foo вызывается со ссылкой на rvalue? Если foo вызывается со ссылкой lvalue, приведение в любом случае не требуется, потому что x также будет ссылкой lvalue внутри foo. Также T будет выведен к типу ссылки lvalue, и поэтому std::forward<T> не изменит тип x.

Я провел тест с использованием boost::typeindex и получил точно такие же типы с std::forward<T> и без него.

#include <iostream>
#include <utility>

#include <boost/type_index.hpp>

using std::cout;
using std::endl;

template <typename T> struct __ { };

template <typename T> struct prt_type { };
template <typename T>
std::ostream& operator<<(std::ostream& os, prt_type<T>) {
  os << "\033[1;35m" << boost::typeindex::type_id<T>().pretty_name()
     << "\033[0m";
  return os;
}

template <typename T>
void foo(T&& x) {
  cout << prt_type<__<T>>{} << endl;
  cout << prt_type<__<decltype(x)>>{} << endl;
  cout << prt_type<__<decltype(std::forward<T>(x))>>{} << endl;
  cout << endl;
}

int main(int argc, char* argv[])
{
  foo(1);

  int i = 2;
  foo (i);

  const int j = 3;
  foo(j);

  foo(std::move(i));

  return 0;
}

Результатом g++ -Wall test.cc && ./a.out с gcc 6.2.0 и boost 1.62.0 является

__<int>
__<int&&>
__<int&&>

__<int&>
__<int&>
__<int&>

__<int const&>
__<int const&>
__<int const&>

__<int>
__<int&&>
__<int&&>

Изменить: я нашел этот ответ: https://stackoverflow.com/a/27409428/2640636 Судя по всему,

как только вы даете имя параметру, это lvalue.

Тогда у меня вопрос, почему было выбрано такое поведение вместо сохранения ссылок rvalue как rvalue, даже если им даны имена? Мне кажется, таким образом можно было обойти все невзгоды.

Edit2: я не спрашиваю, что std::forward делает. Я спрашиваю, зачем это нужно.


person SU3    schedule 18.02.2017    source источник


Ответы (4)


Вы действительно не хотите, чтобы ваши параметры автоматически перемещались в вызываемые функции. Рассмотрим эту функцию:

template <typename T>
void foo(T&& x) {
  bar(x);
  baz(x);
  global::y = std::forward<T>(x);
}

Теперь вам действительно не нужен автоматический переход к bar и пустой параметр к baz.

Текущие правила, требующие от вас указывать, следует ли и когда перемещать или продвигать параметр, не случайны.

person Bo Persson    schedule 18.02.2017
comment
Неужели люди действительно чаще пишут такой код, когда что-то происходит со ссылкой rvalue в той же функции до ее перемещения? Я когда-либо работал с объектом только после того, как он был перемещен. Так что мне это кажется исключительным случаем. - person SU3; 19.02.2017
comment
@vsoftco указал в комментарии, который он удалил, что для того, чтобы изменить язык, потребуется anti_forward. Думаю, я бы предпочел это. Есть ли случаи, с которыми этот шаблон не справился? В вашем примере вы могли бы впоследствии вместо этого вызвать bar и baz на global::y. - person SU3; 19.02.2017
comment
@ SU3 Если вероятность того, что что-то случится, ненулевая, то это обязательно когда-нибудь произойдет в какой-нибудь программе. И это будет труднообнаруживаемая ошибка. Комитет, вероятно, подумал (и я того же мнения), что лучше сделать копию, чем делать случайный ход. std::forward уже немного сложно, представьте себе, что у вас есть anti-std :: forward ... люди сходят с ума :) - person vsoftco; 19.02.2017
comment
Есть еще один случай для этой ссылки пересылки, когда T на самом деле является некоторым const U&, а x на самом деле вообще не является rvalue. Использование std::forward<T>(x) будет работать в обоих случаях. Как я сказал в предыдущем комментарии, это было очень тщательно рассмотрено перед тем, как сформулировать правила для T&&. - person Bo Persson; 19.02.2017
comment
@BoPersson, я не думал, что это не было действительно тщательно продумано. Я просто хотел знать, что было принято во внимание. Теперь, с вашей помощью и с помощью vsoftco, я думаю, что понимаю. - person SU3; 19.02.2017

Разве x не является ссылкой на rvalue внутри foo?

Нет, x - это lvalue внутри foo (у него есть имя и адрес) ссылки типа rvalue. Объедините это с правилами сворачивания ссылок и правилами вывода типа шаблона, и вы увидите, что вам нужно std::forward, чтобы получить правильный тип ссылки.

По сути, если то, что вы передаете как x, является lvalue, скажем, int, тогда T выводится как int&. Тогда int && & становится int& (из-за правил сворачивания ссылок), то есть lvalue ref.

С другой стороны, если вы передадите rvalue, скажем 42, тогда T будет выведено как int, поэтому в конце у вас будет int&& как тип x, то есть rvalue. В основном это то, что делает std::forward: приводит к T&& результат, как

static_cast<T&&>(x)

который становится либо T&&, либо T& из-за правил сворачивания ссылок.

Его полезность становится очевидной в универсальном коде, где вы можете не знать заранее, получите ли вы rvalue или lvalue. Если вы не вызываете std::forward, а только f(x), тогда x будет всегда lvalue, поэтому вы потеряете семантику перемещения, когда это необходимо, и можете получить ненужные копии и т. Д.

Простой пример, в котором вы можете увидеть разницу:

#include <iostream>

struct X
{
    X() = default;
    X(X&&) {std::cout << "Moving...\n";};
    X(const X&) {std::cout << "Copying...\n";}
};

template <typename T>
void f1(T&& x)
{
    g(std::forward<T>(x));
}

template <typename T>
void f2(T&& x)
{
    g(x);
}

template <typename T>
void g(T x)
{ }

int main()
{
    X x;
    std::cout << "with std::forward\n";
    f1(X{}); // moving

    std::cout << "without std::forward\n";
    f2(X{}); // copying
}

Live on Coliru

person vsoftco    schedule 18.02.2017
comment
Я понимаю, что std::forward. Я не понимаю зачем ему это нужно. Как вы и сказали, если вы передадите rvalue, скажем int&&, тогда int&& && схлопнется до int&&, но int&& - это то, чего мы не хотим, так что разве мы не готовы? - person SU3; 19.02.2017
comment
Проблема в том, что в универсальном коде вы можете не знать заранее, что вы передаете, поскольку x является rvalue или lvalue. Если вы просто скажете f(x), тогда x всегда будет считаться lvalue. - person vsoftco; 19.02.2017
comment
@ SU3 std::move всегда будет приводить к rvalue, а std::forward будет приводить к rvalue только условно. Итак, why потому, что это преобразование в rvalue иногда должно выполняться. Также - помните, что все параметры функции всегда являются lvalue - потому что у них есть имя. - person marcinj; 19.02.2017

У меня точно такие же типы с std::forward<T> и без

...нет? Ваш собственный вывод доказывает, что вы ошибались:

__<int>    // T
__<int&&>  // decltype(x)
__<int&&>  // std::forward<T>(x)

Без использования std::forward<T> или decltype(x) вы получите int вместо int&&. Это может случайно не "повысить ценность" x - рассмотрим этот пример:

void foo(int&)  { cout << "int&\n"; }
void foo(int&&) { cout << "int&&\n"; }

template <typename T>
void without_forward(T&& x)
{
    foo(x);
//      ^
//  `x` is an lvalue!
}

template <typename T>
void with_forward(T&& x)
{
//  `std::forward` casts `x` to `int&&`.
//      vvvvvvvvvvvvvvvvvv
    foo(std::forward<T>(x));
//                      ^
//          `x` is an lvalue!
}

template <typename T>
void with_decltype_cast(T&& x)
{
// `decltype(x)` is `int&&`. `x` is casted to `int&&`.
//      vvvvvvvvvvv
    foo(decltype(x)(x));
//                  ^
//          `x` is an lvalue!
}

int main()
{
    without_forward(1);    // prints "int&"
    with_forward(1);       // prints "int&&"
    with_decltype_cast(1); // prints "int&&"
}

Пример палочки

person Vittorio Romeo    schedule 18.02.2017
comment
Я не понимаю. Как мой результат доказывает, что я ошибаюсь? int - это то, что я получаю за T. Тип x не обязательно совпадает с типом T. Меня смущает то, что decltype(x) это int&&, но, как в вашем примере, foo(x) выбирает перегрузку foo(int&). decltype(x) тоже не обязательно является типом x? - person SU3; 19.02.2017
comment
decltype(x) - это объявленный тип x, то есть int&&. Использование x без forward или без приведения приводит к выражению lvalue, вызывающему foo(int&). - person Vittorio Romeo; 19.02.2017
comment
@ SU3: обновил свой ответ, чтобы показать decltype(x) пример приведения. - person Vittorio Romeo; 19.02.2017
comment
Также добавлены встроенные комментарии - дайте мне знать, если вы все еще не понимаете - person Vittorio Romeo; 19.02.2017
comment
Хорошо, я могу взять * использование x без каких-либо результатов приведения, чтобы lvalue"* as an axiom. Then the behavior is as expected. But why was this the chosen design for the language. Would anything break if x` сохранял свой статус как rvalue в таких случаях? - person SU3; 19.02.2017
comment
@ SU3: У меня нет на это хорошего ответа - я тоже думаю, что для этого нужен отдельный вопрос. - person Vittorio Romeo; 19.02.2017
comment
Правильно. Теперь я вижу, что сформулировал свой вопрос недостаточно точно, поэтому дискуссия сбилась с пути в сторону того, как все работает, а не почему они работают именно так. Я намеревался обсудить позже с самого начала. - person SU3; 19.02.2017
comment
Параметры @ SU3 всегда считаются l-значениями (они названы и имеют адрес). В противном случае подумайте, что произойдет с ситуациями типа f(x){ x++;}, когда вы вызовете f(42): вы не сможете изменить x внутри (не можете присвоить lvalue, поэтому x++ не будет компилироваться). Это то, чего большинство из нас не хочет в языке программирования. Во-вторых, как сказал @Bo Persson, вы не хотите случайных движений, они довольно опасны ... представьте себе, что вы внезапно просыпаетесь в другом доме, вы не захотите этого, не так ли? (пока не...) - person vsoftco; 19.02.2017
comment
@vsoftco Понятно. Я думал, что все было так, что я бы устроился в другом доме, если бы не был осторожен. - person SU3; 19.02.2017
comment
@ SU3 Так бывает в реальной жизни. В программном обеспечении у нас есть некоторый контроль :) - person vsoftco; 19.02.2017

x быть r-значением НЕ то же самое, что x иметь тип ссылки на r-значение.

R-значение - это свойство выражения, тогда как r-value-reference - это свойство его типа.

Если вы на самом деле пытаетесь передать переменную, которая является ссылкой на r-значение, в функцию, она обрабатывается как l-значение. decltype вводит вас в заблуждение. Попробуйте и убедитесь:

#include <iostream>
#include <typeinfo>
using namespace std;

template<class T> struct wrap { };

template<class T>
void bar(T &&value) { std::cout << " vs. " << typeid(wrap<T>).name() << std::endl; }

template<class T>
void foo(T &&value) { std::cout << typeid(wrap<T>).name(); return bar(value); }

int main()
{
    int i = 1;
    foo(static_cast<int &>(i));
    foo(static_cast<int const &>(i));
    foo(static_cast<int &&>(i));
    foo(static_cast<int const &&>(i));
}

Вывод:

4wrapIRiE vs. 4wrapIRiE
4wrapIRKiE vs. 4wrapIRKiE
4wrapIiE vs. 4wrapIRiE (они должны совпадать!)
4wrapIKiE vs. 4wrapIRKiE (они должны совпадать!)

person user541686    schedule 18.02.2017
comment
Не связано: вы можете отфильтровать с помощью ./a.out | c++filt (при использовании UNIX / Linux, как это выглядит, как вы), чтобы получить хороший (деискаженный) вывод с именем типа. - person vsoftco; 19.02.2017