Mengapa `std :: move` bernama` std :: move`?

128

Fungsi C ++ 11 std::move(x)tidak benar-benar memindahkan apa pun. Itu hanya sebuah pemeran untuk nilai-r. Mengapa ini dilakukan? Bukankah ini menyesatkan?

Howard Hinnant
sumber
Lebih buruk lagi, tiga argumen itu std::movebenar - benar bergerak ..
Cubbi
Dan jangan lupa tentang C ++ std::char_traits::move
98/03/11
30
Favorit saya yang lain adalah std::remove()yang tidak menghapus elemen: Anda masih harus menelepon erase()untuk benar-benar menghapus elemen-elemen itu dari wadah. Jadi movetidak bergerak, removetidak menghapus. Saya akan memilih nama mark_movable()untuk move.
Ali
4
@ Alli saya akan menemukan mark_movable()membingungkan juga. Ini menunjukkan ada efek samping yang abadi di mana sebenarnya tidak ada.
finnw

Jawaban:

178

Benar bahwa std::move(x)itu hanya gips untuk menilai ulang - lebih khusus untuk xvalue , yang bertentangan dengan prvalue . Dan memang benar bahwa memiliki pemain bernama movekadang-kadang membingungkan orang. Namun maksud penamaan ini bukan untuk membingungkan, melainkan untuk membuat kode Anda lebih mudah dibaca.

Sejarah movetanggal kembali ke proposal pindah asli pada tahun 2002 . Makalah ini pertama kali memperkenalkan referensi nilai, dan kemudian menunjukkan cara menulis yang lebih efisien 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);
}

Kita harus ingat bahwa pada titik ini dalam sejarah, satu - satunya hal yang &&mungkin bisa berarti adalah logis dan . Tidak ada yang akrab dengan referensi nilai, atau implikasi dari melemparkan nilai ke nilai (sementara tidak membuat salinan seperti yang static_cast<T>(t)akan dilakukan). Jadi pembaca kode ini secara alami akan berpikir:

Saya tahu bagaimana swapseharusnya bekerja (menyalin ke sementara dan kemudian bertukar nilai), tetapi apa tujuan dari para pemain jelek itu ?!

Perhatikan juga bahwa swapini hanyalah stand-in untuk semua jenis algoritma permutasi-modifikasi. Diskusi ini jauh , jauh lebih besar daripada swap.

Kemudian proposal tersebut memperkenalkan sintaksis gula yang menggantikannya static_cast<T&&>dengan sesuatu yang lebih mudah dibaca yang menyampaikan tidak tepat apa , melainkan mengapa :

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

Yaitu movehanya untuk sintaksis static_cast<T&&>, dan sekarang kodenya cukup sugestif mengenai mengapa para pemain itu ada di sana: untuk memungkinkan pemindahan semantik!

Orang harus memahami bahwa dalam konteks sejarah, hanya sedikit orang pada saat ini yang benar-benar memahami hubungan intim antara nilai-nilai dan memindahkan semantik (walaupun makalah ini mencoba menjelaskannya juga):

Pindah semantik akan secara otomatis ikut bermain ketika diberikan argumen nilai. Ini sangat aman karena memindahkan sumber daya dari suatu nilai tidak dapat diketahui oleh seluruh program ( tidak ada orang lain yang memiliki referensi ke nilai untuk mendeteksi perbedaan ).

Jika pada saat swapitu disajikan seperti ini:

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

Maka orang akan melihat itu dan berkata:

Tapi mengapa Anda memilih untuk menilai?


Poin utama:

Seperti itu, menggunakan move, tidak ada yang pernah bertanya:

Tapi mengapa kamu pindah?


Seiring berlalunya waktu dan proposal itu disempurnakan, gagasan nilai dan nilai diubah menjadi kategori nilai yang kita miliki saat ini:

Taksonomi

(gambar tanpa malu-malu dicuri dari dirkgently )

Dan hari ini, jika kita ingin swapmengatakan apa yang dilakukannya, daripada mengapa , itu akan terlihat seperti:

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

Dan pertanyaan yang harus ditanyakan oleh setiap orang kepada diri mereka sendiri adalah apakah kode di atas lebih atau kurang dapat dibaca daripada:

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

Atau bahkan yang asli:

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

Dalam hal apa pun, pembuat program C ++ pekerja harian harus tahu bahwa di balik tudung move, tidak ada yang lebih dari sekadar pemeran. Dan programmer C ++ pemula, setidaknya dengan move, akan diberitahu bahwa tujuannya adalah untuk pindah dari rhs, sebagai lawan menyalin dari rhs, bahkan jika mereka tidak mengerti persis bagaimana hal itu dilakukan.

Selain itu, jika seorang programmer menginginkan fungsi ini dengan nama lain, std::movetidak memiliki monopoli pada fungsi ini, dan tidak ada keajaiban bahasa non-portabel yang terlibat dalam implementasinya. Sebagai contoh jika seseorang ingin kode set_value_category_to_xvalue, dan menggunakannya sebagai gantinya, itu sepele untuk melakukannya:

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

Dalam C ++ 14 itu menjadi lebih ringkas:

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

Jadi, jika Anda cenderung, hiasi static_cast<T&&>apa pun yang menurut Anda terbaik, dan mungkin akhirnya Anda akan mengembangkan praktik terbaik baru (C ++ terus berkembang).

Jadi apa yang movedilakukan dalam hal kode objek yang dihasilkan?

Pertimbangkan ini test:

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

Dikompilasi dengan clang++ -std=c++14 test.cpp -O3 -S, ini menghasilkan kode objek ini:

__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

Sekarang jika tes diubah menjadi:

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

Sama sekali tidak ada perubahan sama sekali dalam kode objek. Seseorang dapat menggeneralisasi hasil ini ke: Untuk objek yang bergerak sepele , std::movetidak memiliki dampak.

Sekarang mari kita lihat contoh ini:

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

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

Ini menghasilkan:

__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

Jika Anda menjalankan __ZN1XaSERKS_melalui c++filtmenghasilkan: X::operator=(X const&). Tidak mengherankan di sini. Sekarang jika tes diubah menjadi:

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

Kemudian masih tidak ada perubahan apa pun dalam kode objek yang dihasilkan. std::movetidak melakukan apa pun selain melemparkan jke nilai sebuah, dan kemudian nilai itu Xmengikat ke operator penugasan salinan X.

Sekarang mari tambahkan operator pemindahan tugas ke X:

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

Sekarang kode objek tidak berubah:

__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

Menjalankan __ZN1XaSEOS_melalui c++filtmengungkapkan bahwa X::operator=(X&&)yang dipanggil bukannya X::operator=(X const&).

Dan hanya itu yang ada untuk std::move! Ini benar-benar menghilang pada saat dijalankan. Satu-satunya dampaknya adalah pada saat kompilasi di mana ia dapat mengubah apa yang disebut overload.

Howard Hinnant
sumber
7
Berikut ini adalah sumber titik untuk grafik itu: Saya membuatnya kembali digraph D { glvalue -> { lvalue; xvalue } rvalue -> { xvalue; prvalue } expression -> { glvalue; rvalue } }untuk kepentingan umum :) Unduh di sini sebagai SVG
lihat
7
Apakah ini masih terbuka untuk bikeshedding? Saya sarankan allow_move;)
dyp
2
@dyp Favorit saya masih movable.
Daniel Frey
6
Scott Meyers menyarankan penggantian nama std::moveke rvalue_cast: youtube.com/…
nairware
6
Karena nilai sekarang mengacu pada nilai baik dan nilai rendah, rvalue_castapakah ambigu dalam maknanya: jenis nilai apa yang dikembalikan? xvalue_castakan menjadi nama yang konsisten di sini. Sayangnya, kebanyakan orang, saat ini, juga tidak mengerti apa yang dilakukannya. Dalam beberapa tahun lagi, pernyataan saya semoga menjadi salah.
Howard Hinnant
20

Biarkan saya tinggalkan kutipan di sini dari C ++ 11 FAQ yang ditulis oleh B. Stroustrup, yang merupakan jawaban langsung untuk pertanyaan OP:

move (x) berarti "Anda dapat memperlakukan x sebagai nilai". Mungkin akan lebih baik jika move () disebut rval (), tetapi sekarang move () telah digunakan selama bertahun-tahun.

Omong-omong, saya sangat menikmati FAQ - layak dibaca

podkova
sumber
2
Untuk menjiplak komentar @ HowardHinnant dari jawaban lain: Jawaban Stroustrup tidak akurat, karena sekarang ada dua jenis nilai - nilai dan nilai, dan std :: move benar-benar merupakan pemeran nilai x.
einpoklum