Как std :: move () передает значения в RValues?

105

Я просто обнаружил, что не до конца понимаю логику std::move().

Сначала я погуглил, но похоже, что есть только документы о том, как использовать std::move(), а не о том , как работает его структура.

Я имею в виду, что я знаю, что такое функция-член шаблона, но когда я смотрю на std::move()определение в VS2010, это все еще сбивает с толку.

определение std :: move () приведено ниже.

template<class _Ty> inline
typename tr1::_Remove_reference<_Ty>::_Type&&
    move(_Ty&& _Arg)
    {   // forward _Arg as movable
        return ((typename tr1::_Remove_reference<_Ty>::_Type&&)_Arg);
    }

В первую очередь мне кажется странным параметр (_Ty && _Arg), потому что, когда я вызываю функцию, как вы видите ниже,

// main()
Object obj1;
Object obj2 = std::move(obj1);

это в основном равно

// std::move()
_Ty&& _Arg = Obj1;

Но, как вы уже знаете, вы не можете напрямую связать LValue со ссылкой на RValue, что заставляет меня думать, что это должно быть так.

_Ty&& _Arg = (Object&&)obj1;

Однако это абсурд, потому что std :: move () должен работать для всех значений.

Итак, я думаю, чтобы полностью понять, как это работает, я должен также взглянуть на эти структуры.

template<class _Ty>
struct _Remove_reference
{   // remove reference
    typedef _Ty _Type;
};

template<class _Ty>
struct _Remove_reference<_Ty&>
{   // remove reference
    typedef _Ty _Type;
};

template<class _Ty>
struct _Remove_reference<_Ty&&>
{   // remove rvalue reference
    typedef _Ty _Type;
};

К сожалению, это все еще сбивает с толку, и я этого не понимаю.

Я знаю, что это все из-за отсутствия у меня базовых навыков синтаксиса в C ++. Я хотел бы знать, как они работают, и любые документы, которые я могу найти в Интернете, будут более чем приветствоваться. (Если вы можете просто объяснить это, это тоже будет круто).

Дин Со
источник
32
@NicolBolas Напротив, сам вопрос показывает, что OP недооценивает себя в этом заявлении. Именно владение OP базовыми синтаксическими навыками позволяет им даже задавать вопросы.
Кайл Стрэнд
Я в любом случае не согласен - вы можете быстро учиться или уже хорошо разбираетесь в информатике или программировании в целом, даже на C ++, и все еще боретесь с синтаксисом. Лучше потратить свое время на изучение механики, алгоритмов и т. Д., Но полагаться на ссылки, чтобы освежить синтаксис по мере необходимости, чем быть технически сформулированным, но иметь поверхностное понимание таких вещей, как копирование / перемещение / вперед. Как вы должны знать, «когда и как использовать std :: move, когда вы хотите переместить конструкцию чего-либо», прежде чем вы узнаете, что это за ход?
John P
Думаю, я пришел сюда, чтобы понять, как moveработает, а не как это реализовано. Я считаю это объяснение действительно полезным: pagefault.blog/2018/03/01/… .
Антон Данейко

Ответы:

172

Начнем с функции перемещения (которую я немного очистил):

template <typename T>
typename remove_reference<T>::type&& move(T&& arg)
{
  return static_cast<typename remove_reference<T>::type&&>(arg);
}

Начнем с более простой части - то есть, когда функция вызывается с rvalue:

Object a = std::move(Object());
// Object() is temporary, which is prvalue

и наш moveшаблон создается следующим образом:

// move with [T = Object]:
remove_reference<Object>::type&& move(Object&& arg)
{
  return static_cast<remove_reference<Object>::type&&>(arg);
}

Поскольку remove_referenceконвертирует T&в Tили T&&в Tи Objectне является ссылкой, наша последняя функция:

Object&& move(Object&& arg)
{
  return static_cast<Object&&>(arg);
}

Теперь вы можете спросить: а нужен ли нам гипс? Ответ: да, есть. Причина проста; названный справочник Rvalue будет рассматриваться как именующие (и неявное преобразование из именующих в RValue ссылки запрещено стандарт).


Вот что происходит, когда мы вызываем moveс lvalue:

Object a; // a is lvalue
Object b = std::move(a);

и соответствующий moveэкземпляр:

// move with [T = Object&]
remove_reference<Object&>::type&& move(Object& && arg)
{
  return static_cast<remove_reference<Object&>::type&&>(arg);
}

Опять remove_referenceконвертируем Object&в Objectи получаем:

Object&& move(Object& && arg)
{
  return static_cast<Object&&>(arg);
}

Теперь мы переходим к сложной части: что вообще Object& &&означает и как оно может быть привязано к lvalue?

Чтобы обеспечить идеальную пересылку, стандарт C ++ 11 предоставляет специальные правила для сворачивания ссылок, а именно:

Object &  &  = Object &
Object &  && = Object &
Object && &  = Object &
Object && && = Object &&

Как видите, под этими правилами на Object& &&самом деле подразумевается Object&простая ссылка lvalue, которая позволяет связывать lvalue.

Таким образом, конечная функция:

Object&& move(Object& arg)
{
  return static_cast<Object&&>(arg);
}

что мало чем отличается от предыдущего экземпляра с rvalue - они оба приводят свой аргумент к ссылке rvalue, а затем возвращают ее. Разница в том, что первый экземпляр можно использовать только с rvalues, а второй работает с lvalues.


Чтобы объяснить, почему нам нужно remove_referenceнемного больше, давайте попробуем эту функцию

template <typename T>
T&& wanna_be_move(T&& arg)
{
  return static_cast<T&&>(arg);
}

и создайте его с помощью lvalue.

// wanna_be_move [with T = Object&]
Object& && wanna_be_move(Object& && arg)
{
  return static_cast<Object& &&>(arg);
}

Применяя упомянутые выше правила сворачивания ссылок, вы можете увидеть, что мы получаем функцию, которую нельзя использовать как move(проще говоря, вы вызываете ее с lvalue, вы получаете lvalue обратно). Во всяком случае, эта функция является функцией идентичности.

Object& wanna_be_move(Object& arg)
{
  return static_cast<Object&>(arg);
}
Vitus
источник
3
Хороший ответ. Хотя я понимаю, что для lvalue рекомендуется оценивать Tas Object&, я не знал, что это действительно сделано. Я ожидал, Tчто Objectв этом случае также оценю , так как я думал, что это было причиной введения ссылок на оболочки и std::ref, или нет.
Christian Rau
2
Есть разница между template <typename T> void f(T arg)(это и есть статья в Википедии) и template <typename T> void f(T& arg). Первый разрешает значение (и если вы хотите передать ссылку, вы должны обернуть ее std::ref), а второй всегда разрешает ссылку. К сожалению, правила для шаблона аргумента дедукции являются довольно сложными, поэтому я не могу предоставить точные рассуждения , почему T&&resovles к Object& &&(но это происходит на самом деле).
Vitus
1
Но есть ли причина, по которой этот прямой подход не работает? template <typename T> T&& also_wanna_be_move(T& arg) { return static_cast<T&&>(arg); }
greggo 09
1
Это объясняет, почему требуется remove_reference, но я до сих пор не понимаю, почему функция должна принимать T && (вместо T &). Если я правильно понимаю это объяснение, не имеет значения, получите ли вы T && или T &, потому что в любом случае remove_reference переведет его в T, а затем вы снова добавите &&. Так почему бы не сказать, что вы принимаете T & (семантически это то, что вы принимаете) вместо T && и полагаться на идеальную переадресацию, позволяющую вызывающему абоненту передать T &?
mgiuca
3
@mgiuca: Если вы хотите std::moveпреобразовать только lvalues ​​в rvalues, тогда да, T&все будет в порядке. Этот трюк делается в основном для гибкости: вы можете вызвать std::moveвсе (включая rvalue) и получить обратно rvalue.
Витус
4

_Ty - это параметр шаблона, и в этой ситуации

Object obj1;
Object obj2 = std::move(obj1);

_Ty - это тип "Object &"

поэтому необходима ссылка _Remove_reference.

Было бы больше похоже

typedef Object& ObjectRef;
Object obj1;
ObjectRef&& obj1_ref = obj1;
Object&& obj2 = (Object&&)obj1_ref;

Если бы мы не удалили ссылку, это было бы так, как если бы мы делали

Object&& obj2 = (ObjectRef&&)obj1_ref;

Но ObjectRef && сводится к Object &, который мы не могли привязать к obj2.

Причина, по которой он сокращается таким образом, заключается в поддержке идеальной пересылки. См. Эту статью .

Вон Катон
источник
Это не объясняет, почему _Remove_reference_это необходимо. Например, если у вас есть Object&typedef и вы берете на него ссылку, вы все равно получите Object&. Почему это не работает с &&? На это есть ответ, и он связан с безупречной пересылкой.
Никол Болас
2
Правда. И ответ очень интересный. A & && сокращается до A &, поэтому, если мы попытаемся использовать (ObjectRef &&) obj1_ref, в этом случае мы получим Object &.
Vaughn Cato,