1. ホーム
  2. c++

[解決済み] なぜ `std::move` という名前なのですか?

2022-06-21 02:11:43

疑問点

C++11の std::move(x) 関数は実際にはまったく何も動かしません。 単に r-value にキャストしているだけです。 なぜこのようなことが行われたのでしょうか? これは誤解を招くものではないでしょうか?

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

正しいのは std::move(x) は単にrvalueへのキャストであり、より具体的には xvalue とは対照的に . という名前のキャストを持つことも事実です。 move という名前のキャストを持つことが、時に人々を混乱させることも事実です。 しかし、この命名の意図は混乱させることではなく、むしろコードをより読みやすくすることです。

の歴史は move にさかのぼります。 の移動提案までさかのぼります。 . この論文では、まず rvalue リファレンスを紹介し、より効率的な std::swap :

template <class T>
void
swap(T& a, T& b)
{
    T tmp(static_cast<T&&>(a));
    a = static_cast<T&&>(b);
    b = static_cast<T&&>(tmp);
}

歴史のこの時点では、"だけだったことを思い起こす必要があります。 && を意味する可能性があるのは 論理的で . 誰も rvalue の参照に精通しておらず、lvalue を rvalue にキャストすることの意味もわかっていませんでした (ただし、コピーを作成するのではなく static_cast<T>(t) のようにコピーはしないが)。 ですから、このコードの読者は当然こう思うでしょう。

私はどのように swap がどのように機能するか(temporaryにコピーして値を交換する)はわかっていますが、あの醜いキャストは何のためなのでしょうか?

また、以下の点にも注意してください。 swap は実際にはあらゆる種類の順列変更アルゴリズムの代用品に過ぎないことにも注意してください。 この議論は 大いに よりもはるかに大きなものです。 swap .

次に、この提案では シンタックスシュガー を導入します。 static_cast<T&&> をより読みやすいものに置き換えるもので、正確な ではなく、むしろ なぜ :

template <class T>
void
swap(T& a, T& b)
{
    T tmp(move(a));
    a = move(b);
    b = move(tmp);
}

すなわち move は単なるシンタックスシュガーで static_cast<T&&> そして今、このコードはそれらのキャストがなぜあるのかについて非常に示唆的です。

歴史の文脈では、この時点で rvalues と移動セマンティックスの間の密接な関係を本当に理解していた人はほとんどいなかったことを理解する必要があります (ただし、この論文ではその点についても説明しようとしています)。

引数として rvalue が与えられると、着手セマンティクスが自動的に適用されます。 引数が与えられると、移動セマンティクスが自動的に適用されます。のリソースを移動してもプログラムの他の部分には気づかれないので、これは完全に安全です。 rvalue からリソースを移動しても、プログラムの残りの部分には気づかれないからです ( は他の誰も を検出するためのrvalueへの参照を持っていません。 ).

もし、その時に swap が代わりにこのように表示されていたとします。

template <class T>
void
swap(T& a, T& b)
{
    T tmp(cast_to_rvalue(a));
    a = cast_to_rvalue(b);
    b = cast_to_rvalue(tmp);
}

そうすれば、人々はそれを見て、こう言ったことでしょう。

しかし、なぜrvalueにキャストしているのでしょうか?


本題です。

そのままでは move を使用していたため、誰も質問しませんでした。

でも、なんで引っ越すの?


年月が経ち、提案が洗練されるにつれて、lvalue と rvalue という概念は 値のカテゴリ に改良されました。

(画像は恥ずかしながら dirkgently )

で、今日、もし私たちが swap を正確に言うと の代わりに なぜ のように見えるはずです。

template <class T>
void
swap(T& a, T& b)
{
    T tmp(set_value_category_to_xvalue(a));
    a = set_value_category_to_xvalue(b);
    b = set_value_category_to_xvalue(tmp);
}

そして、誰もが自分に問うべき質問は、上記のコードがより可読性が高いか低いかということです。

template <class T>
void
swap(T& a, T& b)
{
    T tmp(move(a));
    a = move(b);
    b = move(tmp);
}

オリジナルでもいい。

template <class T>
void
swap(T& a, T& b)
{
    T tmp(static_cast<T&&>(a));
    a = static_cast<T&&>(b);
    b = static_cast<T&&>(tmp);
}

いずれにせよ、C++プログラマの職人なら move の裏側では、キャスト以上のことは何も行われていないことを知るべきです。 そして、初心者の C++ プログラマは、少なくとも move であれば、その意図は を動かす とは対照的に、rhs から コピー を正確に理解していなくても、右辺から どのように を正確に理解していなくてもです。

さらに、プログラマがこの機能を別の名前で望む場合。 std::move はこの機能を独占しているわけではありませんし、その実装に関わる移植不可能な言語マジックもありません。 たとえば、もし人が set_value_category_to_xvalue というように、簡単に使うことができます。

template <class T>
inline
constexpr
typename std::remove_reference<T>::type&&
set_value_category_to_xvalue(T&& t) noexcept
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

C++14では、さらに簡潔になります。

template <class T>
inline
constexpr
auto&&
set_value_category_to_xvalue(T&& t) noexcept
{
    return static_cast<std::remove_reference_t<T>&&>(t);
}

というわけで、もしあなたがその気になったら、あなたの static_cast<T&&> をどのように飾るか、そしておそらく新しいベストプラクティスを開発することになるでしょう (C++ は常に進化しています)。

では move は生成されたオブジェクトコードという点ではどうなのでしょうか?

次のように考えてみましょう。 test :

void
test(int& i, int& j)
{
    i = j;
}

でコンパイルされた clang++ -std=c++14 test.cpp -O3 -S でコンパイルすると,このようなオブジェクトコードになります.

__Z4testRiS_:                           ## @_Z4testRiS_
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    movl    (%rsi), %eax
    movl    %eax, (%rdi)
    popq    %rbp
    retq
    .cfi_endproc

これで、テストが変更された場合

void
test(int& i, int& j)
{
    i = std::move(j);
}

そこには 全く変化なし をオブジェクトコードに追加しました。 この結果を一般化することができます。 に対して に対して、些細に移動可能な オブジェクトを std::move は影響を与えません。

では、この例を見てみましょう。

struct X
{
    X& operator=(const X&);
};

void
test(X& i, X& j)
{
    i = j;
}

これで生成されます。

__Z4testR1XS0_:                         ## @_Z4testR1XS0_
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    popq    %rbp
    jmp __ZN1XaSERKS_           ## TAILCALL
    .cfi_endproc

もしあなたが __ZN1XaSERKS_ を通して c++filt を生成します。 X::operator=(X const&) . ここで驚くことはありません。 さて、もしテストが次のように変更されたら。

void
test(X& i, X& j)
{
    i = std::move(j);
}

次に、まだ 変化なし を生成されたオブジェクトコードに適用します。 std::move は何もしていませんが、キャスト j を rvalue にキャストし、その rvalue を X のコピー代入演算子と結合する。 X .

では、移動代入演算子を X :

struct X
{
    X& operator=(const X&);
    X& operator=(X&&);
};

次にオブジェクトコード を変更します。

__Z4testR1XS0_:                         ## @_Z4testR1XS0_
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    popq    %rbp
    jmp __ZN1XaSEOS_            ## TAILCALL
    .cfi_endproc

実行中 __ZN1XaSEOS_ を通して c++filt が明らかにするのは X::operator=(X&&) が呼び出されていることがわかります。 X::operator=(X const&) .

そして のみです。 std::move ! 実行時には完全に消えてしまいます。 唯一の影響はコンパイル時で、それは があるかもしれません。 どのオーバーロードが呼ばれるかを変更します。