Bagaimana std :: move () mentransfer nilai ke RValues?

104

Saya baru saja menemukan diri saya tidak sepenuhnya memahami logika std::move().

Awalnya saya googling di Google tapi sepertinya hanya ada dokumen tentang cara penggunaan std::move(), bukan cara kerja strukturnya.

Maksud saya, saya tahu apa itu fungsi anggota template tetapi ketika saya melihat std::move()definisi di VS2010, itu masih membingungkan.

definisi dari std :: move () di bawah ini.

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

Yang aneh pertama kali bagi saya adalah parameternya, (_Ty && _Arg), karena ketika saya memanggil fungsi seperti yang Anda lihat di bawah,

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

itu pada dasarnya sama dengan

// std::move()
_Ty&& _Arg = Obj1;

Tetapi seperti yang telah Anda ketahui, Anda tidak dapat langsung menautkan LValue ke referensi RValue, yang membuat saya berpikir seharusnya seperti ini.

_Ty&& _Arg = (Object&&)obj1;

Namun, ini tidak masuk akal karena std :: move () harus berfungsi untuk semua nilai.

Jadi saya kira untuk sepenuhnya memahami cara kerjanya, saya harus melihat struct ini juga.

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

Sayangnya itu masih membingungkan dan saya tidak mengerti.

Saya tahu bahwa ini semua karena saya kurang memiliki keterampilan sintaks dasar tentang C ++. Saya ingin tahu bagaimana ini bekerja secara menyeluruh dan dokumen apa pun yang bisa saya dapatkan di internet akan disambut dengan sangat baik. (Jika Anda bisa menjelaskan ini, itu akan luar biasa juga).

Dean Seo
sumber
31
@NicolBolas Sebaliknya, pertanyaan itu sendiri menunjukkan bahwa OP menjual terlalu rendah dalam pernyataan itu. Justru pemahaman OP tentang keterampilan sintaksis dasar yang memungkinkan mereka untuk mengajukan pertanyaan.
Kyle Strand
Saya tidak setuju - Anda dapat belajar dengan cepat atau sudah memiliki pemahaman yang baik tentang ilmu komputer atau pemrograman secara keseluruhan, bahkan dalam C ++, dan masih kesulitan dengan sintaksnya. Lebih baik menghabiskan waktu Anda mempelajari mekanika, algoritme, dll. Tetapi mengandalkan referensi untuk memoles sintaks sesuai kebutuhan, daripada mengartikulasikan secara teknis tetapi memiliki pemahaman yang dangkal tentang hal-hal seperti salin / bergerak / maju. Bagaimana Anda bisa tahu "kapan dan bagaimana menggunakan std :: move saat Anda ingin memindahkan konstruksi sesuatu" sebelum Anda tahu apa yang dilakukan move?
John P
Saya pikir saya datang ke sini untuk memahami cara movekerjanya daripada bagaimana penerapannya. Saya menemukan penjelasan ini sangat berguna: pagefault.blog/2018/03/01/… .
Anton Daneyko

Jawaban:

171

Kami mulai dengan fungsi pemindahan (yang saya bersihkan sedikit):

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

Mari kita mulai dengan bagian yang lebih mudah - yaitu, ketika fungsi tersebut dipanggil dengan rvalue:

Object a = std::move(Object());
// Object() is temporary, which is prvalue

dan movetemplate kita dibuat sebagai berikut:

// move with [T = Object]:
remove_reference<Object>::type&& move(Object&& arg)
{
  return static_cast<remove_reference<Object>::type&&>(arg);
}

Karena remove_referencemengonversi T&menjadi Tatau T&&menjadi T, dan Objectbukan referensi, fungsi terakhir kita adalah:

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

Sekarang, Anda mungkin bertanya-tanya: apakah kita membutuhkan pemerannya? Jawabannya adalah: ya, kami lakukan. Alasannya sederhana; bernama referensi nilai p adalah diperlakukan sebagai lvalue (dan konversi implisit dari lvalue ke referensi nilai p dilarang oleh standar).


Inilah yang terjadi ketika kita memanggil movedengan lvalue:

Object a; // a is lvalue
Object b = std::move(a);

dan moveinstansiasi yang sesuai :

// move with [T = Object&]
remove_reference<Object&>::type&& move(Object& && arg)
{
  return static_cast<remove_reference<Object&>::type&&>(arg);
}

Sekali lagi, remove_referenceberalihlah Object&ke Objectdan kami mendapatkan:

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

Sekarang kita sampai pada bagian yang sulit: apa Object& &&artinya dan bagaimana itu bisa mengikat ke lvalue?

Untuk memungkinkan penerusan yang sempurna, standar C ++ 11 memberikan aturan khusus untuk penciutan referensi, yaitu sebagai berikut:

Object &  &  = Object &
Object &  && = Object &
Object && &  = Object &
Object && && = Object &&

Seperti yang Anda lihat, di bawah aturan ini Object& &&sebenarnya berarti Object&, yang merupakan referensi nilai l biasa yang memungkinkan mengikat nilai l.

Fungsi akhirnya adalah:

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

yang tidak berbeda dengan contoh sebelumnya dengan rvalue - keduanya melemparkan argumennya ke rvalue referensi dan kemudian mengembalikannya. Perbedaannya adalah bahwa instansiasi pertama hanya dapat digunakan dengan nilai r, sedangkan yang kedua bekerja dengan nilai l.


Untuk menjelaskan mengapa kita membutuhkan remove_referencelebih banyak, mari coba fungsi ini

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

dan membuat instance dengan lvalue.

// wanna_be_move [with T = Object&]
Object& && wanna_be_move(Object& && arg)
{
  return static_cast<Object& &&>(arg);
}

Dengan menerapkan aturan penciutan referensi yang disebutkan di atas, Anda dapat melihat kami mendapatkan fungsi yang tidak dapat digunakan sebagai move(sederhananya, Anda menyebutnya dengan lvalue, Anda mendapatkan lvalue kembali). Jika ada, fungsi ini adalah fungsi identitas.

Object& wanna_be_move(Object& arg)
{
  return static_cast<Object&>(arg);
}
Vitus
sumber
3
Jawaban bagus. Meskipun saya memahami bahwa untuk nilai l adalah ide yang baik untuk mengevaluasi Tsebagai Object&, saya tidak tahu bahwa ini benar-benar dilakukan. Saya juga berharap Tuntuk mengevaluasi Objectdalam kasus ini, karena saya pikir ini adalah alasan untuk memperkenalkan referensi pembungkus dan std::ref, atau bukan.
Christian Rau
2
Ada perbedaan antara template <typename T> void f(T arg)(yang ada di artikel Wikipedia) dan template <typename T> void f(T& arg). Yang pertama memutuskan untuk menilai (dan jika Anda ingin meneruskan referensi, Anda harus membungkusnya std::ref), sedangkan yang kedua selalu memutuskan untuk referensi. Sayangnya, aturan untuk pengurangan argumen template agak rumit, jadi saya tidak bisa memberikan alasan yang tepat mengapa T&&resovles ke Object& &&(tapi itu memang terjadi).
Vitus
1
Tetapi, adakah alasan mengapa pendekatan langsung ini tidak berhasil? template <typename T> T&& also_wanna_be_move(T& arg) { return static_cast<T&&>(arg); }
greggo
1
Itu menjelaskan mengapa remove_reference diperlukan, tapi saya masih tidak mengerti mengapa fungsi harus menggunakan T && (bukan T &). Jika saya memahami penjelasan ini dengan benar, tidak masalah apakah Anda mendapatkan T && atau T & karena bagaimanapun juga, remove_reference akan mengirimkannya ke T, maka Anda akan menambahkan kembali &&. Jadi mengapa tidak mengatakan Anda menerima T & (yang secara semantik adalah apa yang Anda terima), alih-alih T && dan mengandalkan penerusan sempurna untuk memungkinkan pemanggil melewati T &?
mgiuca
3
@mgiuca: Jika Anda std::movehanya ingin memberikan nilai l ke nilai r, ya, T&akan baik-baik saja. Trik ini dilakukan sebagian besar untuk fleksibilitas: Anda dapat memanggil std::movesemuanya (termasuk nilai r) dan mendapatkan nilai r kembali.
Vitus
4

_Ty adalah parameter template, dan dalam situasi ini

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

_Ty adalah tipe "Objek &"

itulah mengapa _Remove_reference diperlukan.

Ini akan lebih seperti

typedef Object& ObjectRef;
Object obj1;
ObjectRef&& obj1_ref = obj1;
Object&& obj2 = (Object&&)obj1_ref;

Jika kami tidak menghapus referensi, itu akan seperti yang kami lakukan

Object&& obj2 = (ObjectRef&&)obj1_ref;

Tapi ObjectRef && direduksi menjadi Object &, yang tidak bisa kami ikat ke obj2.

Alasan pengurangan cara ini adalah untuk mendukung penerusan sempurna. Lihat makalah ini .

Vaughn Cato
sumber
Ini tidak menjelaskan segalanya tentang mengapa _Remove_reference_itu perlu. Misalnya, jika Anda memiliki Object&sebagai typedef, dan Anda mengacu padanya, Anda masih mendapatkan Object&. Mengapa itu tidak berhasil dengan &&? Ada adalah jawaban untuk itu, dan itu ada hubungannya dengan forwarding yang sempurna.
Nicol Bolas
2
Benar. Dan jawabannya sangat menarik. A & && direduksi menjadi A &, jadi jika kita mencoba menggunakan (ObjectRef &&) obj1_ref, kita akan mendapatkan Object & sebagai gantinya dalam kasus ini.
Vaughn Cato