Могу ли я обычно / всегда использовать std :: forward вместо std :: move?

Я смотрел Скотта Мейерса поговорим об универсальных справочниках с конференции C ++ and Beyond 2012, и пока все имеет смысл. Однако примерно через 50 минут один из зрителей задает вопрос, который меня тоже интересовал. Мейерс говорит, что его не волнует ответ, потому что он не-идиоматичен и может ввести его в заблуждение, но мне все равно интересно.

Представленный код выглядит следующим образом:

// Typical function bodies with overloading:
void doWork(const Widget& param)   // copy
{
  // ops and exprs using param
}
void doWork(Widget&& param)        // move
{
  // ops and exprs using std::move(param)
}

// Typical function implementations with universal reference:
template <typename T>
void doWork(T&& param)             // forward => copy and move
{
  // ops and exprs using std::forward<T>(param)
}

Дело в том, что когда мы берем ссылку на rvalue, мы знаем, что у нас есть rvalue, поэтому мы должны std::move сохранить тот факт, что это rvalue. Когда мы берем универсальную ссылку (T&&, где T - выведенный тип), мы хотим, чтобы std::forward сохранил тот факт, что это могло быть lvalue или rvalue.

Итак, вопрос: поскольку std::forward сохраняет, было ли значение, переданное в функцию, lvalue или rvalue, а std::move просто приводит свой аргумент к rvalue, можем ли мы просто использовать std::forward везде? Будет ли std::forward вести себя как std::move во всех случаях, когда мы будем использовать std::move, или есть какие-то важные различия в поведении, которые не учитываются обобщением Мейерса?

Я не предлагаю, чтобы кто-то это делал, потому что, как правильно говорит Мейерс, это совершенно не идиоматично, но следующее также допустимое использование std::move:

void doWork(Widget&& param)         // move
{
  // ops and exprs using std::forward<Widget>(param)
}

person Joseph Mansfield    schedule 04.11.2012    source источник
comment
move (lvalue) преобразует параметр в rvalue. forward (lvalue) оставляет его как lvalue.   -  person Andrew Tomazos    schedule 04.11.2012
comment
@ AndrewTomazos-Fathomling std::forward(lvalue_expression) выдаст lvalue, если тип lvalue_expression является lvalue, и даст rvalue, если тип lvalue_expression является rvalue (например, в случае именованного rvalue). Здесь я использую std::forward для выражения, которое, как мне известно, имеет тип rvalue.   -  person Joseph Mansfield    schedule 04.11.2012
comment
@Andrew Tomazos - Fathomling - это ложь, весь смысл std::forward в том, что он иногда может изменять lvalue на rvalue.   -  person Mankarse    schedule 04.11.2012
comment
Терминология немного неоднозначная. когда я говорю вперед (lvalue), я имею в виду параметр, который имеет выведенный тип T & через свертывание ссылок.   -  person Andrew Tomazos    schedule 04.11.2012
comment
comment
@ BЈовић Не думаю, что это дубликат. Мы говорим об одном и том же разговоре и о той же теме, но задаем разные вопросы.   -  person Joseph Mansfield    schedule 05.11.2012
comment
Ответ Скотта Мейерса.   -  person Joseph Mansfield    schedule 12.11.2012


Ответы (2)


Это два очень разных и взаимодополняющих инструмента.

  • std::move выводит аргумент и безусловно создает выражение rvalue. Это имеет смысл применить к реальному объекту или переменной.

  • std::forward принимает обязательный аргумент шаблона (вы должны указать это!) И волшебным образом создает выражение lvalue или rvalue в зависимости от того, каким был тип (посредством добавления && и правил сворачивания). Это имеет смысл применять только к выведенному, шаблонному аргументу функции.

Возможно, следующие примеры лучше иллюстрируют это:

#include <utility>
#include <memory>
#include <vector>
#include "foo.hpp"

std::vector<std::unique_ptr<Foo>> v;

template <typename T, typename ...Args>
std::unique_ptr<T> make_unique(Args &&... args)
{
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));  // #1
}

int main()
{
    {
        std::unique_ptr<Foo> p(new Foo('a', true, Bar(1,2,3)));
        v.push_back(std::move(p));                                  // #2
    }

    {
        v.push_back(make_unique<Foo>('b', false, Bar(5,6,7)));      // #3
    }

    {
        Bar b(4,5,6);
        char c = 'x';
        v.push_back(make_unique<Foo>(c, b.ready(), b));             // #4
    }
}

В ситуации № 2 у нас есть существующий конкретный объект p, и мы безоговорочно хотим от него уйти. Только std::move имеет смысл. Здесь нечего "пересылать". У нас есть именованная переменная, и мы хотим от нее перейти.

С другой стороны, ситуация № 1 принимает список любых аргументов, и каждый аргумент должен быть перенаправлен в той же категории значений, что и в исходном вызове. Например, в № 3 аргументы являются временными выражениями, и поэтому они будут пересылаться как rvalue. Но мы также могли бы смешать именованные объекты в вызове конструктора, как в ситуации №4, и тогда нам потребуется пересылка как lvalue.

person Kerrek SB    schedule 04.11.2012
comment
Как бы я ни не хотел, я должен дать +1 за это, потому что я бы ответил точно так же. Черт побери, если так будет продолжаться, ты действительно получишь 4-й золотой значок C ++ 11. :П - person Xeo; 04.11.2012
comment
Эй, у тебя его еще нет. :) Вам нужно еще несколько голосов в теге, и я все еще надеюсь получить еще несколько ответов, прежде чем вы получите эти голоса. :П - person Xeo; 05.11.2012
comment
зачем кому-то перемещать unique_ptr? Я что-то упускаю? Разве приведенный выше пример не пытается переместить хранилище unique_ptr? Почему это имеет смысл? - person Gabriel; 11.12.2013
comment
@Gabriel: Без движения ничего бы не вышло. std::unique_ptr нельзя скопировать. - person Ben Voigt; 09.10.2014
comment
@Gabriel: Вы неправильно поняли: вы всегда перемещаете ресурс handle, а не сам ресурс. Ресурсы по своему определению являются неподвижными объектами; движимое - это ресурс право собственности. - person Kerrek SB; 09.10.2014
comment
Спасибо за разъяснение, это было много лет назад, когда я еще не был так уверен в ссылках на rvalue и так далее! - person Gabriel; 10.10.2014
comment
Стоит отметить, что std :: forward имеет смысл в случае, когда ссылка rvalue на самом деле является ссылкой пересылки. В противном случае, forward ‹T› не будет пересылать предоставленный аргумент, но переместит его, даже если T является выведенным параметром шаблона. Небезопасно использовать forward ‹T› для параметров, не объявленных как T &&. - person Григорий Шуренк&; 10.01.2015
comment
@KerrekSB ... и волшебным образом создает ссылку lvalue или выражение rvalue в зависимости от того, какой был тип .... Это должно быть исправлено, потому что это неверно. Выражение rvalue - это не тип, это категория значений. Вы, вероятно, хотели сказать ссылку rvalue (а не выражение). - person KeyC0de; 01.10.2018
comment
@ Nik-Lz: Я думаю, это нужно исправить, чтобы создать выражение lvalue или rvalue - результат вызова функции - это выражение, а не тип, верно? - person Kerrek SB; 03.10.2018
comment
@KerrekSB Да, конечно. Это правильно. У выражения есть: 1. тип, 2. категория значения. Но да, то, что вы говорите, правильно. - person KeyC0de; 03.10.2018

Да, если param является Widget&&, тогда следующие три выражения эквивалентны (при условии, что Widget не является ссылочным типом):

std::move(param)
std::forward<Widget>(param)
static_cast<Widget&&>(param)

В общем (когда Widget может быть ссылкой), std::move(param) эквивалентно обоим из следующих выражений:

std::forward<std::remove_reference<Widget>::type>(param)
static_cast<std::remove_reference<Widget>::type&&>(param)

Обратите внимание, насколько std::move лучше перемещать вещи. Смысл std::forward в том, что он хорошо сочетается с правилами вывода типа шаблона:

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

int main() {
    int a{};
    int const b{};
               //Deduced T   Signature    Result of `forward<T>` Result of `move`
    foo(a);    //int&        foo(int&)       lvalue int          xvalue int
    foo(b);    //int const&  foo(int const&) lvalue int const    xvalue int const
    foo(int{});//int         foo(int&&)      xvalue int          xvalue int
}
person Mankarse    schedule 04.11.2012
comment
Если Widget&& не является выведенным типом, например template<class Widget> f(Widget&& x) - person Andrew Tomazos; 04.11.2012
comment
@Andrew Tomazos - Fathomling - Конечно. Я предполагал, что Widget не является ссылочным типом (как в примере в вопросе). - person Mankarse; 04.11.2012