Apa itu std :: move (), dan kapan itu harus digunakan?

656
  1. Apa itu?
  2. Apa fungsinya?
  3. Kapan itu harus digunakan?

Tautan yang baik dihargai.

Basilevs
sumber
43
Bjarne Stroustrup menjelaskan langkah dalam A Pengantar Singkat untuk Menilai Nilai
DumbCoder
1
Pindahkan semantik
Basilevs
12
Pertanyaan ini mengacu pada std::move(T && t); ada juga std::move(InputIt first, InputIt last, OutputIt d_first)yang merupakan algoritma yang terkait dengan std::copy. Saya tunjukkan sehingga orang lain tidak bingung seperti saya ketika pertama kali dihadapkan dengan std::movetiga argumen. en.cppreference.com/w/cpp/algorithm/move
josaphatv

Jawaban:

287

Halaman Wikipedia di C ++ 11 Referensi nilai-R dan pindahkan konstruktor

  1. Di C ++ 11, selain menyalin konstruktor, objek dapat memindahkan konstruktor.
    (Dan selain menyalin operator penugasan, mereka memiliki operator penugasan pindah.)
  2. Konstruktor bergerak digunakan sebagai pengganti konstruktor salin, jika objek memiliki tipe "rvalue-reference" ( Type &&).
  3. std::move() adalah para pemeran yang menghasilkan referensi-nilai-nilai ke suatu objek, untuk memungkinkan perpindahan darinya.

Ini adalah cara C ++ baru untuk menghindari salinan. Sebagai contoh, menggunakan move constructor, a std::vectorbisa saja menyalin pointer internal ke data ke objek baru, meninggalkan objek yang dipindahkan dalam keadaan dipindahkan dari keadaan, karena itu tidak menyalin semua data. Ini akan menjadi C ++ - valid.

Coba googling untuk memindahkan semantik, menilai ulang, penerusan sempurna.

Scharron
sumber
40
Pindah-semantik mengharuskan objek yang dipindahkan tetap valid , yang bukan merupakan kondisi yang salah. (Dasar Pemikiran: Itu masih harus dirusak, buat itu berfungsi.)
GManNickG
13
@ GM: baik, itu harus dalam keadaan yang aman untuk dihancurkan, tetapi, AFAIK, itu tidak harus dapat digunakan untuk hal lain.
Zan Lynx
8
@ ZanLynx: Benar. Perhatikan bahwa pustaka standar juga mengharuskan objek yang dipindahkan dapat ditentukan, tetapi ini hanya untuk objek yang digunakan dalam stdlib, bukan persyaratan umum.
GManNickG
25
-1 "std :: move () adalah cara C ++ 11 untuk menggunakan move semantik" Silakan perbaiki itu. std::move()bukan cara untuk menggunakan semantik langkah, semantik langkah dilakukan secara transparan kepada programmer. moveitu hanya pemain untuk meneruskan nilai dari satu titik ke titik lain di mana nilai asli tidak akan lagi digunakan.
Manu343726
15
Saya akan melangkah lebih jauh. std::movesendiri tidak "apa-apa" - tidak memiliki efek samping. Itu hanya memberi sinyal kepada kompiler bahwa programmer tidak peduli apa yang terjadi pada objek itu lagi. yaitu memberikan izin ke bagian lain dari perangkat lunak untuk bergerak dari objek, tetapi tidak mengharuskan itu dipindahkan. Bahkan, penerima referensi nilai tidak harus membuat janji tentang apa yang akan atau tidak akan dilakukan dengan data.
Aaron McDaid
241

1. "Apa itu?"

Sementara std::move() secara teknis fungsi - saya akan mengatakan tidak benar-benar fungsi . Ini semacam konverter antara cara kompiler mempertimbangkan nilai ekspresi.

2. "Apa fungsinya?"

Hal pertama yang perlu diperhatikan adalah std::move() tidak benar-benar memindahkan apa pun . Itu mengkonversi ekspresi dari menjadi nilai (seperti variabel bernama) menjadi nilai x . Nilai x memberi tahu kompilator:

Anda dapat menjarah saya, memindahkan apa pun yang saya pegang dan menggunakannya di tempat lain (karena bagaimanapun saya akan segera dihancurkan) ".

dengan kata lain, saat Anda menggunakan std::move(x), Anda mengizinkan penyusun mengkalibkan x. Jadi jika xmemiliki, katakanlah, buffernya sendiri dalam memori - setelah std::move()kompiler dapat memiliki objek lain memilikinya.

Anda juga dapat beralih dari nilai awal (seperti sementara yang Anda lewati), tetapi ini jarang berguna.

3. "Kapan itu harus digunakan?"

Cara lain untuk mengajukan pertanyaan ini adalah "Untuk apa saya bisa mematikan sumber daya objek yang ada?" baik, jika Anda menulis kode aplikasi, Anda mungkin tidak akan main-main dengan benda-benda sementara yang dibuat oleh kompiler. Jadi terutama Anda akan melakukan ini di tempat-tempat seperti konstruktor, metode operator, fungsi standar-perpustakaan-seperti algoritma dll. Di mana objek bisa dibuat dan dihancurkan secara otomatis banyak. Tentu saja, itu hanya aturan praktis.

Penggunaan tipikal adalah memindahkan sumber daya dari satu objek ke objek lain alih-alih menyalin. @Guillaume menghubungkan ke halaman ini yang memiliki contoh singkat langsung: menukar dua objek dengan sedikit penyalinan.

template <class T>
swap(T& a, T& b) {
    T tmp(a);   // we now have two copies of a
    a = b;      // we now have two copies of b (+ discarded a copy of a)
    b = tmp;    // we now have two copies of tmp (+ discarded a copy of b)
}

menggunakan gerakan memungkinkan Anda untuk menukar sumber daya alih-alih menyalinnya:

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

Pikirkan apa yang terjadi ketika T, katakanlah, vector<int>ukuran n. Pada versi pertama Anda membaca dan menulis elemen 3 * n, pada versi kedua Anda pada dasarnya membaca dan menulis hanya 3 pointer ke buffer vektor, ditambah ukuran 3 buffer. Tentu saja, kelas Tperlu tahu bagaimana melakukan gerakan; kelas Anda harus memiliki operator penugasan bergerak dan konstruktor gerakan agar kelas Tdapat berfungsi.

einpoklum
sumber
3
Untuk waktu yang lama saya pernah mendengar tentang semantik bergerak ini, saya tidak pernah melihat mereka. Dari deskripsi yang Anda berikan ini, sepertinya ini adalah salinan yang dangkal dan bukan salinan yang dalam.
Zebrafish
7
@TitoneMaurice: Kecuali itu bukan salinan - karena nilai aslinya tidak lagi dapat digunakan.
einpoklum
3
@ Zebrafish Anda tidak mungkin salah. Salinan dangkal meninggalkan yang asli dalam kondisi yang sama persis, suatu langkah biasanya menghasilkan yang asli kosong atau dalam keadaan yang dinyatakan valid.
rubenvb
17
@rubenvb Zebra tidak sepenuhnya salah. Meskipun benar bahwa objek cannabilised asli biasanya disabot dengan sengaja untuk menghindari kesalahan yang membingungkan (mis. Atur pointer ke nullptr untuk memberi sinyal bahwa ia tidak lagi memiliki pointees), fakta bahwa seluruh langkah diimplementasikan dengan hanya menyalin pointer dari sumber. ke tujuan (dan sengaja menghindari melakukan apa pun dengan pointee) memang mengingatkan pada salinan dangkal. Bahkan, saya akan mengatakan bahwa suatu gerakan adalah salinan yang dangkal, diikuti secara opsional oleh penghancuran diri sebagian sumber. (lanjutan)
Lightness Races in Orbit
3
(lanjt.) Jika kita mengizinkan definisi ini (dan saya lebih suka menyukainya), maka pengamatan @ Zebrafish tidak salah, hanya sedikit tidak lengkap.
Lightness Races in Orbit
146

Anda dapat menggunakan langkah ketika Anda perlu "mentransfer" konten suatu objek di tempat lain, tanpa melakukan salinan (yaitu konten tidak digandakan, itu sebabnya dapat digunakan pada beberapa objek yang tidak dapat disalin, seperti unique_ptr). Itu juga mungkin bagi suatu objek untuk mengambil konten dari objek sementara tanpa melakukan salinan (dan menghemat banyak waktu), dengan std :: move.

Tautan ini sangat membantu saya:

http://thbecker.net/articles/rvalue_references/section_01.html

Maaf jika jawaban saya terlambat, tetapi saya juga mencari tautan yang bagus untuk std :: move, dan saya menemukan tautan di atas sedikit "keras".

Ini memberi penekanan pada referensi nilai-r, di mana konteks Anda harus menggunakannya, dan saya pikir itu lebih rinci, itu sebabnya saya ingin berbagi tautan ini di sini.

Guillaume
sumber
26
Tautan yang bagus. Saya selalu menemukan artikel wikipedia, dan tautan lain yang saya temukan agak membingungkan karena mereka hanya memberikan fakta kepada Anda, membiarkannya bagi Anda untuk mencari tahu apa arti sebenarnya / dasar pemikirannya. Sementara "pindahkan semantik" di konstruktor agak jelas, semua detail tentang meneruskan && - nilai di sekitar tidak ... jadi deskripsi gaya tutorialnya sangat bagus.
Christian Stieber
66

T: Apa itu std::move?

A: std::move()adalah fungsi dari C ++ Standard Library untuk casting ke referensi nilai.

Secara sederhana std::move(t)setara dengan:

static_cast<T&&>(t);

Nilai adalah sementara yang tidak bertahan di luar ekspresi yang mendefinisikannya, seperti hasil fungsi antara yang tidak pernah disimpan dalam variabel.

int a = 3; // 3 is a rvalue, does not exist after expression is evaluated
int b = a; // a is a lvalue, keeps existing after expression is evaluated

Implementasi untuk std :: move () diberikan di N2027: "Pengantar Singkat untuk Referensi Nilai" sebagai berikut:

template <class T>
typename remove_reference<T>::type&&
std::move(T&& a)
{
    return a;
}

Seperti yang Anda lihat, std::movemengembalikan T&&tidak masalah jika dipanggil dengan nilai ( T), tipe referensi (T& ) atau referensi nilai ( T&&).

T: Apa fungsinya?

A: Sebagai pemain, itu tidak melakukan apa pun selama runtime. Hanya relevan pada waktu kompilasi untuk memberi tahu kompiler bahwa Anda ingin terus mempertimbangkan referensi sebagai nilai.

foo(3 * 5); // obviously, you are calling foo with a temporary (rvalue)

int a = 3 * 5;
foo(a);     // how to tell the compiler to treat `a` as an rvalue?
foo(std::move(a)); // will call `foo(int&& a)` rather than `foo(int a)` or `foo(int& a)`

Apa yang tidak dilakukannya:

  • Buat salinan argumennya
  • Hubungi pembuat salinan
  • Ubah objek argumen

T: Kapan harus digunakan?

A: Anda harus menggunakan std::move jika Anda ingin memanggil fungsi yang mendukung pemindahan semantik dengan argumen yang bukan nilai (ekspresi sementara).

Ini menimbulkan pertanyaan lanjutan bagi saya:

  • Apa itu semantik bergerak? Memindahkan semantik berbeda dengan menyalin semantik adalah teknik pemrograman di mana anggota objek diinisialisasi dengan 'mengambil alih' alih-alih menyalin anggota objek lain. 'Pengambilalihan' semacam itu hanya masuk akal dengan pointer dan pegangan sumber daya, yang dapat ditransfer dengan murah dengan menyalin pointer atau pegangan integer daripada data yang mendasarinya.

  • Kelas dan objek apa yang mendukung semantik bergerak? Terserah Anda sebagai pengembang untuk menerapkan semantik bergerak di kelas Anda sendiri jika ini akan mendapat manfaat dengan mentransfer anggota mereka daripada menyalinnya. Setelah Anda menerapkan pemindahan semantik, Anda akan langsung mendapat manfaat dari pekerjaan dari banyak pemrogram perpustakaan yang telah menambahkan dukungan untuk menangani kelas dengan pemindahan semantik secara efisien.

  • Mengapa kompiler tidak dapat mengetahuinya sendiri? Kompiler tidak bisa begitu saja memanggil kelebihan fungsi yang lain kecuali Anda mengatakannya. Anda harus membantu kompilator memilih apakah versi fungsi biasa atau pindah harus dipanggil.

  • Dalam situasi apa saya ingin memberitahu kompiler bahwa ia harus memperlakukan variabel sebagai nilai? Ini kemungkinan besar akan terjadi dalam fungsi templat atau pustaka, di mana Anda tahu bahwa hasil peralihan dapat diselamatkan.

Christopher Oezbek
sumber
2
+1 Besar untuk contoh kode dengan semantik dalam komentar. Jawaban teratas lainnya mendefinisikan std :: move menggunakan "move" sendiri - tidak benar-benar menjelaskan apa pun! --- Saya percaya perlu disebutkan bahwa tidak membuat salinan argumen berarti bahwa nilai asli tidak dapat digunakan secara andal.
ty
34

std :: move itu sendiri tidak banyak membantu. Saya berpikir bahwa itu disebut konstruktor bergerak untuk objek, tetapi itu benar-benar hanya melakukan tipe casting (casting variabel lvalue ke rvalue sehingga variabel tersebut dapat diteruskan sebagai argumen ke konstruktor bergerak atau operator penugasan).

Jadi std :: move hanya digunakan sebagai prekursor untuk menggunakan semantik bergerak. Pindah semantik pada dasarnya adalah cara yang efisien untuk berurusan dengan benda-benda sementara.

Pertimbangkan Objek A = B + C + D + E + F;

Ini adalah kode yang terlihat bagus, tetapi E + F menghasilkan objek sementara. Kemudian D + temp menghasilkan objek sementara lainnya dan seterusnya. Di setiap operator "+" normal suatu kelas, salinan dalam terjadi.

Sebagai contoh

Object Object::operator+ (const Object& rhs) {
    Object temp (*this);
    // logic for adding
    return temp;
}

Penciptaan objek sementara dalam fungsi ini tidak berguna - objek sementara ini akan dihapus pada akhir baris karena mereka keluar dari ruang lingkup.

Kita bisa menggunakan move semantik untuk "menjarah" objek sementara dan melakukan sesuatu seperti

 Object& Object::operator+ (Object&& rhs) {
     // logic to modify rhs directly
     return rhs;
 }

Ini menghindari salinan dalam yang tidak perlu dibuat. Dengan mengacu pada contoh, satu-satunya bagian di mana penyalinan dalam terjadi sekarang E + F. Sisanya menggunakan semantik bergerak. Konstruktor bergerak atau operator penugasan juga perlu diimplementasikan untuk menetapkan hasilnya ke A.

pengguna929404
sumber
3
Anda berbicara tentang pindah semantik. Anda harus menambahkan jawaban Anda bagaimana std :: move dapat digunakan karena pertanyaannya tentang itu.
Koushik Shetty
2
@Koushik std :: move tidak melakukan banyak hal - tetapi digunakan untuk mengimplementasikan semantik langkah. Jika Anda tidak tahu tentang std :: move, Anda mungkin juga tidak tahu tentang memindahkan semantik
user929404
1
"Tidak melakukan banyak" (ya hanya static_cast ke referensi nilai). apa yang sebenarnya ia lakukan dan yang dilakukannya adalah apa yang diminta OP. Anda tidak perlu tahu bagaimana std :: move bekerja tetapi Anda harus tahu apa yang dilakukan semantik bergerak. lebih jauh lagi, "tetapi digunakan untuk mengimplementasikan semantik bergerak" dengan cara lain. tahu semantik bergerak dan Anda akan mengerti std :: pindahkan sebaliknya tidak. Langkah hanya membantu dalam gerakan dan itu sendiri menggunakan semantik bergerak. std :: move tidak melakukan apa pun selain mengubah argumennya menjadi rvalue referensi, yang diperlukan oleh semantik bergerak.
Koushik Shetty
10
"tetapi E + F menghasilkan objek sementara" - Operator +berbelok ke kiri ke kanan, bukan ke kanan ke kiri. Maka B+Cakan menjadi yang pertama!
Ajay
8

"Apa itu?" dan "Apa fungsinya?" telah dijelaskan di atas.

Saya akan memberikan contoh "kapan harus digunakan".

Sebagai contoh, kami memiliki kelas dengan banyak sumber daya seperti array besar di dalamnya.

class ResHeavy{ //  ResHeavy means heavy resource
    public:
        ResHeavy(int len=10):_upInt(new int[len]),_len(len){
            cout<<"default ctor"<<endl;
        }

        ResHeavy(const ResHeavy& rhs):_upInt(new int[rhs._len]),_len(rhs._len){
            cout<<"copy ctor"<<endl;
        }

        ResHeavy& operator=(const ResHeavy& rhs){
            _upInt.reset(new int[rhs._len]);
            _len = rhs._len;
            cout<<"operator= ctor"<<endl;
        }

        ResHeavy(ResHeavy&& rhs){
            _upInt = std::move(rhs._upInt);
            _len = rhs._len;
            rhs._len = 0;
            cout<<"move ctor"<<endl;
        }

    // check array valid
    bool is_up_valid(){
        return _upInt != nullptr;
    }

    private:
        std::unique_ptr<int[]> _upInt; // heavy array resource
        int _len; // length of int array
};

Kode uji:

void test_std_move2(){
    ResHeavy rh; // only one int[]
    // operator rh

    // after some operator of rh, it becomes no-use
    // transform it to other object
    ResHeavy rh2 = std::move(rh); // rh becomes invalid

    // show rh, rh2 it valid
    if(rh.is_up_valid())
        cout<<"rh valid"<<endl;
    else
        cout<<"rh invalid"<<endl;

    if(rh2.is_up_valid())
        cout<<"rh2 valid"<<endl;
    else
        cout<<"rh2 invalid"<<endl;

    // new ResHeavy object, created by copy ctor
    ResHeavy rh3(rh2);  // two copy of int[]

    if(rh3.is_up_valid())
        cout<<"rh3 valid"<<endl;
    else
        cout<<"rh3 invalid"<<endl;
}

Output seperti di bawah ini:

default ctor
move ctor
rh invalid
rh2 valid
copy ctor
rh3 valid

Kita dapat melihat bahwa std::movedengan move constructormembuat transformasi sumber daya dengan mudah.

Di mana lagi yang std::moveberguna?

std::movejuga bisa berguna saat menyortir berbagai elemen. Banyak algoritme pengurutan (seperti sortasi pemilihan dan sortir gelembung) bekerja dengan menukar pasangan elemen. Sebelumnya, kami harus menggunakan copy-semantik untuk bertukar. Sekarang kita dapat menggunakan semantik bergerak, yang lebih efisien.

Ini juga bisa berguna jika kita ingin memindahkan konten yang dikelola oleh satu penunjuk pintar ke yang lain.

Dikutip:

Jayhello
sumber
0

Berikut adalah contoh lengkap, menggunakan std :: move untuk vektor kustom (sederhana)

Output yang diharapkan:

 c: [10][11]
 copy ctor called
 copy of c: [10][11]
 move ctor called
 moved c: [10][11]

Kompilasi sebagai:

  g++ -std=c++2a -O2 -Wall -pedantic foo.cpp

Kode:

#include <iostream>
#include <algorithm>

template<class T> class MyVector {
private:
    T *data;
    size_t maxlen;
    size_t currlen;
public:
    MyVector<T> () : data (nullptr), maxlen(0), currlen(0) { }
    MyVector<T> (int maxlen) : data (new T [maxlen]), maxlen(maxlen), currlen(0) { }

    MyVector<T> (const MyVector& o) {
        std::cout << "copy ctor called" << std::endl;
        data = new T [o.maxlen];
        maxlen = o.maxlen;
        currlen = o.currlen;
        std::copy(o.data, o.data + o.maxlen, data);
    }

    MyVector<T> (const MyVector<T>&& o) {
        std::cout << "move ctor called" << std::endl;
        data = o.data;
        maxlen = o.maxlen;
        currlen = o.currlen;
    }

    void push_back (const T& i) {
        if (currlen >= maxlen) {
            maxlen *= 2;
            auto newdata = new T [maxlen];
            std::copy(data, data + currlen, newdata);
            if (data) {
                delete[] data;
            }
            data = newdata;
        }
        data[currlen++] = i;
    }

    friend std::ostream& operator<<(std::ostream &os, const MyVector<T>& o) {
        auto s = o.data;
        auto e = o.data + o.currlen;;
        while (s < e) {
            os << "[" << *s << "]";
            s++;
        }
        return os;
    }
};

int main() {
    auto c = new MyVector<int>(1);
    c->push_back(10);
    c->push_back(11);
    std::cout << "c: " << *c << std::endl;
    auto d = *c;
    std::cout << "copy of c: " << d << std::endl;
    auto e = std::move(*c);
    delete c;
    std::cout << "moved c: " << e << std::endl;
}
Neil McGill
sumber