Pindahkan operator penugasan dan `if (this! = & Rhs)`

126

Di operator penugasan sebuah kelas, Anda biasanya perlu memeriksa apakah objek yang ditugaskan adalah objek pemanggilan sehingga Anda tidak mengacaukan semuanya:

Class& Class::operator=(const Class& rhs) {
    if (this != &rhs) {
        // do the assignment
    }

    return *this;
}

Apakah Anda membutuhkan hal yang sama untuk operator penugasan pindahan? Apakah pernah ada situasi yang this == &rhsbenar?

? Class::operator=(Class&& rhs) {
    ?
}
Seth Carnegie
sumber
12
Tidak relevan dengan Q yang ditanyakan, & hanya agar pengguna baru yang membaca Q ini mengikuti garis waktu (karena saya tahu Seth sudah mengetahui hal ini) tidak salah paham, Copy dan Swap adalah cara yang benar untuk mengimplementasikan Copy assignment Operator dimana Anda tidak perlu memeriksa penugasan diri dsb.
Alok Simpan
5
@VaughnCato: A a; a = std::move(a);.
Xeo
11
@VaughnCato Menggunakan std::movenormal. Kemudian pertimbangkan aliasing, dan ketika Anda berada jauh di dalam tumpukan panggilan dan Anda memiliki satu referensi ke T, dan satu sama lain referensi ke T... apakah Anda akan memeriksa identitas di sini? Apakah Anda ingin menemukan panggilan (atau panggilan) pertama di mana mendokumentasikan bahwa Anda tidak dapat menyampaikan argumen yang sama dua kali akan secara statis membuktikan bahwa kedua referensi tersebut tidak akan disebut alias? Atau apakah Anda akan membuat penugasan sendiri berfungsi?
Luc Danton
2
@ LucDanton Saya lebih suka pernyataan di operator penugasan. Jika std :: move digunakan sedemikian rupa sehingga memungkinkan untuk berakhir dengan penugasan mandiri rvalue, saya akan menganggapnya sebagai bug yang harus diperbaiki.
Vaughn Cato
4
@VaughnCato Satu tempat self-swap normal ada di dalam salah satu std::sortatau std::shuffle- setiap kali Anda menukar elemen ith dan jth dari sebuah array tanpa memeriksa i != jterlebih dahulu. ( std::swapdiimplementasikan dalam hal tugas pindah.)
Quuxplusone

Jawaban:

143

Wow, begitu banyak yang harus dibersihkan di sini ...

Pertama, Copy dan Swap tidak selalu merupakan cara yang benar untuk mengimplementasikan Copy Assignment. Hampir pasti dalam kasus dumb_array, ini adalah solusi yang kurang optimal.

Penggunaan Copy dan Swap adalah untuk dumb_arraycontoh klasik menempatkan operasi paling mahal dengan fitur terlengkap di lapisan bawah. Ini sempurna untuk klien yang menginginkan fitur terlengkap dan bersedia membayar penalti kinerja. Mereka mendapatkan apa yang mereka inginkan.

Namun, ini merupakan bencana bagi klien yang tidak membutuhkan fitur lengkap dan malah mencari kinerja tertinggi. Bagi mereka dumb_arrayhanyalah perangkat lunak lain yang harus mereka tulis ulang karena terlalu lambat. Telah dumb_arraydirancang secara berbeda, itu bisa memuaskan kedua klien tanpa kompromi ke salah satu klien.

Kunci untuk memuaskan kedua klien adalah dengan membangun operasi tercepat di tingkat terendah, dan kemudian menambahkan API di atasnya untuk fitur yang lebih lengkap dengan biaya lebih banyak. Yaitu Anda membutuhkan jaminan pengecualian yang kuat, baiklah, Anda membayarnya. Anda tidak membutuhkannya? Inilah solusi yang lebih cepat.

Mari kita konkret: Berikut ini, jaminan pengecualian dasar yang cepat, operator Tugas Salin untuk dumb_array:

dumb_array& operator=(const dumb_array& other)
{
    if (this != &other)
    {
        if (mSize != other.mSize)
        {
            delete [] mArray;
            mArray = nullptr;
            mArray = other.mSize ? new int[other.mSize] : nullptr;
            mSize = other.mSize;
        }
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }
    return *this;
}

Penjelasan:

Salah satu hal yang lebih mahal yang dapat Anda lakukan pada perangkat keras modern adalah melakukan perjalanan ke heap. Apa pun yang dapat Anda lakukan untuk menghindari perjalanan ke heap adalah menghabiskan waktu & tenaga dengan baik. Klien dumb_arraymungkin ingin sering menetapkan array dengan ukuran yang sama. Dan ketika mereka melakukannya, yang perlu Anda lakukan hanyalah memcpy(tersembunyi di bawah std::copy). Anda tidak ingin mengalokasikan array baru dengan ukuran yang sama dan kemudian membatalkan alokasi yang lama dengan ukuran yang sama!

Sekarang untuk klien Anda yang benar-benar menginginkan keamanan pengecualian yang kuat:

template <class C>
C&
strong_assign(C& lhs, C rhs)
{
    swap(lhs, rhs);
    return lhs;
}

Atau mungkin jika Anda ingin memanfaatkan tugas pindahan di C ++ 11 yang seharusnya:

template <class C>
C&
strong_assign(C& lhs, C rhs)
{
    lhs = std::move(rhs);
    return lhs;
}

Jika dumb_arrayklien menghargai kecepatan, mereka harus memanggil operator=. Jika mereka membutuhkan keamanan pengecualian yang kuat, ada algoritme umum yang dapat mereka panggil yang akan bekerja pada berbagai macam objek dan hanya perlu diterapkan sekali.

Sekarang kembali ke pertanyaan awal (yang memiliki tipe-o pada saat ini):

Class&
Class::operator=(Class&& rhs)
{
    if (this == &rhs)  // is this check needed?
    {
       // ...
    }
    return *this;
}

Ini sebenarnya pertanyaan kontroversial. Beberapa akan mengatakan ya, tentu saja, beberapa akan mengatakan tidak.

Pendapat pribadi saya tidak, Anda tidak perlu cek ini.

Alasan:

Ketika sebuah objek terikat ke referensi nilai r, itu adalah salah satu dari dua hal:

  1. Sementara.
  2. Objek yang ingin Anda percayai oleh si penelepon bersifat sementara.

Jika Anda memiliki referensi ke objek yang bersifat sementara, maka menurut definisi, Anda memiliki referensi unik ke objek tersebut. Itu tidak mungkin direferensikan oleh tempat lain di seluruh program Anda. Yaitu this == &temporary tidak mungkin .

Sekarang jika klien Anda telah berbohong kepada Anda dan berjanji kepada Anda bahwa Anda mendapatkan sementara sementara Anda tidak, maka klien bertanggung jawab untuk memastikan bahwa Anda tidak perlu peduli. Jika Anda ingin benar-benar berhati-hati, saya yakin ini akan menjadi implementasi yang lebih baik:

Class&
Class::operator=(Class&& other)
{
    assert(this != &other);
    // ...
    return *this;
}

Yaitu Jika Anda sedang melewati referensi diri, ini adalah bug pada bagian dari klien yang harus diperbaiki.

Untuk kelengkapannya, berikut adalah operator penugasan pindah untuk dumb_array:

dumb_array& operator=(dumb_array&& other)
{
    assert(this != &other);
    delete [] mArray;
    mSize = other.mSize;
    mArray = other.mArray;
    other.mSize = 0;
    other.mArray = nullptr;
    return *this;
}

Dalam kasus penggunaan tipikal dari tugas bergerak, *thisakan menjadi objek yang dipindahkan dan delete [] mArray;seharusnya tidak ada operasi. Sangat penting bahwa implementasi menghapus pada nullptr secepat mungkin.

Peringatan:

Beberapa orang akan berpendapat bahwa itu swap(x, x)adalah ide yang bagus, atau hanya kejahatan yang perlu. Dan ini, jika swap beralih ke swap default, dapat menyebabkan penugasan pindah sendiri.

Saya tidak setuju bahwa swap(x, x)adalah pernah ide yang baik. Jika ditemukan di kode saya sendiri, saya akan menganggapnya sebagai bug kinerja dan memperbaikinya. Tetapi jika Anda ingin mengizinkannya, sadari bahwa swap(x, x)hanya self-move-assignemnet pada nilai yang dipindahkan. Dan dalam dumb_arraycontoh kita ini tidak akan berbahaya sama sekali jika kita hanya menghilangkan assert, atau membatasinya ke kasus yang dipindahkan:

dumb_array& operator=(dumb_array&& other)
{
    assert(this != &other || mSize == 0);
    delete [] mArray;
    mSize = other.mSize;
    mArray = other.mArray;
    other.mSize = 0;
    other.mArray = nullptr;
    return *this;
}

Jika Anda menetapkan sendiri dua dipindahkan dari (kosong) dumb_array, Anda tidak melakukan sesuatu yang salah selain memasukkan instruksi yang tidak berguna ke dalam program Anda. Pengamatan yang sama ini dapat dilakukan untuk sebagian besar objek.

<Memperbarui>

Saya telah memikirkan masalah ini lagi, dan agak mengubah posisi saya. Sekarang saya percaya bahwa penugasan harus toleran terhadap penugasan mandiri, tetapi kondisi pengeposan pada penugasan salinan dan pemindahan tugas berbeda:

Untuk tugas menyalin:

x = y;

seseorang harus memiliki kondisi pasca di mana nilai ytidak boleh diubah. Bila &x == &ykemudian kondisi pos ini diterjemahkan menjadi: tugas salin sendiri seharusnya tidak berdampak pada nilai x.

Untuk tugas pindah:

x = std::move(y);

seseorang harus memiliki kondisi pasca yang ymemiliki status valid tetapi tidak ditentukan. Bila &x == &ykemudian kondisi pos ini diterjemahkan menjadi: xmemiliki status yang valid tetapi tidak ditentukan. Yaitu tugas pindah sendiri tidak harus tanpa operasi. Tapi seharusnya tidak crash. Kondisi pasca ini konsisten dengan mengizinkan swap(x, x)untuk hanya bekerja:

template <class T>
void
swap(T& x, T& y)
{
    // assume &x == &y
    T tmp(std::move(x));
    // x and y now have a valid but unspecified state
    x = std::move(y);
    // x and y still have a valid but unspecified state
    y = std::move(tmp);
    // x and y have the value of tmp, which is the value they had on entry
}

Di atas berfungsi, selama x = std::move(x)tidak macet. Itu dapat pergi xdalam keadaan valid tetapi tidak ditentukan.

Saya melihat tiga cara untuk memprogram operator penugasan pindahan untuk dumb_arraymencapai ini:

dumb_array& operator=(dumb_array&& other)
{
    delete [] mArray;
    // set *this to a valid state before continuing
    mSize = 0;
    mArray = nullptr;
    // *this is now in a valid state, continue with move assignment
    mSize = other.mSize;
    mArray = other.mArray;
    other.mSize = 0;
    other.mArray = nullptr;
    return *this;
}

Implementasi di atas mentolerir penugasan mandiri, tetapi *thisdan otherakhirnya menjadi larik berukuran nol setelah penugasan gerak sendiri, tidak peduli berapa nilai aslinya *this. Ini bagus.

dumb_array& operator=(dumb_array&& other)
{
    if (this != &other)
    {
        delete [] mArray;
        mSize = other.mSize;
        mArray = other.mArray;
        other.mSize = 0;
        other.mArray = nullptr;
    }
    return *this;
}

Implementasi di atas mentolerir penugasan mandiri dengan cara yang sama seperti yang dilakukan operator penugasan salinan, dengan menjadikannya no-op. Ini juga bagus.

dumb_array& operator=(dumb_array&& other)
{
    swap(other);
    return *this;
}

Hal di atas tidak masalah hanya jika dumb_arraytidak menyimpan sumber daya yang harus dihancurkan "segera". Misalnya jika satu-satunya sumber daya adalah memori, hal di atas sudah cukup. Jika dumb_arraymungkin dapat menahan kunci mutex atau status buka file, klien dapat mengharapkan sumber daya tersebut di lh tugas pemindahan segera dirilis dan oleh karena itu implementasi ini dapat menjadi masalah.

Biaya yang pertama adalah dua toko tambahan. Biaya yang kedua adalah tes-dan-cabang. Keduanya bekerja. Keduanya memenuhi semua persyaratan Tabel 22 persyaratan MoveAssignable dalam standar C ++ 11. Yang ketiga juga bekerja modulo non-memory-resource-concern.

Ketiga implementasi dapat memiliki biaya yang berbeda tergantung pada perangkat kerasnya: Seberapa mahalkah sebuah cabang? Apakah ada banyak register atau sangat sedikit?

Pengambilannya adalah bahwa tugas-pindah-sendiri, tidak seperti tugas-menyalin-sendiri, tidak harus mempertahankan nilai saat ini.

</Memperbarui>

Satu suntingan terakhir (semoga) terinspirasi oleh komentar Luc Danton:

Jika Anda menulis kelas tingkat tinggi yang tidak secara langsung mengelola memori (tetapi mungkin memiliki basis atau anggota yang melakukannya), implementasi terbaik dari tugas pindah sering kali:

Class& operator=(Class&&) = default;

Ini akan memindahkan menetapkan setiap basis dan setiap anggota secara bergiliran, dan tidak akan termasuk this != &othercek. Ini akan memberi Anda kinerja tertinggi dan keamanan pengecualian dasar dengan asumsi tidak ada invarian yang perlu dipertahankan di antara basis dan anggota Anda. Untuk klien Anda yang menuntut keamanan pengecualian yang kuat, arahkan mereka ke sana strong_assign.

Howard Hinnant
sumber
6
Saya tidak tahu bagaimana perasaan tentang jawaban ini. Itu membuatnya tampak seperti mengimplementasikan kelas-kelas seperti itu (yang mengelola memorinya dengan sangat eksplisit) adalah hal yang umum untuk dilakukan. Memang benar bahwa ketika Anda melakukan write satu kelas tersebut harus sangat sangat berhati-hati tentang jaminan keselamatan pengecualian dan menemukan sweet spot untuk antarmuka untuk menjadi ringkas tapi nyaman, tapi pertanyaannya tampaknya meminta saran umum.
Luc Danton
Ya, saya pasti tidak pernah menggunakan salin-dan-tukar karena hanya membuang-buang waktu untuk kelas yang mengelola sumber daya dan hal-hal (mengapa pergi dan membuat seluruh salinan lain dari semua data Anda?). Dan terima kasih, ini menjawab pertanyaan saya.
Seth Carnegie
5
Tidak disukai untuk saran bahwa memindahkan-tugas-dari-diri harus pernah menegaskan-gagal atau menghasilkan hasil yang "tidak ditentukan". Tugas-dari-diri secara harfiah adalah kasus termudah untuk diselesaikan. Jika kelas Anda mengalami error std::swap(x,x), lalu mengapa saya harus memercayainya untuk menangani operasi yang lebih rumit dengan benar?
Quuxplusone
1
@Quuxplusone: Saya setuju dengan Anda tentang assert-fail, seperti yang dicatat dalam pembaruan pada jawaban saya. Sejauh ini std::swap(x,x), itu hanya berfungsi bahkan ketika x = std::move(x)menghasilkan hasil yang tidak ditentukan. Cobalah! Anda tidak harus mempercayai saya.
Howard Hinnant
Poin bagus @HowardHinnant, swapberfungsi selama x = move(x)daun xdalam kondisi dapat dipindahkan. Dan std::copy/ std::movealgoritme telah ditentukan untuk menghasilkan perilaku yang tidak terdefinisi pada salinan tanpa operasi (aduh; pemain berusia 20 tahun ini memmovemendapatkan kasus yang sepele tapi std::movetidak!). Jadi saya kira saya belum memikirkan "slam dunk" untuk penugasan diri. Tapi jelas penugasan diri adalah sesuatu yang sering terjadi dalam kode nyata, apakah Standar telah memberkatinya atau tidak.
Quuxplusone
11

Pertama, Anda salah menerima tanda tangan operator penugasan pindah. Karena gerakan mencuri sumber daya dari objek sumber, sumber harus berupa constreferensi non -nilai-r.

Class &Class::operator=( Class &&rhs ) {
    //...
    return *this;
}

Perhatikan bahwa Anda masih kembali melalui referensi nilai- l (non- const) .

Untuk kedua jenis penugasan langsung, standarnya bukan untuk memeriksa penugasan mandiri, tetapi untuk memastikan penugasan mandiri tidak menyebabkan crash-and-burn. Umumnya, tidak ada yang secara eksplisit melakukan x = xatau y = std::move(y)memanggil, tetapi aliasing, terutama melalui beberapa fungsi, dapat menyebabkan a = batau c = std::move(d)menjadi penugasan mandiri. Pemeriksaan eksplisit untuk penugasan mandiri, yaitu this == &rhs, yang melewatkan inti fungsi saat benar adalah salah satu cara untuk memastikan keamanan penugasan mandiri. Tapi itu salah satu cara terburuk, karena ini mengoptimalkan kasus langka (semoga) sementara itu anti-pengoptimalan untuk kasus yang lebih umum (karena percabangan dan mungkin cache hilang).

Sekarang ketika (setidaknya) salah satu operand adalah objek sementara secara langsung, Anda tidak akan pernah bisa memiliki skenario penugasan mandiri. Beberapa orang menganjurkan asumsi kasus itu dan mengoptimalkan kodenya sedemikian rupa sehingga kode itu menjadi bodoh untuk bunuh diri ketika anggapannya salah. Saya mengatakan bahwa membuang pemeriksaan objek yang sama pada pengguna tidak bertanggung jawab. Kami tidak membuat argumen itu untuk penugasan salinan; mengapa membalik posisi untuk pindah-tugas?

Mari kita buat contoh, diubah dari responden lain:

dumb_array& dumb_array::operator=(const dumb_array& other)
{
    if (mSize != other.mSize)
    {
        delete [] mArray;
        mArray = nullptr;  // clear this...
        mSize = 0u;        // ...and this in case the next line throws
        mArray = other.mSize ? new int[other.mSize] : nullptr;
        mSize = other.mSize;
    }
    std::copy(other.mArray, other.mArray + mSize, mArray);
    return *this;
}

Penugasan salinan ini menangani penugasan mandiri dengan baik tanpa pemeriksaan eksplisit. Jika ukuran sumber dan tujuan berbeda, maka deallocation dan realokasi mendahului penyalinan. Jika tidak, penyalinan sudah selesai. Penetapan sendiri tidak mendapatkan jalur yang dioptimalkan, itu dibuang ke jalur yang sama seperti saat ukuran sumber dan tujuan dimulai dengan ukuran yang sama. Penyalinan secara teknis tidak diperlukan ketika dua objek itu setara (termasuk ketika mereka adalah objek yang sama), tetapi itulah harga ketika tidak melakukan pemeriksaan kesetaraan (dari segi nilai atau alamat) karena pemeriksaan tersebut sendiri akan sangat sia-sia. waktu. Perhatikan bahwa penugasan sendiri objek di sini akan menyebabkan serangkaian penugasan mandiri tingkat elemen; jenis elemen harus aman untuk melakukan ini.

Seperti contoh sumbernya, penugasan salinan ini memberikan jaminan keamanan pengecualian dasar. Jika Anda menginginkan jaminan yang kuat, gunakan operator tugas terpadu dari kueri Salin dan Tukar asli , yang menangani tugas penyalinan dan pemindahan. Tapi inti dari contoh ini adalah mengurangi keselamatan satu peringkat untuk mendapatkan kecepatan. (BTW, kami mengasumsikan bahwa nilai elemen individu adalah independen; bahwa tidak ada batasan invarian yang membatasi beberapa nilai dibandingkan dengan yang lain.)

Mari kita lihat tugas-pindah untuk tipe yang sama ini:

class dumb_array
{
    //...
    void swap(dumb_array& other) noexcept
    {
        // Just in case we add UDT members later
        using std::swap;

        // both members are built-in types -> never throw
        swap( this->mArray, other.mArray );
        swap( this->mSize, other.mSize );
    }

    dumb_array& operator=(dumb_array&& other) noexcept
    {
        this->swap( other );
        return *this;
    }
    //...
};

void  swap( dumb_array &l, dumb_array &r ) noexcept  { l.swap( r ); }

Jenis yang dapat ditukar yang memerlukan penyesuaian harus memiliki fungsi bebas dua argumen yang dipanggil swapdalam namespace yang sama dengan jenisnya. (Pembatasan namespace memungkinkan panggilan yang tidak memenuhi syarat untuk ditukar bekerja.) Jenis penampung juga harus menambahkan swapfungsi anggota publik agar sesuai dengan penampung standar. Jika anggota swaptidak tersedia, maka fungsi bebas swapmungkin perlu ditandai sebagai teman tipe yang dapat ditukar. Jika Anda menyesuaikan gerakan yang akan digunakan swap, maka Anda harus memberikan kode pertukaran Anda sendiri; kode standar memanggil kode pemindahan tipe, yang akan menghasilkan rekursi timbal balik tak terbatas untuk tipe yang disesuaikan pemindahan.

Seperti destruktor, fungsi swap dan operasi pemindahan harus tidak pernah dibuang jika memungkinkan, dan mungkin ditandai seperti itu (dalam C ++ 11). Jenis dan rutinitas library standar memiliki pengoptimalan untuk jenis pemindahan yang tidak dapat dilempar.

Versi pertama dari tugas pindah ini memenuhi kontrak dasar. Penanda sumber daya sumber ditransfer ke objek tujuan. Sumber daya lama tidak akan bocor karena objek sumber sekarang mengelolanya. Dan objek sumber dibiarkan dalam keadaan dapat digunakan di mana operasi lebih lanjut, termasuk penugasan dan penghancuran, dapat diterapkan padanya.

Perhatikan bahwa pemindahan hak ini secara otomatis aman untuk pemindahan hak, karena swappanggilan itu. Ini juga sangat aman untuk pengecualian. Masalahnya adalah retensi sumber daya yang tidak perlu. Sumber daya lama untuk tujuan secara konseptual tidak lagi diperlukan, tetapi di sini mereka masih ada sehingga objek sumber tetap valid. Jika penghancuran objek sumber yang dijadwalkan berlangsung lama, kita membuang-buang ruang sumber daya, atau lebih buruk lagi jika total ruang sumber daya terbatas dan petisi sumber daya lainnya akan terjadi sebelum objek sumber (baru) resmi mati.

Masalah inilah yang menyebabkan nasehat guru saat ini yang kontroversial tentang penargetan diri selama penugasan pindah. Cara menulis tugas-pindah tanpa sisa sumber daya adalah seperti:

class dumb_array
{
    //...
    dumb_array& operator=(dumb_array&& other) noexcept
    {
        delete [] this->mArray;  // kill old resources
        this->mArray = other.mArray;
        this->mSize = other.mSize;
        other.mArray = nullptr;  // reset source
        other.mSize = 0u;
        return *this;
    }
    //...
};

Sumber disetel ulang ke kondisi default, sementara sumber daya tujuan lama dimusnahkan. Dalam kasus penugasan diri, objek Anda saat ini berakhir dengan bunuh diri. Cara utama untuk mengatasinya adalah mengelilingi kode tindakan dengan sebuah if(this != &other)blok, atau mengencangkannya dan membiarkan klien memakan assert(this != &other)baris awal (jika Anda merasa baik).

Alternatifnya adalah mempelajari cara membuat tugas menyalin sangat aman untuk pengecualian, tanpa tugas terpadu, dan menerapkannya ke tugas pindah:

class dumb_array
{
    //...
    dumb_array& operator=(dumb_array&& other) noexcept
    {
        dumb_array  temp{ std::move(other) };

        this->swap( temp );
        return *this;
    }
    //...
};

Ketika otherdan thisberbeda, otherdikosongkan oleh perpindahan ke tempdan tetap seperti itu. Kemudian thiskehilangan sumber daya lama untuk tempsementara mendapatkan sumber daya yang semula dipegang other. Kemudian sumber daya lama thisterbunuh ketika tempmelakukannya.

Ketika penugasan diri terjadi, pengosongan othermenjadi tempkosong thisjuga. Kemudian objek target mendapatkan sumber dayanya kembali kapan tempdan thisbertukar. Kematian tempmengklaim benda kosong, yang seharusnya tidak ada operasi. Itu this/other object terus sumber daya.

Pindah-tugas tidak boleh melempar selama konstruksi-pindah dan pertukaran juga. Biaya juga untuk menjadi aman selama penugasan mandiri adalah beberapa instruksi lagi di atas tipe tingkat rendah, yang harus dibanjiri oleh panggilan deallocation.

CTMacUser
sumber
Apakah Anda perlu memeriksa apakah ada memori yang dialokasikan sebelum memanggil deleteblok kode kedua Anda?
user3728501
3
Contoh kode kedua Anda, operator penugasan salinan tanpa pemeriksaan penugasan sendiri, salah. std::copymenyebabkan perilaku tidak terdefinisi jika rentang sumber dan tujuan tumpang tindih (termasuk kasus saat keduanya bertepatan). Lihat C ++ 14 [alg.copy] / 3.
MM
6

Saya termasuk dalam kelompok orang yang menginginkan operator aman penugasan mandiri, tetapi tidak ingin menulis cek penugasan mandiri dalam penerapannya operator=. Dan sebenarnya saya bahkan tidak ingin menerapkannyaoperator= sama sekali, saya ingin perilaku default berfungsi 'langsung di luar kotak'. Anggota spesial terbaik adalah mereka yang datang secara gratis.

Dengan demikian, persyaratan MoveAssignable yang ada dalam Standar dijelaskan sebagai berikut (dari 17.6.3.1 Persyaratan argumen template [utility.arg.requirements], n3290):

Ekspresi Jenis Pengembalian Nilai Pengembalian Kondisi pasca
t = rv T & tt ekuivalen dengan nilai rv sebelum penugasan

di mana placeholder dijelaskan sebagai: " t[adalah] nilai l yang dapat dimodifikasi dari tipe T;" dan "rv adalah nilai r tipe T;". Perhatikan bahwa itu adalah persyaratan yang diletakkan pada tipe yang digunakan sebagai argumen untuk templat pustaka Standar, tetapi mencari di tempat lain di Standar Saya perhatikan bahwa setiap persyaratan pada tugas pemindahan serupa dengan yang ini.

Ini berarti a = std::move(a)harus 'aman'. Jika yang Anda butuhkan adalah tes identitas (misalnya this != &other), maka lakukanlah, atau Anda bahkan tidak dapat memasukkan objek Anda std::vector! (Kecuali jika Anda tidak menggunakan anggota / operasi yang memerlukan MoveAssignable; tapi lupakan itu.) Perhatikan bahwa dengan contoh sebelumnya a = std::move(a), maka this == &otherakan benar-benar tahan.

Luc Danton
sumber
Bisakah Anda menjelaskan bagaimana a = std::move(a)tidak bekerja akan membuat kelas tidak berfungsi std::vector? Contoh?
Paul J. Lucas
@ PaulJ.Lucas Calling std::vector<T>::erasetidak diperbolehkan kecuali TMoveAssignable. (Sebagai tambahan IIRC, beberapa persyaratan MoveAssignable dilonggarkan ke MoveInsertable sebagai gantinya di C ++ 14.)
Luc Danton
Oke, jadi Tharus MoveAssignable, tapi mengapa harus erase()bergantung pada pemindahan elemen ke dirinya sendiri ?
Paul J. Lucas
@ PaulJ.Lucas Tidak ada jawaban yang memuaskan untuk pertanyaan itu. Semuanya bermuara pada 'jangan putus kontrak'.
Luc Danton
2

Saat operator=fungsi Anda saat ini ditulis, karena Anda telah membuat argumen rvalue-reference const, tidak mungkin Anda dapat "mencuri" pointer dan mengubah nilai referensi rvalue yang masuk ... Anda tidak bisa mengubahnya, Anda hanya bisa membaca darinya. Saya hanya akan melihat masalah jika Anda mulai memanggil deletepointer, dll. Di thisobjek Anda seperti yang Anda lakukan dalam operator=metode referensi-lvaue normal , tetapi semacam itu mengalahkan poin dari rvalue-version ... yaitu, itu akan Tampaknya berlebihan untuk menggunakan versi rvalue untuk pada dasarnya melakukan operasi yang sama biasanya dengan metode const-lvalue operator=.

Sekarang jika Anda mendefinisikan Anda operator=untuk mengambil constreferensi non -rvalue, maka satu-satunya cara saya bisa melihat pemeriksaan yang diperlukan adalah jika Anda meneruskan thisobjek ke fungsi yang dengan sengaja mengembalikan referensi nilai r daripada sementara.

Sebagai contoh, misalkan seseorang mencoba untuk menulis sebuah operator+fungsi, dan menggunakan campuran referensi nilai r dan referensi nilai untuk "mencegah" sementara tambahan dibuat selama beberapa operasi penjumlahan bertumpuk pada tipe objek:

struct A; //defines operator=(A&& rhs) where it will "steal" the pointers
          //of rhs and set the original pointers of rhs to NULL

A&& operator+(A& rhs, A&& lhs)
{
    //...code

    return std::move(rhs);
}

A&& operator+(A&& rhs, A&&lhs)
{
    //...code

    return std::move(rhs);
}

int main()
{
    A a;

    a = (a + A()) + A(); //calls operator=(A&&) with reference bound to a

    //...rest of code
}

Sekarang, dari apa yang saya pahami tentang referensi rvalue, melakukan hal di atas tidak disarankan (yaitu, Anda harus mengembalikan referensi sementara, bukan rvalue), tetapi, jika seseorang masih melakukan itu, maka Anda ingin memeriksa untuk membuatnya yakin rvalue-reference yang masuk tidak mereferensikan objek yang sama dengan thispointer.

Jason
sumber
Perhatikan bahwa "a = std :: move (a)" adalah cara yang sepele untuk mengatasi situasi ini. Jawaban Anda valid.
Vaughn Cato
1
Sangat setuju bahwa itu cara yang paling sederhana, meskipun saya pikir kebanyakan orang tidak akan melakukannya dengan sengaja :-) ... Perlu diingat bahwa jika rvalue-reference adalah const, maka Anda hanya dapat membacanya, jadi hanya perlu buatlah pemeriksaan jika Anda memutuskan operator=(const T&&)untuk melakukan inisialisasi ulang yang sama dengan thisyang akan Anda lakukan dalam operator=(const T&)metode tipikal daripada operasi gaya bertukar (misalnya, mencuri pointer, dll daripada membuat salinan dalam).
Jason
1

Jawaban saya tetap bahwa tugas pindah tidak harus menyelamatkan diri sendiri, tetapi memiliki penjelasan yang berbeda. Pertimbangkan std :: unique_ptr. Jika saya menerapkannya, saya akan melakukan sesuatu seperti ini:

unique_ptr& operator=(unique_ptr&& x) {
  delete ptr_;
  ptr_ = x.ptr_;
  x.ptr_ = nullptr;
  return *this;
}

Jika Anda melihat Scott Meyers menjelaskan hal ini, dia melakukan hal serupa. (Jika Anda mengembara mengapa tidak melakukan swap - ini memiliki satu tulisan tambahan). Dan ini tidak aman untuk penugasan mandiri.

Terkadang hal ini sangat disayangkan. Pertimbangkan untuk keluar dari vektor semua bilangan genap:

src.erase(
  std::partition_copy(src.begin(), src.end(),
                      src.begin(),
                      std::back_inserter(even),
                      [](int num) { return num % 2; }
                      ).first,
  src.end());

Ini tidak masalah untuk bilangan bulat tetapi saya tidak yakin Anda dapat membuat sesuatu seperti ini bekerja dengan semantik bergerak.

Kesimpulannya: memindahkan tugas ke objek itu sendiri tidak baik dan Anda harus hati-hati.

Pembaruan kecil.

  1. Saya tidak setuju dengan Howard, yang merupakan ide yang buruk, tapi tetap - saya pikir tugas memindahkan diri dari objek yang "dipindahkan" harus bekerja, karena swap(x, x)harus bekerja. Algoritma menyukai hal-hal ini! Selalu menyenangkan ketika kasing sudut berfungsi. (Dan saya belum melihat kasus di mana itu tidak gratis. Tidak berarti itu tidak ada).
  2. Beginilah cara menetapkan unique_ptrs diimplementasikan di libc ++: unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...} Aman untuk tugas pemindahan sendiri.
  3. Pedoman Inti berpendapat bahwa tidak masalah untuk memindahkan tugas sendiri.
Denis Yaroshevskiy
sumber
0

Ada situasi yang (ini == rhs) bisa saya pikirkan. Untuk pernyataan ini: Myclass obj; std :: move (obj) = std :: move (obj)

monster kecil
sumber
Object kelas saya; std :: move (obj) = std :: move (obj);
little_monster