Pertanyaan Apa yang memindahkan semantik?


Saya baru saja selesai mendengarkan radio Rekayasa Perangkat Lunak wawancara podcast 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 belum mengerti memindahkan semantik... Apa sebenarnya mereka?


1374
2018-06-23 22:46


asal


Jawaban:


Saya merasa paling mudah untuk memahami semantik gerakan dengan kode contoh. Mari kita mulai dengan kelas string yang sangat sederhana yang hanya menyimpan pointer ke blok memori yang dialokasikan-tumpukan:

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

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

Karena kami memilih untuk mengelola ingatan itu sendiri, kami harus mengikuti aturan tiga. Saya akan menunda menulis operator penugasan dan hanya mengimplementasikan destruktor dan konstruktor salin untuk saat ini:

    ~string()
    {
        delete[] data;
    }

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

Copy constructor mendefinisikan apa artinya menyalin objek string. Parameter const string& that mengikat semua ekspresi string tipe yang memungkinkan Anda 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 muncul wawasan kunci ke dalam semantik gerakan. Perhatikan bahwa hanya di baris pertama tempat kami menyalin x apakah salinan yang mendalam ini benar-benar diperlukan, karena kita mungkin ingin memeriksa x nanti dan akan sangat terkejut jika x entah bagaimana telah berubah. Apakah Anda memperhatikan bagaimana saya baru saja mengatakan x tiga kali (empat kali jika Anda memasukkan kalimat ini) dan berarti objek yang sama persis setiap saat? Kami menyebut ekspresi seperti x "Nilai-nilai".

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

C ++ 0x memperkenalkan mekanisme baru yang disebut "rujukan referensi" yang, antara lain, memungkinkan kami untuk mendeteksi argumen r-nilai melalui fungsi overloading. Yang harus kita lakukan adalah menulis konstruktor dengan parameter referensi rvalue. Di dalam konstruktor itu bisa kita lakukan apapun yang kita inginkan dengan sumbernya, selama kita membiarkannya masuk beberapa negara yang valid:

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

Apa yang telah kami lakukan di sini? Alih-alih menyalin data heap secara mendalam, kita baru saja menyalin pointer dan kemudian mengatur pointer asli ke null. Akibatnya, kami telah "mencuri" data yang awalnya milik string sumber. Sekali lagi, wawasan kunci 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 "konstruktor bergerak". Tugasnya adalah memindahkan sumber daya dari satu objek ke objek lain alih-alih menyalinnya.

Selamat, Anda sekarang memahami dasar-dasar bergerak semantik! Mari lanjutkan dengan menerapkan operator penugasan. Jika Anda tidak terbiasa dengan salin dan tukar idiom, pelajari dan kembalilah, karena ini adalah idiom C ++ yang luar biasa terkait dengan keamanan pengecualian.

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

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

Perhatikan bahwa kami meneruskan parameter that  berdasarkan nilai, jadi that harus diinisialisasi seperti objek string lainnya. Persis bagaimana caranya that akan diinisialisasi? Di masa lalu C ++ 98, jawabannya pasti "oleh konstruktor salinan". Dalam C ++ 0x, compiler memilih antara konstruktor salin dan konstruktor bergerak berdasarkan pada apakah argumen ke operator penugasan adalah lvalue atau rvalue.

Jadi, jika Anda berkata a = b, yang salin konstruktor akan menginisialisasi that (karena ekspresinya b adalah lvalue), dan operator penugasan menukar konten dengan salinan yang baru dibuat. Itu adalah definisi dari copy dan swap idiom - membuat salinan, menukar isi dengan salinan, dan kemudian menyingkirkan salinan dengan meninggalkan ruang lingkup. Tidak ada yang baru di sini.

Tetapi jika Anda berkata a = x + y, yang pindahkan konstruktor akan menginisialisasi that (karena ekspresinya x + y adalah rvalue), jadi tidak ada salinan mendalam yang terlibat, hanya langkah yang efisien. that masih merupakan objek independen dari argumen, tetapi konstruksinya sepele, karena data tumpukan tidak harus disalin, cukup dipindahkan. Itu tidak perlu untuk menyalinnya karena x + y adalah rvalue, dan lagi, tidak apa-apa untuk berpindah dari objek string yang dilambangkan dengan rvalues.

Untuk meringkas, konstruktor menyalin membuat salinan yang mendalam, karena sumbernya harus tetap tak tersentuh. Konstruktor langkah, di sisi lain, dapat menyalin pointer dan kemudian mengatur pointer di sumber ke null. Tidak apa-apa untuk "membatalkan" objek sumber dengan cara ini, karena klien tidak memiliki cara memeriksa objek lagi.

Saya harap contoh ini mendapat titik utama. Ada banyak hal lain untuk merevaluasi referensi dan memindahkan semantik yang sengaja ditinggalkan untuk membuatnya tetap sederhana. Jika Anda ingin lebih jelasnya silakan lihat jawaban tambahan saya.


2034
2018-06-24 12:40



Jawaban pertama saya adalah pengantar yang sangat disederhanakan untuk memindahkan semantik, dan banyak detail yang ditinggalkan dengan tujuan 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 tepat untuk menggantinya dengan teks yang benar-benar berbeda. Saya pikir itu masih berfungsi dengan baik sebagai perkenalan 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

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

  1. Mengubah salinan mahal menjadi gerakan murah. Lihat jawaban pertama saya untuk sebuah contoh. Perhatikan bahwa jika suatu objek tidak mengelola setidaknya satu sumber daya eksternal (baik secara langsung, atau tidak langsung melalui objek-objek anggotanya), semantik gerakan tidak akan menawarkan keuntungan apa pun atas salinan semantik. Dalam hal ini, 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 untuk bergerak"; yaitu jenis-jenis yang menyalin tidak masuk akal, tetapi bergerak tidak. Contohnya termasuk kunci, menangani file, dan pointer pintar dengan semantik kepemilikan unik. Catatan: Jawaban ini membahas std::auto_ptr, template perpustakaan standar C ++ 98 yang tidak dipakai lagi, yang digantikan oleh std::unique_ptr di C ++ 11. Programer C ++ menengah mungkin paling tidak familiar std::auto_ptr, dan karena "semantik gerakan" yang ditampilkannya, sepertinya titik awal yang baik untuk membahas semantik gerakan dalam C ++ 11. YMMV.

Apa itu langkah?

Perpustakaan standar C ++ 98 menawarkan pointer pintar dengan semantik kepemilikan unik yang disebut std::auto_ptr<T>. Jika Anda tidak terbiasa dengan 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 tentang auto_ptr adalah perilaku "menyalin" nya:

auto_ptr<Shape> a(new Triangle);

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

auto_ptr<Shape> b(a);

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

Perhatikan bagaimana inisialisasi dari b dengan a tidak tidak salin segitiga, tetapi mentransfer kepemilikan segitiga dari a untuk b. Kami juga mengatakan "a aku s pindah ke  b"atau" segitiga itu terharu dari a  untuk  b". Ini mungkin terdengar membingungkan, karena segitiga itu sendiri selalu berada di tempat yang sama di memori.

Untuk memindahkan objek berarti memindahkan kepemilikan beberapa sumber yang ia kelola ke objek lain.

Copy constructor dari auto_ptr mungkin 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
}

Gerakan berbahaya dan tidak berbahaya

Hal yang berbahaya tentang auto_ptr adalah bahwa apa yang secara sintaksis terlihat seperti salinan sebenarnya adalah sebuah langkah. Mencoba memanggil fungsi anggota saat dipindahkan auto_ptr akan mengaktifkan perilaku tidak terdefinisi, jadi Anda harus sangat berhati-hati untuk tidak menggunakan auto_ptr setelah 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

Tapi auto_ptr tidak 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 tidak terdefinisi, sedangkan yang lain tidak. Jadi apa perbedaan antara ekspresi a dan make_triangle()? Bukankah mereka berdua tipe yang sama? Memang benar, tetapi mereka berbeda kategori nilai.

Kategori nilai

Tentunya, harus ada perbedaan besar antara ungkapan itu a yang menunjukkan suatu auto_ptr variabel, dan ekspresi make_triangle() yang menandakan panggilan fungsi yang mengembalikan sebuah auto_ptr dengan nilai, sehingga menciptakan suatu sementara segar auto_ptr keberatan setiap kali dipanggil. a adalah contoh dari sebuah lvalue, sedangkan make_triangle() adalah contoh dari sebuah rvalue.

Pindah dari nilai-nilai seperti a berbahaya, karena nanti kami bisa mencoba memanggil fungsi anggota melalui a, memohon perilaku tidak terdefinisi. Di sisi lain, bergerak dari rvalu seperti make_triangle() sangat aman, karena setelah konstruktor salin telah melakukan tugasnya, kita tidak bisa menggunakan yang sementara lagi. Tidak ada ekspresi yang menunjukkan kata sementara; jika kita hanya menulis make_triangle()lagi, kita dapatkan berbeda sementara. Bahkan, pindah dari sementara sudah pergi pada baris berikutnya:

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

Perhatikan bahwa huruf l dan r memiliki asal bersejarah di sisi kiri dan sisi kanan tugas. Ini tidak lagi benar di C ++, karena ada lvalues ​​yang tidak dapat muncul di sisi kiri penugasan (seperti array atau tipe yang ditentukan pengguna tanpa operator penugasan), dan ada rvalu yang dapat (semua rvalu dari tipe kelas dengan operator penugasan).

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

Rujukan nilai

Kami sekarang mengerti bahwa pindah dari nilai-nilai itu berpotensi berbahaya, tetapi pindah dari rvalu tidaklah berbahaya. Jika C ++ memiliki dukungan bahasa untuk membedakan argumen nilai dari argumen nilai, kita dapat sepenuhnya melarang pindah dari nilai, atau setidaknya membuat bergerak dari nilai-nilai eksplisit di situs panggilan, sehingga kami tidak lagi berpindah karena kecelakaan.

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

Jika kita melempar const ke dalam campuran, kami sudah memiliki empat jenis referensi yang berbeda. Jenis ekspresi seperti apa X bisakah mereka mengikat?

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

Dalam prakteknya, Anda bisa melupakannya const X&&. Keterbatasan membaca dari rujukan tidak terlalu berguna.

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

Konversi implisit

Referensi r value mendapat beberapa versi. Sejak versi 2.1, referensi rvalue X&& juga mengikat semua kategori nilai dari jenis yang berbeda Y, asalkan ada konversi implisit dari Y untuk X. Dalam hal ini, tipe sementara X dibuat, dan rujukan referensi terikat pada yang sementara:

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

some_function("hello world");

Dalam contoh di atas, "hello world" adalah lvalue of type const char[12]. Karena ada konversi implisit dari const char[12] melalui const char* untuk std::string, tipe sementara std::string dibuat, dan r terikat pada sementara itu. Ini adalah salah satu kasus di mana perbedaan antara rvalues ​​(ekspresi) dan temporaries (objek) agak kabur.

Pindahkan konstruktor

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

Di C ++ 11, std::auto_ptr<T> telah diganti oleh std::unique_ptr<T> yang memanfaatkan rvalue referensi. Saya akan mengembangkan dan mendiskusikan versi sederhana dari unique_ptr. Pertama, kami merangkum pointer mentah dan membebani operator -> dan *, jadi kelas kami terasa seperti penunjuk:

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

public:

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

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

Konstruktor mengambil kepemilikan objek, dan destructor menghapusnya:

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

    ~unique_ptr()
    {
        delete ptr;
    }

Sekarang sampai pada bagian yang menarik, konstruktor bergerak:

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

Konstruktor langkah ini melakukan persis apa auto_ptr copy constructor, tetapi hanya dapat diberikan dengan rvalu:

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

Baris kedua gagal dikompilasi, karena a adalah lvalue, tetapi parameternya unique_ptr&& source hanya bisa terikat pada rvalues. Inilah yang kami inginkan; gerakan berbahaya tidak boleh implisit. Baris ketiga hanya mengkompilasi, karena make_triangle() adalah rvalue. Langkah konstruktor akan mengalihkan kepemilikan dari sementara ke c. Sekali lagi, inilah yang kami inginkan.

Pergerakan konstruktor mentransfer kepemilikan sumber daya yang dikelola ke objek saat ini.

Pindahkan operator penugasan

Bagian terakhir yang hilang adalah operator tugas pindah. Tugasnya adalah untuk 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 penerapan operator tugas pemindahan ini menduplikasi logika dari destruktor dan konstruktor gerakan. Apakah Anda akrab 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 source adalah variabel tipe unique_ptr, itu akan diinisialisasi oleh konstruktor bergerak; artinya, argumen akan dipindahkan ke parameter. Argumen ini masih diperlukan untuk menjadi rvalue, karena constructor bergerak itu sendiri memiliki parameter referensi rvalue. Ketika aliran kontrol mencapai kurung tutup operator=, source keluar dari ruang lingkup, melepaskan sumber daya lama secara otomatis.

Operator penugasan pindah mentransfer kepemilikan sumber daya yang dikelola ke objek saat ini, melepaskan sumber daya lama.   Idiasa move-and-swap menyederhanakan implementasi.

Pindah dari nilai-nilai

Terkadang, kita ingin pindah dari nilai-nilai. Artinya, kadang-kadang kita ingin compiler untuk memperlakukan nilai lv seolah-olah itu adalah rvalue, sehingga dapat memohon konstruktor bergerak, meskipun itu bisa berpotensi tidak aman. Untuk tujuan ini, C ++ 11 menawarkan template fungsi perpustakaan standar yang disebut std::move di dalam header <utility>. Nama ini agak disayangkan, karena std::move hanya melemparkan nilai lv ke rvalue; itu benar tidak memindahkan apa saja dengan sendirinya. Itu hanya memungkinkan bergerak. Mungkin seharusnya diberi nama std::cast_to_rvalue atau std::enable_move, tapi kami terjebak dengan nama itu sekarang.

Di sini adalah bagaimana Anda secara eksplisit berpindah dari nilai lv:

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, a tidak lagi memiliki segitiga. Tidak apa-apa, karena pada secara eksplisit penulisan std::move(a), kami membuat niat kami jelas: "Kontraktor yang terhormat, lakukan apapun yang Anda inginkan a untuk menginisialisasi c; Saya tidak peduli a lagi. Jangan ragu untuk memiliki cara Anda a. "

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

Xvalues

Perhatikan bahwa meskipun std::move(a) adalah rvalue, evaluasinya tidak tidak buat objek sementara. Teka-teki ini memaksa komite untuk memperkenalkan kategori nilai ketiga. Sesuatu yang dapat terikat pada rujukan referensi, meskipun itu bukan nilai dalam pengertian tradisional, disebut an xvalue (nilai eXpiring). Nilai-nilai tradisional diubah namanya menjadi prvalues (Nilai rujukan murni).

Baik prvalues ​​dan xvalues ​​adalah rvalues. Nilai X dan nilai keduanya glalue (Nilai umum). Hubungan lebih mudah dipahami dengan diagram:

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

Perhatikan bahwa hanya xvalues ​​yang benar-benar baru; sisanya hanya karena mengganti nama dan pengelompokan.

C ++ 98 rvalues ​​dikenal sebagai prvalues ​​dalam C ++ 11. Secara mental mengganti semua kejadian "rvalue" di paragraf sebelumnya dengan "prvalue".

Bergerak keluar dari fungsi

Sejauh ini, kita telah melihat pergerakan ke variabel lokal, dan ke dalam parameter fungsi. Tetapi bergerak juga mungkin 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 return pernyataan sebagai argumen untuk 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 bisa secara implisit pindah fungsi:

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

Bagaimana konstruktor langkah menerima lvalue result sebagai argumen? Ruang lingkup result akan berakhir, dan itu akan hancur selama stack unwinding. Tidak ada yang bisa mengeluh setelah itu result entah bagaimana telah berubah; saat aliran kontrol kembali ke pemanggil, result tidak ada lagi! Oleh karena itu, C ++ 11 memiliki aturan khusus yang memungkinkan mengembalikan objek otomatis dari fungsi tanpa harus menulis std::move. Faktanya, Anda harus tak pernah menggunakan std::move untuk memindahkan objek otomatis dari fungsi, karena ini menghambat "optimasi nilai pengembalian bernama" (NRVO).

Tidak pernah digunakan std::move untuk memindahkan objek otomatis dari fungsi.

Perhatikan bahwa di kedua fungsi pabrik, jenis kembalinya adalah nilai, bukan rujukan referensi. Rujukan nilai adalah referensi, dan seperti biasa, Anda tidak boleh mengembalikan referensi ke objek otomatis; penelepon akan berakhir dengan referensi menggantung jika Anda menipu kompilator untuk 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 rujukan referensi. Bergerak secara eksklusif dilakukan oleh konstruktor bergerak, bukan oleh std::move, dan bukan hanya mengikat rvalue ke referensi rvalue.

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, kompilator akan mengeluh itu parameter adalah lvalue. Jika Anda melihat jenisnya, Anda melihat referensi rvalue, tetapi rujukan referensi hanya berarti "referensi yang terikat dengan nilai"; itu benar tidak berarti bahwa referensi itu sendiri adalah rvalue! Memang, parameter hanyalah sebuah variabel biasa dengan sebuah nama. Kamu dapat memakai parameter sesering yang Anda suka di dalam tubuh konstruktor, dan selalu menunjukkan objek yang sama. Secara implisit bergerak dari itu akan berbahaya, maka bahasa melarangnya.

Referensi rvalue yang diberi nama adalah lvalue, sama seperti variabel lainnya.

Solusinya adalah secara manual mengaktifkan langkah:

class Foo
{
    unique_ptr<Shape> member;

public:

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

Anda bisa membantah itu parameter tidak digunakan lagi setelah inisialisasi member. Mengapa tidak ada aturan khusus untuk dimasukkan secara diam-diam std::move seperti halnya dengan nilai kembali? Mungkin karena itu akan terlalu membebani para pelaksana compiler. Misalnya, bagaimana jika badan konstruktor berada di unit terjemahan lain? Sebaliknya, aturan nilai kembalian hanya harus memeriksa tabel simbol untuk menentukan apakah pengenal setelah atau tidak return kata kunci menunjukkan objek otomatis.

Anda juga bisa lulus parameter berdasarkan nilai. Untuk tipe hanya bergerak unique_ptr, sepertinya belum ada idiom yang mapan. Secara pribadi, saya lebih suka lewat nilai, karena menyebabkan lebih sedikit kekacauan di antarmuka.

Fungsi anggota khusus

C + + 98 secara implisit menyatakan tiga fungsi anggota khusus pada permintaan, yaitu ketika dibutuhkan di suatu tempat: konstruktor copy, operator penugasan salinan dan destruktor.

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

Referensi r value mendapat beberapa versi. Sejak versi 3.0, C ++ 11 mendeklarasikan dua tambahan fungsi anggota khusus sesuai permintaan: konstruktor bergerak dan operator penugasan pindah. Perhatikan bahwa baik VC10 maupun VC11 belum sesuai dengan versi 3.0, jadi Anda harus menerapkannya sendiri.

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

Kedua fungsi anggota khusus baru ini hanya dinyatakan secara implisit jika tidak ada fungsi anggota khusus yang dinyatakan secara manual. Juga, jika Anda menyatakan konstruktor langkah Anda atau memindahkan operator penugasan, baik konstruktor menyalin maupun operator penugasan salinan akan dinyatakan secara implisit.

Apa artinya aturan-aturan ini dalam praktiknya?

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 menerapkan fungsi anggota khusus sendiri. Tentu saja, jika kelas Anda tidak mendapat manfaat dari semantik bergerak, tidak perlu menerapkan operasi bergerak khusus.

Perhatikan bahwa operator penugasan salinan dan operator penugasan pindah dapat menyatu menjadi satu operator penugasan tunggal, mengambil argumennya berdasarkan nilainya:

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 menerapkan turun dari lima ke empat. Ada tradeoff antara pengecualian-keamanan dan efisiensi di sini, tapi saya bukan ahli dalam masalah ini.

Meneruskan referensi (sebelumnya dikenal sebagai Referensi universal)

Pertimbangkan template fungsi berikut:

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

Anda mungkin berharap T&& untuk hanya mengikat rvalues, karena pada pandangan pertama, itu tampak seperti referensi rvalue. Ternyata, T&& juga mengikat ke 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 rvalue of type X, T disimpulkan menjadi X, karenanya T&& cara X&&. Inilah yang diharapkan orang. Tetapi jika argumennya adalah lvalue of type X, karena aturan khusus, T disimpulkan menjadi X&, karenanya T&& akan berarti sesuatu seperti X& &&. Tetapi karena C ++ masih belum memiliki referensi referensi, jenisnya X& && aku s runtuh ke X&. Ini mungkin terdengar membingungkan dan tidak berguna pada awalnya, tetapi referensi runtuh sangat penting untuk penerusan sempurna (yang tidak akan dibahas di sini).

T && bukan referensi rvalue, tetapi referensi penerusan. Ini juga mengikat nilai-nilai, dalam hal ini T dan T&& keduanya referensi lvalue.

Jika Anda ingin membatasi suatu template fungsi ke rvalues, Anda dapat menggabungkan SFINAE dengan ciri-ciri jenis:

#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 referensi runtuh, di sini adalah bagaimana std::move diimplementasikan:

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, move menerima segala jenis parameter berkat referensi forwarding T&&, dan mengembalikan referensi rvalue. Itu std::remove_reference<T>::type panggilan meta-fungsi diperlukan karena jika tidak, untuk lvalues ​​of type X, tipe kembalinya X& &&, yang akan runtuh X&. Sejak t selalu bernilai lvalue (ingat bahwa referensi rvalue yang diberi nama adalah lvalue), tetapi kami ingin mengikat t ke referensi rvalue, kita harus secara eksplisit melakukan cast t ke jenis pengembalian yang benar. Panggilan fungsi yang mengembalikan referensi rvalue itu sendiri merupakan xvalue. Sekarang Anda tahu dari mana xvalues ​​berasal;)

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

Perhatikan bahwa mengembalikan referensi rvalue baik-baik saja dalam contoh ini, karena t tidak menunjukkan objek otomatis, tetapi sebagai objek yang dilewatkan oleh penelepon.


891
2017-07-18 11:24



Pindah semantik didasarkan pada rujukan nilai.
Sebuah rvalue adalah objek sementara, yang akan dihancurkan pada akhir ekspresi. Dalam C ++ saat ini, ralue hanya mengikat const referensi. C ++ 1x akan memungkinkan non-const rujukan nilai, dieja T&&, yang merupakan referensi ke objek rvalue.
Karena rvalue akan mati di akhir ekspresi, Anda bisa mencuri datanya. Dari pada penyalinan ke objek lain, Anda pindah 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

Dalam kode di atas, dengan compiler lama hasil f() aku s disalin ke x menggunakan Xcopy constructor. Jika kompilator Anda mendukung semantik dan bergerak X memiliki konstruktor bergerak, maka itu yang disebut sebagai gantinya. Sejak itu rhs argumen adalah sebuah rvalue, kita tahu itu tidak diperlukan lagi dan kita bisa mencuri nilainya.
Jadi nilainya terharu dari yang tidak disebutkan namanya sementara kembali dari f() untuk x (sementara data dari x, diinisialisasi ke yang kosong X, dipindahkan ke sementara, yang akan hancur setelah penugasan).


67
2018-06-23 23:12



Misalkan Anda memiliki fungsi yang mengembalikan objek substansial:

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

Ketika Anda menulis kode seperti ini:

Matrix r = multiply(a, b);

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

Ini sangat penting jika (seperti mungkin Matrix contoh di atas), objek yang disalin mengalokasikan memori ekstra pada heap untuk menyimpan representasi internalnya. Sebuah konstruktor copy harus membuat salinan lengkap dari representasi internal, atau menggunakan penghitungan referensi dan semantik copy-on-write secara inter-interal. Sebuah konstruktor bergerak akan meninggalkan memori heap sendirian dan hanya menyalin pointer di dalam Matrix obyek.


46
2018-06-23 22:53



Jika Anda benar-benar tertarik pada penjelasan yang baik dan mendalam tentang semantik gerakan, saya sangat menyarankan untuk membaca makalah asli tentang mereka, "A Proposal untuk Menambah Semantik Semangat Dukungan ke Bahasa C ++." 

Ini sangat mudah diakses dan mudah dibaca dan itu membuat kasus luar biasa untuk manfaat yang mereka tawarkan. Ada makalah lain yang lebih baru dan terbaru tentang semantik gerakan yang tersedia situs web WG21, tetapi yang ini mungkin yang paling mudah karena mendekati hal-hal dari tampilan tingkat atas dan tidak terlalu banyak masuk ke dalam rincian bahasa yang kasar.


27
2018-06-23 23:32



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

Di C + + 03, objek sering disalin, hanya untuk dihancurkan atau ditetapkan-over sebelum kode apa pun menggunakan nilai lagi. Misalnya, ketika Anda kembali dengan nilai dari suatu fungsi — kecuali RVO melakukan tendangan — 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 pass-by-value ketika objek sumber adalah sementara, seperti algoritma sort yang hanya mengatur ulang item, realokasi di vector ketika itu capacity() terlampaui, dll.

Ketika pasangan menyalin / menghancurkan semacam itu mahal, biasanya karena objek tersebut memiliki beberapa sumber daya kelas berat. Sebagai contoh, vector<string> dapat memiliki blok memori yang dialokasikan secara dinamis berisi array string objek, masing-masing dengan memori dinamisnya sendiri. Menyalin objek semacam itu mahal: Anda harus mengalokasikan memori baru untuk setiap blok yang dialokasikan secara dinamis di sumber, dan menyalin semua nilai di seluruh. Kemudian Anda perlu mengalihkan semua memori yang baru saja Anda salin. Namun, bergerak besar vector<string> berarti hanya menyalin beberapa pointer (yang merujuk ke blok memori dinamis) ke tujuan dan memusatkan mereka di sumber.


21
2018-04-08 19:47



Dalam istilah yang mudah (praktis):

Menyalin objek berarti menyalin anggota "statis" dan memanggil new operator untuk objek-objeknya yang dinamis. Kanan?

class A
{
   int i, *p;

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

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

Tapi, apakah itu tidak berbahaya? Tentu saja, Anda bisa merusak objek yang dinamis dua kali (kesalahan segmentasi). Jadi, untuk menghindari itu, Anda harus "membatalkan" penunjuk sumber agar tidak 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 fungsi dengan objek anonim (temporal, rvalue object, ..., Anda dapat memanggilnya dengan nama yang berbeda):

void heavyFunction(HeavyType());

Dalam situasi itu, objek anonim dibuat, selanjutnya disalin ke parameter fungsi, dan setelah itu 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 "rvalue". Mereka ada di C ++ 11 hanya untuk mendeteksi apakah objek yang diterima adalah anonim atau tidak. Saya pikir Anda sudah tahu bahwa "lvalue" adalah entitas yang dapat dialihkan (bagian kiri dari = operator), jadi Anda perlu referensi bernama ke suatu objek agar dapat bertindak sebagai lvalue. Sebuah rvalue adalah justru kebalikannya, sebuah objek tanpa referensi bernama. Karena itu, objek anonim dan rnalue 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 A harus "disalin", kompilator membuat referensi lnilai atau rujukan referensi sesuai dengan jika objek yang diteruskan diberi nama atau tidak. Bila tidak, konstruktor-bergerak Anda dipanggil dan Anda tahu objek itu bersifat sementara dan Anda dapat memindahkan objek-objek dinamisnya alih-alih menyalinnya, menghemat ruang dan memori.

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

Jika objek Anda kompleks dan perusak memiliki efek sekunder lainnya, seperti memanggil ke fungsi pustaka, memanggil 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 a nullptr tugas untuk setiap anggota dinamis) dan lebih umum.

Pertanyaan umum lainnya: apa perbedaannya 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 bendera yang dapat berubah atau sesuatu seperti itu), dan tidak ada perbedaan praktis untuk konstruktor salin.

Dan apa penerusan sempurna? Penting untuk mengetahui bahwa "rujukan referensi" adalah referensi ke objek bernama dalam "lingkup pemanggil". Namun dalam ruang lingkup yang sebenarnya, referensi rvalue adalah nama untuk suatu objek, jadi, ia bertindak sebagai objek bernama. Jika Anda melewatkan referensi rvalue ke fungsi lain, Anda melewati objek bernama, jadi, objek tidak diterima seperti objek sementara.

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

Objeknya a akan disalin ke parameter sebenarnya other_function. Jika Anda menginginkan objek a terus diperlakukan sebagai objek sementara, Anda harus menggunakan std::move fungsi:

other_function(std::move(a));

Dengan garis ini, std::move akan melakukan cast a ke rvalue dan other_function akan menerima objek sebagai objek tanpa nama. Tentu saja jika other_functiontidak memiliki kelebihan spesifik untuk bekerja dengan objek tanpa nama, perbedaan ini tidak penting.

Apakah itu penerusan sempurna? Tidak, tapi kami sangat dekat. Penerusan sempurna hanya berguna untuk bekerja dengan template, dengan tujuan untuk mengatakan: jika saya harus mengirimkan objek ke fungsi lain, saya perlu bahwa jika saya menerima objek bernama, objek dilewatkan sebagai objek bernama, dan ketika tidak, Saya ingin menyampaikannya seperti objek tanpa nama:

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

Itulah tanda tangan dari fungsi prototipikal yang menggunakan penyampaian sempurna, diimplementasikan dalam C ++ 11 dengan cara std::forward. Fungsi ini memanfaatkan beberapa aturan instantiasi template:

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

Jadi jika T adalah referensi lnilai untuk A (T = A &), a jugaSEBUAH& && => A &). Jika T adalah referensi rvalue A, a juga (A && && => A &&). Dalam kedua kasus, a adalah objek bernama dalam ruang lingkup yang sebenarnya, tetapi T berisi informasi dari "tipe referensi" nya dari sudut pandang pemanggil lingkup. Informasi ini (T) dilewatkan sebagai parameter template ke forward dan 'a' dipindahkan atau tidak sesuai dengan jenis T.


19
2017-08-18 15:57



Ini seperti menyalin semantik, tetapi alih-alih harus menggandakan semua data yang Anda dapatkan untuk mencuri data dari objek yang "dipindahkan" dari.


17
2018-06-23 22:56