1. ホーム
  2. c++

[解決済み] std::move()はどのようにRValuesに値を転送するのですか?

2022-10-14 12:46:14

質問

の論理を十分に理解していないことに気づきました。 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++ についての基本的な構文スキルを欠いているためであることは分かっています。 これらがどのように動作するのか徹底的に知りたいので、インターネット上で得られるどんな文書でも大歓迎です。(もし、あなたがこれを説明してくれるなら、それも素晴らしいことです)。

どのように解決するのですか?

まず、移動関数から始めます(ちょっとだけきれいにしました)。

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参照 はlvalueとして扱われるからです(そして、lvalueから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& を意味します。これは、L値をバインドすることができるプレーンなL値参照です。

最終的な関数はこのようになります。

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

は、前のrvalueを使ったインスタンス化と同じように、どちらも引数をrvalueの参照にキャストして、それを返します。違いは、最初のインスタンス化はrvalueでのみ使用でき、2番目のものはlvalueで動作することです。


なぜ 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が返ってくるということです)。どちらかというと、この関数はidentity関数です。

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