Apa itu semantik bergerak?

1703

Saya baru saja selesai mendengarkan wawancara podcast radio Rekayasa Perangkat Lunak dengan Scott Meyers mengenai C ++ 0x . Sebagian besar fitur baru masuk akal bagi saya, dan saya benar-benar bersemangat tentang C ++ 0x sekarang, dengan pengecualian satu. Saya masih tidak bisa bergerak semantik ... Apa itu sebenarnya?

dicroce
sumber
20
Saya menemukan [artikel blog Eli Bendersky] ( eli.thegreenplace.net/2011/12/15/... ) tentang nilai dan nilai dalam C dan C ++ cukup informatif. Dia juga menyebutkan referensi nilai dalam C ++ 11 dan memperkenalkan mereka dengan contoh-contoh kecil.
Nils
16
Eksposisi Alex Allain tentang topik ini ditulis dengan sangat baik.
Patrick Sanan
19
Setiap tahun saya bertanya-tanya apa arti semantik bergerak "baru" dalam C ++, saya google dan membuka halaman ini. Saya membaca jawabannya, otak saya mati. Saya kembali ke C, dan melupakan semuanya! Saya menemui jalan buntu.
langit
7
@sky Pertimbangkan std :: vector <> ... Di suatu tempat di sana ada pointer ke array di heap. Jika Anda menyalin objek ini, buffer baru harus dialokasikan dan data dari buffer perlu disalin ke buffer baru. Apakah ada keadaan di mana tidak apa-apa untuk hanya mencuri pointer? Jawabannya adalah YA, ketika kompiler tahu objek itu sementara. Pindah semantik memungkinkan Anda untuk menentukan bagaimana nyali kelas Anda dapat dipindahkan dan jatuh di objek yang berbeda ketika kompiler tahu objek yang Anda pindahkan akan segera pergi.
dicroce
Satu-satunya referensi yang dapat saya pahami: learncpp.com/cpp-tutorial/… , yaitu alasan awal semantik bergerak adalah dari smart pointer.
jw_

Jawaban:

2481

Saya merasa paling mudah untuk memahami semantik bergerak dengan kode contoh. Mari kita mulai dengan kelas string yang sangat sederhana yang hanya memegang pointer ke blok memori yang dialokasikan heap:

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = std::strlen(p) + 1;
        data = new char[size];
        std::memcpy(data, p, size);
    }

Karena kita memilih untuk mengelola ingatan kita sendiri, kita perlu mengikuti aturan ketiganya . Saya akan menunda menulis operator penugasan dan hanya mengimplementasikan destructor dan copy constructor untuk saat ini:

    ~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = std::strlen(that.data) + 1;
        data = new char[size];
        std::memcpy(data, that.data, size);
    }

Copy constructor mendefinisikan apa artinya menyalin objek string. Parameter const string& thatmengikat semua ekspresi string tipe yang memungkinkan Anda untuk membuat salinan dalam contoh berikut:

string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3

Sekarang tiba wawasan kunci ke dalam semantik bergerak. Perhatikan bahwa hanya pada baris pertama di mana kita menyalin, salinan xyang dalam ini benar-benar diperlukan, karena kita mungkin ingin memeriksanya xnanti dan akan sangat terkejut jika xentah bagaimana telah berubah. Apakah Anda memperhatikan bagaimana saya hanya mengatakan xtiga kali (empat kali jika Anda memasukkan kalimat ini) dan berarti objek yang sama persis setiap kali? Kami menyebut ekspresi seperti x"nilai".

Argumen di baris 2 dan 3 bukanlah nilai, tetapi nilai, karena objek string yang mendasari tidak memiliki nama, sehingga klien tidak memiliki cara untuk memeriksanya lagi pada titik waktu berikutnya. rvalues ​​menunjukkan objek sementara yang dihancurkan pada titik koma berikutnya (lebih tepatnya: pada akhir ekspresi penuh yang secara leksikal berisi nilai). Ini penting karena selama inisialisasi bdan c, kita bisa melakukan apa pun yang kita inginkan dengan string sumber, dan klien tidak dapat membedakannya !

C ++ 0x memperkenalkan mekanisme baru yang disebut "referensi nilai" yang, antara lain, memungkinkan kami untuk mendeteksi argumen nilai melalui fungsi yang berlebihan. Yang harus kita lakukan adalah menulis konstruktor dengan parameter referensi nilai. Di dalam konstruktor bahwa kita bisa melakukan apapun yang kita inginkan dengan sumber, selama kita meninggalkannya di beberapa negara yang valid:

    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }

Apa yang sudah kita lakukan di sini? Alih-alih menyalin secara mendalam data tumpukan, kami hanya menyalin pointer dan kemudian mengatur pointer asli ke nol (untuk mencegah 'hapus []' dari destruktor objek sumber dari merilis 'data yang baru saja dicuri'). Akibatnya, kami telah "mencuri" data yang semula milik string sumber. Sekali lagi, wawasan kuncinya adalah bahwa dalam keadaan apa pun klien tidak dapat mendeteksi bahwa sumber telah dimodifikasi. Karena kami tidak benar-benar menyalin di sini, kami menyebut konstruktor ini sebagai "pindahkan konstruktor". Tugasnya adalah memindahkan sumber daya dari satu objek ke objek lain alih-alih menyalinnya.

Selamat, Anda sekarang mengerti dasar-dasar pindahan semantik! Mari kita lanjutkan dengan mengimplementasikan operator penugasan. Jika Anda tidak terbiasa dengan idiom copy dan swap , pelajari dan kembali, karena ini adalah idiom C ++ yang luar biasa terkait dengan keamanan pengecualian.

    string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};

Hah, itu dia? "Di mana referensi nilainya?" Anda mungkin bertanya. "Kami tidak membutuhkannya di sini!" adalah jawaban saya :)

Perhatikan bahwa kita melewatkan parameter that dengan nilai , jadi thatharus diinisialisasi sama seperti objek string lainnya. Bagaimana tepatnya thatakan diinisialisasi? Di masa lalu C ++ 98 , jawabannya adalah "oleh copy constructor". Dalam C ++ 0x, kompiler memilih antara copy constructor dan move constructor berdasarkan pada apakah argumen kepada operator penugasan adalah lvalue atau rvalue.

Jadi jika Anda mengatakan a = b, konstruktor salin akan menginisialisasi that(karena ekspresi badalah nilai), dan operator penugasan menukar konten dengan salinan yang baru dibuat, dalam. Itulah definisi idiom copy dan swap - buat salinan, tukar konten dengan salinan, lalu singkirkan salinan dengan meninggalkan ruang lingkup. Tidak ada yang baru di sini.

Tetapi jika Anda mengatakan a = x + y, konstruktor bergerak akan menginisialisasi that(karena ekspresi x + yadalah nilai), sehingga tidak ada salinan yang terlibat, hanya langkah efisien. thatmasih merupakan objek independen dari argumen, tetapi konstruksinya sepele, karena tumpukan data tidak harus disalin, hanya dipindahkan. Tidak perlu menyalinnya karena x + ymerupakan nilai, dan sekali lagi, tidak apa-apa untuk bergerak dari objek string yang dilambangkan dengan nilai.

Untuk meringkas, konstruktor salinan membuat salinan yang dalam, karena sumbernya harus tetap tidak tersentuh. Konstruktor pemindahan, di sisi lain, cukup menyalin pointer dan kemudian mengatur pointer di sumber ke nol. Tidak masalah untuk "membatalkan" objek sumber dengan cara ini, karena klien tidak memiliki cara untuk memeriksa objek lagi.

Saya harap contoh ini sampai di titik utama. Ada banyak lagi untuk menilai kembali referensi dan memindahkan semantik yang sengaja saya tinggalkan agar tetap sederhana. Jika Anda ingin detail lebih lanjut, lihat jawaban pelengkap saya .

fredoverflow
sumber
40
@Tetapi jika ctor saya mendapatkan nilai, yang tidak akan pernah bisa digunakan nanti, mengapa saya bahkan perlu repot-repot meninggalkannya dalam keadaan yang konsisten / aman? Alih-alih mengatur that.data = 0, mengapa tidak membiarkannya saja?
einpoklum
70
@einpoklum Karena tanpa that.data = 0, karakter akan dihancurkan terlalu dini (ketika sementara mati), dan juga dua kali. Anda ingin mencuri data, bukan membagikannya!
fredoverflow
19
@einpoklum Destroyer yang dijadwalkan secara teratur masih berjalan, jadi Anda harus memastikan bahwa keadaan pasca-pindah objek sumber tidak menyebabkan kerusakan. Lebih baik, Anda harus memastikan objek sumber juga bisa menjadi penerima tugas atau tulisan lainnya.
CTMacUser
12
@pranitkothari Ya, semua objek harus dihancurkan, bahkan dipindahkan dari objek. Dan karena kita tidak ingin array char dihapus ketika itu terjadi, kita harus mengatur pointer ke nol.
fredoverflow
7
@ Virus721 delete[]pada nullptr didefinisikan oleh standar C ++ sebagai no-op.
fredoverflow
1057

Jawaban pertama saya adalah pengantar yang sangat disederhanakan untuk memindahkan semantik, dan banyak detail yang sengaja dibuat untuk membuatnya tetap sederhana. Namun, ada banyak lagi untuk memindahkan semantik, dan saya pikir sudah waktunya untuk jawaban kedua untuk mengisi kekosongan. Jawaban pertama sudah cukup lama, dan rasanya tidak benar hanya menggantinya dengan teks yang sama sekali berbeda. Saya pikir ini masih berfungsi dengan baik sebagai pengantar pertama. Tetapi jika Anda ingin menggali lebih dalam, baca terus :)

Stephan T. Lavavej meluangkan waktu untuk memberikan umpan balik yang berharga. Terima kasih banyak, Stephan!

pengantar

Pindah semantik memungkinkan suatu objek, dalam kondisi tertentu, untuk mengambil kepemilikan sumber daya eksternal beberapa objek lain. Ini penting dalam dua cara:

  1. Mengubah salinan mahal menjadi gerakan murah. Lihat jawaban pertama saya sebagai contoh. Perhatikan bahwa jika suatu objek tidak mengelola setidaknya satu sumber daya eksternal (baik secara langsung, atau tidak langsung melalui objek anggotanya), pindahkan semantik tidak akan menawarkan keuntungan apa pun dibandingkan salinan semantik. Dalam hal itu, menyalin objek dan memindahkan objek berarti hal yang sama persis:

    class cannot_benefit_from_move_semantics
    {
        int a;        // moving an int means copying an int
        float b;      // moving a float means copying a float
        double c;     // moving a double means copying a double
        char d[64];   // moving a char array means copying a char array
    
        // ...
    };
  2. Menerapkan tipe aman "hanya bergerak"; yaitu, jenis-jenis yang penyalinannya tidak masuk akal, tetapi pemindahan tidak. Contohnya termasuk kunci, pegangan file, dan petunjuk pintar dengan semantik kepemilikan yang unik. Catatan: Jawaban ini membahas std::auto_ptr, templat perpustakaan standar C ++ 98 yang sudah tidak digunakan lagi, yang digantikan oleh std::unique_ptrdalam C ++ 11. Pemrogram C ++ menengah mungkin paling tidak agak familiar std::auto_ptr, dan karena "pindahkan semantik" yang ditampilkannya, sepertinya merupakan titik awal yang baik untuk membahas pindahan semantik di C ++ 11. YMMV.

Apa itu langkah?

Pustaka standar C ++ 98 menawarkan penunjuk cerdas dengan semantik kepemilikan unik yang disebut std::auto_ptr<T>. Jika Anda belum terbiasa auto_ptr, tujuannya adalah untuk menjamin bahwa objek yang dialokasikan secara dinamis selalu dirilis, bahkan dalam menghadapi pengecualian:

{
    std::auto_ptr<Shape> a(new Triangle);
    // ...
    // arbitrary code, could throw exceptions
    // ...
}   // <--- when a goes out of scope, the triangle is deleted automatically

Hal yang tidak biasa auto_ptradalah perilaku "menyalin":

auto_ptr<Shape> a(new Triangle);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        |
        |
  +-----|---+
  |   +-|-+ |
a | p | | | |
  |   +---+ |
  +---------+

auto_ptr<Shape> b(a);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        +----------------------+
                               |
  +---------+            +-----|---+
  |   +---+ |            |   +-|-+ |
a | p |   | |          b | p | | | |
  |   +---+ |            |   +---+ |
  +---------+            +---------+

Perhatikan bagaimana inisialisasi bdengan atidak tidak menyalin segitiga, melainkan transfer kepemilikan segitiga dari ake b. Kami juga mengatakan " aini dipindahkan ke b " atau "segitiga tersebut dipindahkan dari a ke b ". Ini mungkin terdengar membingungkan karena segitiga itu sendiri selalu berada di tempat yang sama dalam memori.

Untuk memindahkan objek berarti memindahkan kepemilikan atas beberapa sumber daya yang dikelolanya ke objek lain.

Pembuat salinan dari auto_ptrmungkin terlihat seperti ini (agak disederhanakan):

auto_ptr(auto_ptr& source)   // note the missing const
{
    p = source.p;
    source.p = 0;   // now the source no longer owns the object
}

Bergerak berbahaya dan tidak berbahaya

Yang berbahaya tentang auto_ptrapa yang secara sintaksis tampak seperti salinan sebenarnya adalah sebuah langkah. Mencoba memanggil fungsi anggota pada pindah-dari auto_ptrakan memanggil perilaku yang tidak terdefinisi, jadi Anda harus sangat berhati-hati untuk tidak menggunakan auto_ptrsetelah itu telah dipindahkan dari:

auto_ptr<Shape> a(new Triangle);   // create triangle
auto_ptr<Shape> b(a);              // move a into b
double area = a->area();           // undefined behavior

Namun auto_ptrtidak selalu berbahaya. Fungsi pabrik adalah kasus penggunaan yang sangat baik untuk auto_ptr:

auto_ptr<Shape> make_triangle()
{
    return auto_ptr<Shape>(new Triangle);
}

auto_ptr<Shape> c(make_triangle());      // move temporary into c
double area = make_triangle()->area();   // perfectly safe

Perhatikan bagaimana kedua contoh mengikuti pola sintaksis yang sama:

auto_ptr<Shape> variable(expression);
double area = expression->area();

Namun, salah satu dari mereka memanggil perilaku yang tidak terdefinisi, sedangkan yang lain tidak. Jadi apa perbedaan antara ekspresi adan make_triangle()? Bukankah keduanya memiliki tipe yang sama? Memang benar, tetapi mereka memiliki kategori nilai yang berbeda .

Kategori nilai

Jelas, harus ada beberapa perbedaan besar antara ekspresi ayang menunjukkan auto_ptrvariabel, dan ekspresi make_triangle()yang menunjukkan panggilan fungsi yang mengembalikan auto_ptrnilai, sehingga menciptakan auto_ptrobjek sementara yang baru setiap kali dipanggil. aadalah contoh dari sebuah lvalue , sedangkan make_triangle()adalah contoh dari nilai p .

Pindah dari nilai-nilai seperti aitu berbahaya, karena kita nanti bisa mencoba memanggil fungsi anggota melalui a, memanggil perilaku yang tidak terdefinisi. Di sisi lain, pindah dari nilai-nilai seperti make_triangle()sangat aman, karena setelah copy constructor melakukan tugasnya, kita tidak bisa menggunakan yang sementara lagi. Tidak ada ungkapan yang menunjukkan kata sementara; jika kita hanya menulis make_triangle()lagi, kita mendapatkan perbedaan sementara. Bahkan, pindah-dari sementara sudah hilang di baris berikutnya:

auto_ptr<Shape> c(make_triangle());
                                  ^ the moved-from temporary dies right here

Perhatikan bahwa surat-surat ldan rmemiliki asal bersejarah di sisi kiri dan kanan tugas. Ini tidak lagi benar dalam C ++, karena ada nilai yang tidak dapat muncul di sisi kiri suatu tugas (seperti array atau tipe yang ditentukan pengguna tanpa operator penugasan), dan ada nilai yang bisa (semua nilai dari tipe kelas dengan operator penugasan).

Nilai dari tipe kelas adalah ekspresi yang evaluasinya menciptakan objek sementara. Dalam keadaan normal, tidak ada ekspresi lain di dalam lingkup yang sama menunjukkan objek sementara yang sama.

Referensi nilai

Kita sekarang mengerti bahwa pindah dari nilai-nilai berpotensi berbahaya, tetapi bergerak dari nilai-nilai tidak berbahaya. Jika C ++ memiliki dukungan bahasa untuk membedakan argumen lvalue dari argumen rvalue, kami dapat melarang sepenuhnya pindah dari lvalues, atau setidaknya membuat bergerak dari lvalues secara eksplisit di situs panggilan, sehingga kami tidak lagi bergerak secara tidak sengaja.

Jawaban C ++ 11 untuk masalah ini adalah referensi nilai . Referensi nilai adalah jenis referensi baru yang hanya mengikat nilai, dan sintaksnya X&&. Referensi lama yang baik X&sekarang dikenal sebagai referensi nilai . (Perhatikan bahwa X&&adalah tidak referensi ke referensi, tidak ada hal seperti itu di C ++.)

Jika kita memasukkan constcampuran, kita sudah memiliki empat jenis referensi. Jenis ekspresi seperti apa yang Xbisa mereka ikat?

            lvalue   const lvalue   rvalue   const rvalue
---------------------------------------------------------              
X&          yes
const X&    yes      yes            yes      yes
X&&                                 yes
const X&&                           yes      yes

Dalam praktiknya, Anda bisa melupakannya const X&&. Terbatas untuk membaca dari nilai tidak sangat berguna.

Referensi nilai X&&adalah jenis referensi baru yang hanya mengikat nilai saja.

Konversi tersirat

Referensi nilai melewati beberapa versi. Sejak versi 2.1, referensi nilai X&&juga mengikat semua kategori nilai dari jenis yang berbeda Y, asalkan ada konversi implisit dari Yke X. Dalam hal itu, sementara jenis Xdibuat, dan referensi nilai terikat untuk sementara itu:

void some_function(std::string&& r);

some_function("hello world");

Dalam contoh di atas, "hello world"adalah lvalue tipe const char[12]. Karena ada konversi implisit dari const char[12]melalui const char*ke std::string, sebuah sementara tipe std::stringdibuat, dan rpasti bahwa sementara. Ini adalah salah satu kasus di mana perbedaan antara rvalues ​​(ekspresi) dan temporaries (objek) agak buram.

Pindahkan konstruktor

Contoh berguna dari fungsi dengan X&&parameter adalah move constructor X::X(X&& source) . Tujuannya adalah untuk mentransfer kepemilikan sumber daya yang dikelola dari sumber ke objek saat ini.

Dalam C ++ 11, std::auto_ptr<T>telah digantikan oleh std::unique_ptr<T>yang mengambil keuntungan dari referensi nilai. Saya akan mengembangkan dan mendiskusikan versi sederhana dari unique_ptr. Pertama, kami merangkum pointer mentah dan membebani operator ->dan *, jadi kelas kami terasa seperti pointer:

template<typename T>
class unique_ptr
{
    T* ptr;

public:

    T* operator->() const
    {
        return ptr;
    }

    T& operator*() const
    {
        return *ptr;
    }

Konstruktor mengambil kepemilikan objek, dan destruktor menghapusnya:

    explicit unique_ptr(T* p = nullptr)
    {
        ptr = p;
    }

    ~unique_ptr()
    {
        delete ptr;
    }

Sekarang sampai pada bagian yang menarik, move constructor:

    unique_ptr(unique_ptr&& source)   // note the rvalue reference
    {
        ptr = source.ptr;
        source.ptr = nullptr;
    }

Konstruktor gerakan ini melakukan persis apa yang dilakukan auto_ptrkonstruktor salin, tetapi hanya dapat diberikan dengan nilai:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);                 // error
unique_ptr<Shape> c(make_triangle());   // okay

Baris kedua gagal dikompilasi, karena amerupakan nilai lv, tetapi parameter unique_ptr&& sourcehanya dapat terikat ke nilai. Inilah yang kami inginkan; gerakan berbahaya tidak boleh implisit. Baris ketiga mengkompilasi dengan baik, karena make_triangle()merupakan nilai. Konstruktor pemindahan akan mengalihkan kepemilikan dari sementara ke c. Sekali lagi, inilah yang kami inginkan.

Konstruktor pemindahan memindahkan kepemilikan sumber daya yang dikelola ke objek saat ini.

Pindahkan operator penugasan

Bagian yang hilang terakhir adalah operator penugasan pindah. Tugasnya adalah melepaskan sumber daya lama dan memperoleh sumber daya baru dari argumennya:

    unique_ptr& operator=(unique_ptr&& source)   // note the rvalue reference
    {
        if (this != &source)    // beware of self-assignment
        {
            delete ptr;         // release the old resource

            ptr = source.ptr;   // acquire the new resource
            source.ptr = nullptr;
        }
        return *this;
    }
};

Perhatikan bagaimana implementasi operator penugasan langkah ini menduplikasi logika destruktor dan konstruktor bergerak. Apakah Anda terbiasa dengan idiom copy-and-swap? Ini juga dapat diterapkan untuk memindahkan semantik sebagai idiom move-and-swap:

    unique_ptr& operator=(unique_ptr source)   // note the missing reference
    {
        std::swap(ptr, source.ptr);
        return *this;
    }
};

Sekarang itu sourceadalah variabel tipe unique_ptr, itu akan diinisialisasi oleh move constructor; artinya, argumen akan dipindahkan ke parameter. Argumen masih diperlukan untuk menjadi nilai, karena konstruktor bergerak itu sendiri memiliki parameter referensi nilai. Ketika aliran kontrol mencapai penjepit penutupan operator=, sourcekeluar dari ruang lingkup, melepaskan sumber daya lama secara otomatis.

Operator penugasan memindahkan kepemilikan dari sumber daya yang dikelola ke objek saat ini, melepaskan sumber daya lama. Ungkapan move-and-swap mempermudah implementasi.

Pindah dari nilai-nilai

Terkadang, kami ingin beralih dari nilai-nilai. Yaitu, kadang-kadang kita ingin kompiler memperlakukan nilai l seolah-olah itu nilai, sehingga dapat memanggil move constructor, meskipun itu bisa berpotensi tidak aman. Untuk tujuan ini, C ++ 11 menawarkan templat fungsi pustaka standar yang disebut std::movedi dalam header <utility>. Nama ini agak disayangkan, karena std::movehanya melemparkan nilai ke nilai; itu tidak memindahkan apa pun dengan sendirinya. Itu hanya memungkinkan bergerak. Mungkin seharusnya dinamai std::cast_to_rvalueatau std::enable_move, tapi kita terjebak dengan namanya sekarang.

Inilah cara Anda secara eksplisit beralih dari nilai:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);              // still an error
unique_ptr<Shape> c(std::move(a));   // okay

Perhatikan bahwa setelah baris ketiga, atidak lagi memiliki segitiga. Tidak apa-apa, karena dengan menulis secara eksplisitstd::move(a) , kami membuat niat kami jelas: "Konstruktor yang terhormat, lakukan apa pun yang Anda inginkan auntuk menginisialisasi c; Saya tidak peduli alagi. Jangan ragu untuk membantu Anda a."

std::move(some_lvalue) melemparkan nilai ke nilai, sehingga memungkinkan langkah selanjutnya.

Nilai X

Perhatikan bahwa meskipun std::move(a)merupakan nilai, evaluasinya tidak membuat objek sementara. Teka-teki ini memaksa komite untuk memperkenalkan kategori nilai ketiga. Sesuatu yang dapat terikat pada referensi nilai, meskipun itu bukan nilai dalam arti tradisional, disebut nilai x (nilai eXpiring). Nilai-nilai tradisional diubah namanya menjadi nilai-nilai (Nilai murni).

Baik prvalues ​​dan xvalues ​​adalah rvalues. Xvalues ​​dan lvalues ​​keduanya adalah glvalues (Generalized lvalues). Hubungan lebih mudah dipahami dengan diagram:

        expressions
          /     \
         /       \
        /         \
    glvalues   rvalues
      /  \       /  \
     /    \     /    \
    /      \   /      \
lvalues   xvalues   prvalues

Perhatikan bahwa hanya nilai x yang benar-benar baru; sisanya hanya karena penggantian nama dan pengelompokan.

Nilai C ++ 98 dikenal sebagai nilai di C ++ 11. Gantikan secara mental semua kemunculan "nilai" pada paragraf sebelumnya dengan "nilai".

Keluar dari fungsi

Sejauh ini, kita telah melihat pergerakan ke variabel lokal, dan ke dalam parameter fungsi. Tetapi bergerak juga mungkin terjadi dalam arah yang berlawanan. Jika fungsi kembali berdasarkan nilai, beberapa objek di situs panggilan (mungkin variabel lokal atau sementara, tetapi bisa berupa objek apa pun) diinisialisasi dengan ekspresi setelah returnpernyataan sebagai argumen ke konstruktor bergerak:

unique_ptr<Shape> make_triangle()
{
    return unique_ptr<Shape>(new Triangle);
}          \-----------------------------/
                  |
                  | temporary is moved into c
                  |
                  v
unique_ptr<Shape> c(make_triangle());

Mungkin mengejutkan, objek otomatis (variabel lokal yang tidak dideklarasikan sebagai static) juga dapat secara implisit dipindahkan dari fungsi:

unique_ptr<Shape> make_square()
{
    unique_ptr<Shape> result(new Square);
    return result;   // note the missing std::move
}

Kenapa konstruktor langkah menerima nilai resultsebagai argumen? Ruang lingkup resultakan berakhir, dan itu akan hancur selama tumpukan unwinding. Tidak ada yang bisa mengeluh setelah itu yang resultentah bagaimana berubah; ketika aliran kontrol kembali ke pemanggil, resulttidak ada lagi! Untuk alasan itu, C ++ 11 memiliki aturan khusus yang memungkinkan pengembalian objek otomatis dari fungsi tanpa harus menulis std::move. Bahkan, Anda tidak boleh menggunakan std::moveuntuk memindahkan objek otomatis dari fungsi, karena ini menghambat "optimasi nilai pengembalian bernama" (NRVO).

Jangan pernah gunakan std::moveuntuk memindahkan objek otomatis dari fungsi.

Perhatikan bahwa di kedua fungsi pabrik, tipe pengembalian adalah nilai, bukan referensi nilai. Referensi nilai masih referensi, dan seperti biasa, Anda tidak boleh mengembalikan referensi ke objek otomatis; penelepon akan berakhir dengan referensi menggantung jika Anda menipu kompiler agar menerima kode Anda, seperti ini:

unique_ptr<Shape>&& flawed_attempt()   // DO NOT DO THIS!
{
    unique_ptr<Shape> very_bad_idea(new Square);
    return std::move(very_bad_idea);   // WRONG!
}

Jangan pernah mengembalikan objek otomatis dengan referensi nilai. Bergerak secara eksklusif dilakukan oleh move constructor, bukan oleh std::move, dan bukan hanya dengan mengikat nilai ke referensi nilai.

Pindah ke anggota

Cepat atau lambat, Anda akan menulis kode seperti ini:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(parameter)   // error
    {}
};

Pada dasarnya, kompiler akan mengeluh bahwa itu parameteradalah nilai. Jika Anda melihat tipenya, Anda melihat referensi nilai, tetapi referensi nilai berarti "referensi yang terikat dengan nilai"; itu tidak berarti bahwa referensi itu sendiri adalah nilai! Memang, parameterhanya variabel biasa dengan nama. Anda dapat menggunakan parametersesering mungkin di dalam tubuh konstruktor, dan selalu menunjukkan objek yang sama. Secara implisit pindah darinya akan berbahaya, karena itu bahasa melarangnya.

Referensi nilai yang dinamai adalah nilai, seperti halnya variabel lainnya.

Solusinya adalah mengaktifkan langkah secara manual:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(std::move(parameter))   // note the std::move
    {}
};

Anda bisa membantah bahwa parametertidak digunakan lagi setelah inisialisasi member. Mengapa tidak ada aturan khusus untuk memasukkan secara diam-diam std::moveseperti halnya dengan nilai pengembalian? Mungkin karena akan terlalu banyak beban pada implementor compiler. Misalnya, bagaimana jika badan konstruktor ada di unit terjemahan lain? Sebaliknya, aturan nilai balik hanya perlu memeriksa tabel simbol untuk menentukan apakah pengidentifikasi setelah returnkata kunci menunjukkan objek otomatis.

Anda juga dapat melewati nilai parameterberdasarkan. Untuk tipe bergerak saja seperti unique_ptr, sepertinya belum ada idiom yang mapan. Secara pribadi, saya lebih suka memberikan nilai, karena menyebabkan lebih sedikit kekacauan dalam antarmuka.

Fungsi anggota khusus

C ++ 98 secara implisit mendeklarasikan tiga fungsi anggota khusus sesuai permintaan, yaitu, ketika dibutuhkan di suatu tempat: copy constructor, operator penugasan copy, dan destructor.

X::X(const X&);              // copy constructor
X& X::operator=(const X&);   // copy assignment operator
X::~X();                     // destructor

Referensi nilai melewati beberapa versi. Sejak versi 3.0, C ++ 11 menyatakan dua fungsi anggota khusus tambahan sesuai permintaan: konstruktor gerakan dan operator penugasan langkah. Perhatikan bahwa baik VC10 maupun VC11 belum sesuai dengan versi 3.0, jadi Anda harus mengimplementasikannya sendiri.

X::X(X&&);                   // move constructor
X& X::operator=(X&&);        // move assignment operator

Dua fungsi anggota khusus yang baru ini hanya secara implisit dinyatakan jika tidak ada fungsi anggota khusus yang dinyatakan secara manual. Juga, jika Anda mendeklarasikan move constructor Anda sendiri atau memindahkan operator penugasan, baik copy constructor maupun operator penugasan salinan tidak akan dinyatakan secara implisit.

Apa arti aturan ini dalam praktik?

Jika Anda menulis kelas tanpa sumber daya yang tidak dikelola, Anda tidak perlu mendeklarasikan salah satu dari lima fungsi anggota khusus itu sendiri, dan Anda akan mendapatkan salinan semantik yang benar dan memindahkan semantik secara gratis. Jika tidak, Anda harus mengimplementasikan sendiri fungsi anggota khusus. Tentu saja, jika kelas Anda tidak mendapatkan manfaat dari semantik bergerak, tidak perlu menerapkan operasi langkah khusus.

Perhatikan bahwa operator penugasan salinan dan operator penugasan bergerak dapat digabungkan menjadi satu operator penugasan tunggal, yang mengambil argumen berdasarkan nilai:

X& X::operator=(X source)    // unified assignment operator
{
    swap(source);            // see my first answer for an explanation
    return *this;
}

Dengan cara ini, jumlah fungsi anggota khusus untuk mengimplementasikan turun dari lima menjadi empat. Ada tradeoff antara pengecualian-keselamatan dan efisiensi di sini, tapi saya bukan ahli dalam masalah ini.

Referensi penerusan ( sebelumnya dikenal sebagai referensi Universal )

Pertimbangkan templat fungsi berikut:

template<typename T>
void foo(T&&);

Anda mungkin berharap T&&hanya mengikat nilai, karena pada pandangan pertama, itu terlihat seperti referensi nilai. Namun ternyata, T&&juga mengikat nilai-nilai:

foo(make_triangle());   // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a);                 // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&

Jika argumennya adalah nilai jenis X, Tdisimpulkan X, maka T&&berarti X&&. Inilah yang orang harapkan. Tetapi jika argumen adalah nilai jenis X, karena aturan khusus, Tdisimpulkan X&, maka T&&akan berarti sesuatu seperti X& &&. Tapi karena C ++ masih tidak memiliki gagasan referensi ke referensi, tipe X& &&ini diciutkan menjadi X&. Ini mungkin terdengar membingungkan dan tidak berguna pada awalnya, tetapi runtuh referensi sangat penting untuk penerusan yang sempurna (yang tidak akan dibahas di sini).

T&& bukanlah referensi nilai, tetapi referensi penerusan. Ini juga mengikat nilai-nilai, dalam hal ini Tdan T&&keduanya merupakan referensi nilai.

Jika Anda ingin membatasi templat fungsi ke nilai, Anda bisa menggabungkan SFINAE dengan tipe traits:

#include <type_traits>

template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);

Implementasi langkah

Sekarang setelah Anda memahami runtuhnya referensi, berikut ini cara std::movepenerapannya:

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

Seperti yang Anda lihat, movemenerima segala jenis parameter berkat referensi penerusan T&&, dan mengembalikan referensi nilai. The std::remove_reference<T>::typemeta-fungsi panggilan diperlukan karena jika tidak, untuk lvalues tipe X, jenis kembali akan X& &&, yang akan runtuh ke X&. Karena tselalu merupakan lvalue (ingat bahwa referensi rvalue bernama adalah lvalue), tetapi kami ingin mengikat tke rvalue referensi, kami harus secara eksplisit melemparkan tke tipe pengembalian yang benar. Panggilan fungsi yang mengembalikan referensi nilai sendiri merupakan nilai tambah. Sekarang Anda tahu dari mana nilai-nilai berasal;)

Panggilan fungsi yang mengembalikan referensi nilai, seperti std::move, adalah nilai x.

Perhatikan bahwa mengembalikan dengan referensi nilai baik-baik saja dalam contoh ini, karena ttidak menunjukkan objek otomatis, tetapi sebaliknya objek yang dilewatkan oleh pemanggil.

fredoverflow
sumber
24
Ada alasan ketiga mengapa semantik bergerak itu penting: keamanan pengecualian. Seringkali di mana operasi penyalinan dapat melempar (karena perlu mengalokasikan sumber daya dan alokasi mungkin gagal) operasi pemindahan bisa tanpa larangan (karena dapat mentransfer kepemilikan sumber daya yang ada alih-alih mengalokasikan yang baru). Memiliki operasi yang tidak dapat gagal selalu menyenangkan, dan itu bisa menjadi sangat penting ketika menulis kode yang memberikan jaminan pengecualian.
Brangdon
8
Saya bersama Anda sampai ke 'Referensi universal', tetapi kemudian semuanya terlalu abstrak untuk diikuti. Referensi runtuh? Penerusan sempurna? Apakah Anda mengatakan bahwa referensi nilai menjadi referensi universal jika jenisnya templated? Saya berharap ada cara untuk menjelaskan ini sehingga saya akan tahu apakah saya perlu memahaminya atau tidak! :)
Kylotan
8
Tolong tulis buku sekarang ... jawaban ini telah memberi saya alasan untuk percaya jika Anda membahas sudut-sudut C ++ lainnya dengan cara jernih seperti ini, ribuan orang akan memahaminya.
halivingston
12
@halivingston Terima kasih banyak atas tanggapan Anda, saya sangat menghargainya. Masalah dengan menulis buku adalah: itu jauh lebih sulit daripada yang bisa Anda bayangkan. Jika Anda ingin menggali lebih dalam ke C ++ 11 dan seterusnya, saya sarankan Anda membeli "C ++ Modern Efektif" oleh Scott Meyers.
fredoverflow
77

Pindah semantik didasarkan pada referensi nilai .
Nilai adalah objek sementara, yang akan dihancurkan pada akhir ekspresi. Dalam C ++ saat ini, nilai hanya mengikat ke constreferensi. C ++ 1x akan memungkinkan constreferensi non-nilai , dieja T&&, yang merupakan referensi ke objek nilai.
Karena nilai akan mati pada akhir ekspresi, Anda dapat mencuri datanya . Alih-alih menyalinnya ke objek lain, Anda memindahkan datanya ke dalamnya.

class X {
public: 
  X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
    : data_()
  {
     // since 'x' is an rvalue object, we can steal its data
     this->swap(std::move(rhs));
     // this will leave rhs with the empty data
  }
  void swap(X&& rhs);
  // ... 
};

// ...

X f();

X x = f(); // f() returns result as rvalue, so this calls move-ctor

Pada kode di atas, dengan kompiler lama hasil f()yang disalin ke dalam xmenggunakan X's copy constructor. Jika kompiler Anda mendukung pindahan semantik dan Xmemiliki pindahkan-konstruktor, maka itu disebut sebagai gantinya. Sejak rhsargumen adalah nilai p , kita tahu itu tidak diperlukan lagi dan kami bisa mencuri nilainya.
Jadi nilai dipindahkan dari sementara yang tidak disebutkan namanya dikembalikan dari f()ke x(sementara data x, diinisialisasi ke kosong X, dipindahkan ke sementara, yang akan hancur setelah penugasan).

sbi
sumber
1
perhatikan bahwa itu seharusnya this->swap(std::move(rhs));karena referensi nilai yang dinamai adalah nilai-nilai
wmamrak
Ini agak salah, per komentar @ Tacyt: rhsadalah nilai dalam konteks X::X(X&& rhs). Anda perlu menelepon std::move(rhs)untuk mendapatkan nilai, tetapi ini agak membuat jawabannya diperdebatkan.
Asherah
Apa yang memindahkan semantik untuk tipe tanpa pointer? Pindahkan semantik bekerja seperti salinan?
Gusev Slava
@ Gusev: Saya tidak tahu apa yang Anda minta.
sbi
60

Misalkan Anda memiliki fungsi yang mengembalikan objek besar:

Matrix multiply(const Matrix &a, const Matrix &b);

Ketika Anda menulis kode seperti ini:

Matrix r = multiply(a, b);

kemudian kompiler C ++ biasa akan membuat objek sementara untuk hasil multiply(), memanggil copy constructor untuk menginisialisasi r, dan kemudian merusak nilai pengembalian sementara. Memindahkan semantik di C ++ 0x memungkinkan "move constructor" dipanggil untuk menginisialisasi rdengan menyalin isinya, dan kemudian membuang nilai sementara tanpa harus merusaknya.

Ini sangat penting jika (seperti mungkin Matrixcontoh di atas), objek yang disalin mengalokasikan memori tambahan pada heap untuk menyimpan representasi internal. Pembuat salinan harus membuat salinan lengkap dari representasi internal, atau menggunakan penghitungan referensi dan semantik copy-on-write secara internal. Konstruktor bergerak akan meninggalkan memori tumpukan dan hanya menyalin pointer di dalam Matrixobjek.

Greg Hewgill
sumber
2
Bagaimana memindahkan konstruktor dan menyalin konstruktor berbeda?
dicroce
1
@dicroce: Mereka berbeda dengan sintaks, yang terlihat seperti Matrix (const Matrix & src) (copy constructor) dan yang lainnya seperti Matrix (Matrix && src) (move constructor), periksa jawaban utama saya untuk contoh yang lebih baik.
snk_kid
3
@dicroce: Satu membuat objek kosong, dan satu membuat salinan. Jika data yang disimpan dalam objek besar, salinannya bisa mahal. Misalnya, std :: vector.
Billy ONeal
1
@ kunj2aan: Tergantung pada kompiler Anda, saya kira. Kompiler dapat membuat objek sementara di dalam fungsi, dan kemudian memindahkannya ke nilai balik pemanggil. Atau, mungkin dapat langsung membangun objek dalam nilai kembali, tanpa perlu menggunakan konstruktor bergerak.
Greg Hewgill
2
@Jichao: Itu adalah optimasi yang disebut RVO, lihat pertanyaan ini untuk informasi lebih lanjut tentang perbedaan: stackoverflow.com/questions/5031778/…
Greg Hewgill
30

Jika Anda benar-benar tertarik dengan penjelasan yang baik dan mendalam tentang semantik langkah, saya sangat merekomendasikan membaca makalah asli pada mereka, "Sebuah Proposal untuk Menambahkan Dukungan Pindah Semantik ke Bahasa C ++."

Ini sangat mudah diakses dan mudah dibaca dan itu membuat kasus yang sangat baik untuk manfaat yang mereka tawarkan. Ada makalah lain yang lebih baru dan terkini tentang semantik bergerak yang tersedia di situs WG21 , tetapi yang ini mungkin yang paling mudah karena mendekati hal-hal dari tampilan tingkat atas dan tidak terlalu banyak membahas detail bahasa kasar.

James McNellis
sumber
27

Pindah semantik adalah tentang mentransfer sumber daya daripada menyalinnya ketika tidak ada yang membutuhkan nilai sumber lagi.

Dalam C ++ 03, objek sering disalin, hanya untuk dihancurkan atau ditugaskan sebelum kode apa pun menggunakan nilai lagi. Misalnya, ketika Anda kembali dengan nilai dari suatu fungsi — kecuali RVO yang menendang — nilai yang Anda kembalikan disalin ke bingkai tumpukan pemanggil, dan kemudian keluar dari ruang lingkup dan dihancurkan. Ini hanyalah salah satu dari banyak contoh: lihat nilai demi nilai ketika objek sumber bersifat sementara, algoritme seperti sortitu hanya mengatur ulang item, realokasi vectorketika capacity()terlampaui, dll.

Ketika pasangan salinan / penghancuran seperti itu mahal, itu biasanya karena objek memiliki beberapa sumber daya kelas berat. Misalnya, vector<string>mungkin memiliki blok memori yang dialokasikan secara dinamis yang berisi berbagai stringobjek, masing-masing dengan memori dinamisnya sendiri. Menyalin objek seperti itu mahal: Anda harus mengalokasikan memori baru untuk setiap blok yang dialokasikan secara dinamis di sumber, dan menyalin semua nilai di seluruh. Maka Anda perlu membatalkan semua memori yang baru saja Anda salin. Namun, memindahkan besar vector<string>berarti hanya menyalin beberapa petunjuk (yang merujuk pada blok memori dinamis) ke tujuan dan memusatkannya pada sumber.

Dave Abrahams
sumber
23

Secara mudah (praktis):

Menyalin suatu objek berarti menyalin anggota "statis" dan memanggil newoperator untuk objek dinamisnya. Baik?

class A
{
   int i, *p;

public:
   A(const A& a) : i(a.i), p(new int(*a.p)) {}
   ~A() { delete p; }
};

Namun, untuk memindahkan objek (saya ulangi, dalam sudut pandang praktis) menyiratkan hanya untuk menyalin pointer dari objek dinamis, dan bukan untuk membuat yang baru.

Tapi, bukankah itu berbahaya? Tentu saja, Anda dapat merusak objek dinamis dua kali (kesalahan segmentasi). Jadi, untuk menghindari itu, Anda harus "membatalkan" pointer sumber untuk menghindari merusaknya dua kali:

class A
{
   int i, *p;

public:
   // Movement of an object inside a copy constructor.
   A(const A& a) : i(a.i), p(a.p)
   {
     a.p = nullptr; // pointer invalidated.
   }

   ~A() { delete p; }
   // Deleting NULL, 0 or nullptr (address 0x0) is safe. 
};

Ok, tetapi jika saya memindahkan objek, objek sumber menjadi tidak berguna, bukan? Tentu saja, tetapi dalam situasi tertentu itu sangat berguna. Yang paling jelas adalah ketika saya memanggil suatu fungsi dengan objek anonim (temporal, objek nilai, ..., Anda dapat memanggilnya dengan nama yang berbeda):

void heavyFunction(HeavyType());

Dalam situasi itu, objek anonim dibuat, selanjutnya disalin ke parameter fungsi, dan kemudian dihapus. Jadi, di sini lebih baik untuk memindahkan objek, karena Anda tidak memerlukan objek anonim dan Anda dapat menghemat waktu dan memori.

Ini mengarah pada konsep referensi "nilai". Mereka ada di C ++ 11 hanya untuk mendeteksi apakah objek yang diterima anonim atau tidak. Saya pikir Anda sudah tahu bahwa "lvalue" adalah entitas yang dapat dialihkan (bagian kiri dari =operator), jadi Anda memerlukan referensi bernama ke objek untuk dapat bertindak sebagai lvalue. Nilai adalah kebalikannya, objek tanpa referensi nama. Karena itu, objek dan nilai anonim adalah sinonim. Begitu:

class A
{
   int i, *p;

public:
   // Copy
   A(const A& a) : i(a.i), p(new int(*a.p)) {}

   // Movement (&& means "rvalue reference to")
   A(A&& a) : i(a.i), p(a.p)
   {
      a.p = nullptr;
   }

   ~A() { delete p; }
};

Dalam hal ini, ketika suatu objek tipe Aharus "disalin", kompiler membuat referensi nilai atau referensi nilai berdasarkan apakah objek yang dilewati dinamai atau tidak. Ketika tidak, move-constructor Anda dipanggil dan Anda tahu objek itu temporal dan Anda dapat memindahkan objek dinamisnya daripada menyalinnya, menghemat ruang dan memori.

Penting untuk diingat bahwa objek "statis" selalu disalin. Tidak ada cara untuk "memindahkan" objek statis (objek dalam tumpukan dan bukan pada tumpukan). Jadi, perbedaan "bergerak" / "menyalin" ketika suatu objek tidak memiliki anggota dinamis (langsung atau tidak langsung) tidak relevan.

Jika objek Anda kompleks dan destruktor memiliki efek sekunder lainnya, seperti memanggil ke fungsi perpustakaan, memanggil ke fungsi global lainnya atau apa pun itu, mungkin lebih baik memberi sinyal gerakan dengan bendera:

class Heavy
{
   bool b_moved;
   // staff

public:
   A(const A& a) { /* definition */ }
   A(A&& a) : // initialization list
   {
      a.b_moved = true;
   }

   ~A() { if (!b_moved) /* destruct object */ }
};

Jadi, kode Anda lebih pendek (Anda tidak perlu melakukan nullptrtugas untuk setiap anggota dinamis) dan lebih umum.

Pertanyaan khas lainnya: apa perbedaan antara A&&dan const A&&? Tentu saja, dalam kasus pertama, Anda dapat memodifikasi objek dan yang kedua tidak, tetapi, makna praktis? Dalam kasus kedua, Anda tidak dapat memodifikasinya, jadi Anda tidak memiliki cara untuk membatalkan objek (kecuali dengan tanda yang dapat diubah atau sesuatu seperti itu), dan tidak ada perbedaan praktis dengan pembuat salinan.

Dan apakah penerusan yang sempurna ? Penting untuk mengetahui bahwa "referensi nilai" adalah referensi ke objek bernama dalam "ruang lingkup pemanggil". Tetapi dalam lingkup yang sebenarnya, referensi nilai adalah nama ke objek, jadi, itu bertindak sebagai objek bernama. Jika Anda meneruskan referensi nilai ke fungsi lain, Anda meneruskan objek yang dinamai, jadi, objek tidak diterima seperti objek temporal.

void some_function(A&& a)
{
   other_function(a);
}

Objek aakan disalin ke parameter aktual other_function. Jika Anda ingin objek aterus diperlakukan sebagai objek sementara, Anda harus menggunakan std::movefungsi:

other_function(std::move(a));

Dengan baris ini, std::moveakan dilemparkan ake nilai ulang dan other_functionakan menerima objek sebagai objek yang tidak disebutkan namanya. Tentu saja, jika other_functiontidak memiliki kelebihan beban khusus untuk bekerja dengan objek yang tidak disebutkan namanya, perbedaan ini tidak penting.

Apakah itu penerusan yang sempurna? Tidak, tapi kami sangat dekat. Penerusan sempurna hanya berguna untuk bekerja dengan templat, dengan tujuan untuk mengatakan: jika saya perlu meneruskan objek ke fungsi lain, saya perlu bahwa jika saya menerima objek bernama, objek dilewatkan sebagai objek bernama, dan ketika tidak, Saya ingin meneruskannya seperti objek yang tidak disebutkan namanya:

template<typename T>
void some_function(T&& a)
{
   other_function(std::forward<T>(a));
}

Itulah tanda tangan dari fungsi prototipe yang menggunakan penerusan sempurna, diimplementasikan dalam C ++ 11 dengan cara std::forward. Fungsi ini mengeksploitasi beberapa aturan instantiation template:

 `A& && == A&`
 `A&& && == A&&`

Jadi, jika Tmerupakan referensi nilai untuk A( T = A &), ajuga ( A & && => A &). Jika Tmerupakan referensi nilai untuk A, ajuga (A&& && = = A&&). Dalam kedua kasus, aadalah objek bernama dalam lingkup aktual, tetapi Tberisi informasi "tipe referensi" dari sudut pandang ruang lingkup pemanggil. Informasi ini ( T) diteruskan sebagai parameter templat ke forwarddan 'a' dipindahkan atau tidak sesuai dengan jenis T.

Peregring-lk
sumber
20

Ini seperti menyalin semantik, tetapi alih-alih harus menduplikasi semua data Anda bisa mencuri data dari objek yang "dipindahkan" dari.

Terry Mahaffey
sumber
13

Anda tahu apa artinya semantik salinan? itu berarti Anda memiliki jenis yang dapat disalin, untuk jenis yang ditentukan pengguna, Anda menentukan ini baik membeli secara eksplisit menulis operator konstruktor & penugasan salinan atau kompilator membuatnya secara implisit. Ini akan melakukan salinan.

Pindah semantik pada dasarnya adalah tipe yang ditentukan pengguna dengan konstruktor yang mengambil r-nilai referensi (tipe baru referensi menggunakan && (ya dua ampersand)) yang non-const, ini disebut konstruktor bergerak, yang sama berlaku untuk operator penugasan. Jadi apa yang dilakukan seorang konstruktor bergerak, dan bukannya menyalin memori dari argumen sumbernya, ia 'memindahkan' memori dari sumber ke tujuan.

Kapan Anda ingin melakukan itu? well std :: vector adalah contoh, misalkan Anda membuat std :: vector sementara dan Anda mengembalikannya dari fungsi say:

std::vector<foo> get_foos();

Anda akan memiliki overhead dari copy constructor ketika fungsi kembali, jika (dan itu akan di C ++ 0x) std :: vector memiliki move constructor alih-alih menyalinnya hanya dapat mengatur pointer itu dan 'memindahkan' dialokasikan secara dinamis memori ke instance baru. Ini semacam semantik pengalihan kepemilikan dengan std :: auto_ptr.

snk_kid
sumber
1
Saya tidak berpikir ini adalah contoh yang bagus, karena dalam contoh-contoh nilai pengembalian fungsi ini, Return Value Optimization mungkin sudah menghilangkan operasi penyalinan.
Zan Lynx
7

Untuk mengilustrasikan perlunya memindahkan semantik , mari kita pertimbangkan contoh ini tanpa memindahkan semantik:

Berikut adalah fungsi yang mengambil objek bertipe Tdan mengembalikan objek dengan tipe yang sama T:

T f(T o) { return o; }
  //^^^ new object constructed

Fungsi di atas menggunakan panggilan dengan nilai yang berarti bahwa ketika fungsi ini disebut objek harus dibangun untuk digunakan oleh fungsi.
Karena fungsi ini juga mengembalikan berdasarkan nilai , objek baru lainnya dibangun untuk nilai pengembalian:

T b = f(a);
  //^ new object constructed

Dua objek baru telah dibangun, salah satunya adalah objek sementara yang hanya digunakan selama durasi fungsi.

Ketika objek baru dibuat dari nilai balik, konstruktor salin dipanggil untuk menyalin konten objek sementara ke objek baru b. Setelah fungsi selesai, objek sementara yang digunakan dalam fungsi keluar dari ruang lingkup dan dihancurkan.


Sekarang, mari kita pertimbangkan apa yang dilakukan copy constructor .

Pertama-tama harus menginisialisasi objek, lalu menyalin semua data yang relevan dari objek lama ke yang baru.
Tergantung pada kelasnya, mungkin wadahnya dengan data yang sangat banyak, maka itu bisa mewakili banyak waktu dan penggunaan memori

// Copy constructor
T::T(T &old) {
    copy_data(m_a, old.m_a);
    copy_data(m_b, old.m_b);
    copy_data(m_c, old.m_c);
}

Dengan memindahkan semantik sekarang mungkin untuk membuat sebagian besar pekerjaan ini kurang menyenangkan dengan hanya memindahkan data daripada menyalin.

// Move constructor
T::T(T &&old) noexcept {
    m_a = std::move(old.m_a);
    m_b = std::move(old.m_b);
    m_c = std::move(old.m_c);
}

Memindahkan data melibatkan mengaitkan kembali data dengan objek baru. Dan tidak ada salinan sama sekali.

Ini dilakukan dengan rvaluereferensi.
Sebuah rvaluereferensi bekerja cukup banyak seperti lvaluereferensi dengan satu perbedaan penting:
sebuah referensi nilai p dapat dipindahkan dan lvalue tidak bisa.

Dari cppreference.com :

Agar jaminan pengecualian yang kuat dimungkinkan, konstruktor pemindahan yang ditentukan pengguna tidak boleh melempar pengecualian. Bahkan, kontainer standar biasanya bergantung pada std :: move_if_noexcept untuk memilih antara pindah dan menyalin ketika elemen kontainer perlu dipindahkan. Jika kedua konstruktor salin dan pindahkan disediakan, resolusi kelebihan memilih konstruktor pindahkan jika argumennya adalah nilai (baik nilai sementara seperti nama sementara atau nilai x seperti hasil std :: move), dan pilih salin konstruktor jika argumennya adalah nilai (nama objek atau fungsi / operator yang mengembalikan referensi nilai). Jika hanya copy constructor yang disediakan, semua kategori argumen memilihnya (selama itu membutuhkan referensi ke const, karena nilai dapat mengikat ke referensi const), yang membuat menyalin fallback untuk bergerak, ketika bergerak tidak tersedia. Dalam banyak situasi, memindahkan konstruktor dioptimalkan bahkan jika mereka akan menghasilkan efek samping yang dapat diamati, lihat salinan elision. Konstruktor disebut 'pindahkan konstruktor' ketika mengambil referensi nilai sebagai parameter. Tidak diwajibkan untuk memindahkan apa pun, kelas tidak diharuskan memiliki sumber daya untuk dipindahkan dan 'pemindah konstruktor' mungkin tidak dapat memindahkan sumber daya seperti dalam kasus yang diperbolehkan (tapi mungkin tidak masuk akal) di mana parameternya adalah referensi nilai konstan (const T&&).

Andreas DM
sumber
7

Saya menulis ini untuk memastikan saya memahaminya dengan benar.

Pindah semantik diciptakan untuk menghindari penyalinan objek besar yang tidak perlu. Bjarne Stroustrup dalam bukunya "Bahasa Pemrograman C ++" menggunakan dua contoh di mana penyalinan yang tidak perlu terjadi secara default: satu, pertukaran dua objek besar, dan dua, pengembalian objek besar dari suatu metode.

Menukar dua objek besar biasanya melibatkan menyalin objek pertama ke objek sementara, menyalin objek kedua ke objek pertama, dan menyalin objek sementara ke objek kedua. Untuk tipe bawaan, ini sangat cepat, tetapi untuk objek besar ketiga salinan ini bisa memakan banyak waktu. "Pindah tugas" memungkinkan pemrogram untuk menimpa perilaku salin default dan alih-alih bertukar referensi ke objek, yang berarti bahwa tidak ada penyalinan sama sekali dan operasi swap jauh lebih cepat. Tugas pemindahan dapat dipanggil dengan memanggil metode std :: move ().

Mengembalikan objek dari suatu metode secara default melibatkan membuat salinan dari objek lokal dan data terkait di lokasi yang dapat diakses oleh penelepon (karena objek lokal tidak dapat diakses oleh penelepon dan menghilang ketika metode selesai). Ketika tipe bawaan dikembalikan, operasi ini sangat cepat, tetapi jika objek besar dikembalikan, ini bisa memakan waktu lama. Konstruktor bergerak memungkinkan pemrogram untuk menimpa perilaku default ini dan sebagai gantinya "menggunakan kembali" data tumpukan yang terkait dengan objek lokal dengan mengarahkan objek yang dikembalikan ke pemanggil untuk menumpuk data yang terkait dengan objek lokal. Dengan demikian, tidak perlu menyalin.

Dalam bahasa yang tidak memungkinkan pembuatan objek lokal (yaitu, objek pada stack) jenis masalah ini tidak terjadi karena semua objek dialokasikan pada heap dan selalu diakses dengan referensi.

Chris B
sumber
"A" pemindahan tugas "memungkinkan pemrogram untuk mengganti perilaku penyalinan default dan sebagai gantinya menukar referensi ke objek, yang berarti bahwa tidak ada penyalinan sama sekali dan operasi swap lebih cepat." - klaim ini ambigu dan menyesatkan. Untuk menukar dua objek xdan y, Anda tidak bisa hanya "bertukar referensi ke objek" ; mungkin saja objek berisi pointer yang mereferensikan data lain, dan pointer itu dapat ditukar, tetapi operator yang bergerak tidak diharuskan untuk menukar apa pun. Mereka dapat menghapus data dari objek yang dipindahkan-dari, daripada menyimpan data dest di dalamnya.
Tony Delroy
Anda bisa menulis swap()tanpa memindahkan semantik. "Tugas pemindahan dapat dipanggil dengan memanggil metode std :: move ()." - kadang - kadang diperlukan untuk digunakan std::move()- meskipun itu tidak benar-benar memindahkan apa pun - biarkan kompiler tahu bahwa argumennya dapat dipindahkan, kadang-kadang std::forward<>()(dengan referensi penerusan), dan di lain waktu kompiler mengetahui nilai dapat dipindahkan.
Tony Delroy
-2

Berikut jawaban dari buku "Bahasa Pemrograman C ++" oleh Bjarne Stroustrup. Jika Anda tidak ingin melihat video, Anda dapat melihat teks di bawah ini:

Pertimbangkan cuplikan ini. Kembali dari operator + melibatkan menyalin hasil dari variabel lokal resdan ke suatu tempat di mana pemanggil dapat mengaksesnya.

Vector operator+(const Vector& a, const Vector& b)
{
    if (a.size()!=b.size())
        throw Vector_siz e_mismatch{};
    Vector res(a.size());
        for (int i=0; i!=a.size(); ++i)
            res[i]=a[i]+b[i];
    return res;
}

Kami tidak benar-benar menginginkan salinan; kami hanya ingin mendapatkan hasil dari suatu fungsi. Jadi kita perlu memindahkan Vektor daripada menyalinnya. Kita dapat mendefinisikan move constructor sebagai berikut:

class Vector {
    // ...
    Vector(const Vector& a); // copy constructor
    Vector& operator=(const Vector& a); // copy assignment
    Vector(Vector&& a); // move constructor
    Vector& operator=(Vector&& a); // move assignment
};

Vector::Vector(Vector&& a)
    :elem{a.elem}, // "grab the elements" from a
    sz{a.sz}
{
    a.elem = nullptr; // now a has no elements
    a.sz = 0;
}

&& berarti "referensi nilai" dan merupakan referensi di mana kita dapat mengikat nilai. "rvalue" 'dimaksudkan untuk melengkapi "lvalue" yang secara kasar berarti "sesuatu yang dapat muncul di sisi kiri suatu tugas." Jadi nilai berarti kira-kira "nilai yang Anda tidak dapat menetapkan", seperti integer yang dikembalikan oleh panggilan fungsi, dan resvariabel lokal di operator + () untuk Vektor.

Sekarang, pernyataan return res;itu tidak akan disalin!

Rob Pei
sumber