std::move が std::move という名前なのはなぜですか? 質問する

std::move が std::move という名前なのはなぜですか? 質問する

このstd::move(x)関数は実際には何も移動しません。単に右辺値にキャストするだけです。なぜこのような処理が行われたのでしょうか? これは誤解を招きませんか?

ベストアンサー1

正しいのは、std::move(x)右辺値へのキャスト、より具体的にはx値、とは対照的にprvalueまた、キャストに名前を付けると、混乱を招くことがあるのも事実ですmove。ただし、この名前の目的は混乱を招くことではなく、むしろコードを読みやすくすることです。

moveの歴史は2002年の最初の移転提案この論文では、まず右辺値参照を紹介し、次により効率的な の書き方を示します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);
}

歴史のこの時点で、「&&」が意味する唯一のものは論理的で右辺値参照や、左辺値を右辺値にキャストすることの意味(コピーを作成するのではなくstatic_cast<T>(t))について詳しい人は誰もいませんでした。そのため、このコードを読む人は当然次のように考えるでしょう。

どのように動作するかはわかっていますswap(一時的にコピーしてから値を交換する) が、これらの醜いキャストの目的は何ですか?

また、これは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&&>、コードは、それらのキャストが存在する理由をかなり示唆しています。つまり、移動セマンティクスを有効にするためです。

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

移動セマンティクスは、右辺値引数が与えられたときに自動的に機能します。これは、右辺値からリソースを移動してもプログラムの他の部分からは気づかれないため、完全に安全です(違いを検出するために右辺値を参照する人が他にいない)。

当時は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);
}

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

しかし、なぜ右辺値にキャストするのでしょうか?


要点:

実際のところ、 を使用するとmove、誰も次のことを尋ねませんでした。

でも、なぜ引っ越すのですか?


年月が経ち、提案が洗練されるにつれて、左辺値と右辺値の概念は次のように洗練されていった。価値カテゴリー今日は次の通りです。

分類

(画像は恥ずかしげもなく盗用不器用に

そして今日、もし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、その意図は動く右から、反対にコピー正確に理解していなくても、右からどうやってそれは達成されます。

さらに、プログラマーがこの機能に別の名前を使用したい場合、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キャストしただけで、その後、その右辺値はのコピー代入演算子にバインドされます。jXX

ここで、移動代入演算子を次のように追加します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実行時に完全に消えます。コンパイル時にのみ影響があります。かもしれない呼び出されるオーバーロードを変更します。

おすすめ記事