std :: move()はどのようにして値をRValuesに転送しますか?


103

の論理を完全に理解していないことに気づきました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 ++に関する基本的な構文スキルが不足しているためです。これらがどのように機能するかを知りたいのですが、インターネットで入手できるドキュメントは歓迎されます。(もしあなたがこれを説明できるなら、それも素晴らしいでしょう)。


31
@NicolBolas逆に、質問自体は、OPがそのステートメントで彼ら自身をアンダーセルしていることを示しています。OP が基本的な構文スキルを把握しているため、問題を提起することさえできます。
カイルストランド

とにかく私は同意しません-あなたはすぐに学ぶことができますし、すでにC ++でさえ、コンピュータサイエンスやプログラミング全体を十分に理解していて、まだ構文に苦労しているかもしれません。技術やアルゴリズムなどを学ぶのに時間を費やす方がよいですが、技術的に明確であるよりもコピー/移動/転送などのことを浅く理解するよりも、必要に応じて構文をブラッシュアップするために参照に依存する方が良いです。どのように移動するかを知る前に、「いつ、どのようにstd :: moveを使用して、何かを構築したいのか」を知っているはずです。
John P

move実装方法ではなく、動作方法を理解するためにここに来たと思います。この説明は本当に役立つと思います:pagefault.blog/2018/03/01/…
アントンダネイコ

回答:


170

move関数から始めます(少しクリーンアップしました)。

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

簡単な部分から始めましょう。つまり、関数が右辺値で呼び出されます。

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&され、参照ではないため、最終的な関数は次のとおりです。TT&&TObject

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

さて、あなたは不思議に思うかもしれません:キャストさえ必要ですか?答えは「はい、そうです」です。理由は簡単です。名前付き右辺値参照左辺値として扱われます(左辺値から右辺値参照への暗黙的な変換は標準で禁止されています)。


movelvalueで呼び出すと、次のようになります。

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&。これは、左辺値をバインドできるプレーンな左辺値参照です。

したがって、最終的な関数は次のとおりです。

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

これは、以前の右辺値のインスタンス化とは異なりません。どちらも、引数を右辺値参照にキャストしてから返します。違いは、最初のインスタンス化は右辺値でのみ使用でき、2番目のインスタンス化は左辺値で機能することです。


なぜremove_referenceもう少し必要なのかを説明するために、この関数を試してみましょう

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

左辺値でインスタンス化します。

// 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);
}

3
素敵な答え。私は左辺値のためにそれを評価することは良い考えであることを理解しますがTObject&、私はこれが本当に行われていることを知りませんでした。これがラッパー参照とを導入した理由だと思ったので、この場合Tも評価するつもりObjectでしたstd::refか、そうではありませんでした。
クリスチャン・ラウ

2
template <typename T> void f(T arg)(ウィキペディアの記事にあるもの)とには違いがありtemplate <typename T> void f(T& arg)ます。最初のものは値に解決されます(参照を渡す場合は、それをでラップする必要がありますstd::ref)、2番目のものは常に参照に解決されます。悲しいことに、テンプレート引数の控除の規則はかなり複雑であるため、T&&再割り当てを行う理由を正確に説明することはできませんObject& &&(実際に起こります)。
Vitus

1
しかし、この直接的なアプローチが機能しない理由はありますか?template <typename T> T&& also_wanna_be_move(T& arg) { return static_cast<T&&>(arg); }
greggo 2014年

1
それが、remove_referenceが必要な理由を説明していますが、関数が(T&ではなく)T &&をとる必要がある理由はまだわかりません。この説明を正しく理解していれば、T &&とT&のどちらを取得してもかまいません。どちらの方法でも、remove_referenceはそれをTにキャストしてから、&&を追加します。では、T &&の代わりにT&(意味的にはあなたが受け入れているもの)を受け入れ、発信者がT&を渡すことを許可するために完全な転送に依存していると言ってみませんか?
mgiuca 2014年

3
@mgiuca:std::move左辺値を右辺値にキャストするだけの場合は、はい、T&大丈夫です。このトリックは主に柔軟性のために行われます。std::moveすべて(右辺値を含む)を呼び出して右辺値を取り戻すことができます。
ヴィート

4

_Tyはテンプレートパラメータであり、この状況では

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

_Tyはタイプ「オブジェクト&」です

これが_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&た場合でも、を取得します。&&でそれが機能しないのはなぜですか?そこであることへの答えは、それは完璧な転送に関係しています。
Nicol Bolas、2011

2
そうだね。そして答えは非常に興味深いです。A&&&はA&に縮小されるため、(ObjectRef &&)obj1_refを使用しようとすると、この場合は代わりにObject&が取得されます。
Vaughn Cato、
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.