Kapan membuat tipe tidak dapat dipindahkan di C ++ 11?

127

Saya terkejut ini tidak muncul di hasil pencarian saya, saya pikir seseorang akan menanyakan ini sebelumnya, mengingat kegunaan semantik bergerak di C ++ 11:

Kapan saya harus (atau apakah sebaiknya saya) membuat kelas tidak dapat dipindahkan di C ++ 11?

(Alasan lain selain masalah kompatibilitas dengan kode yang ada, yaitu.)

pengguna541686
sumber
2
boost selalu selangkah lebih maju - "mahal untuk memindahkan jenis" ( boost.org/doc/libs/1_48_0/doc/html/container/move_emplace.html )
SChepurin
1
Saya pikir ini adalah pertanyaan yang sangat bagus dan berguna ( +1dari saya) dengan jawaban yang sangat menyeluruh dari Herb (atau kembarannya, sepertinya ), jadi saya menjadikannya sebagai entri FAQ. Jika seseorang keberatan hanya ping saya di ruang tunggu , jadi ini bisa dibahas di sana.
sbi
1
Kelas bergerak AFAIK masih dapat dikenakan pemotongan, jadi masuk akal untuk melarang pemindahan (dan penyalinan) untuk semua kelas dasar polimorfik (yaitu semua kelas dasar dengan fungsi virtual).
Philipp
1
@Mehrdad: Saya hanya mengatakan bahwa "T memiliki konstruktor bergerak" dan " T x = std::move(anotherT);legal" tidak setara. Yang terakhir adalah permintaan pindah yang mungkin jatuh kembali ke ctor salinan jika T tidak memiliki ctor bergerak. Jadi, apa sebenarnya arti "bergerak"?
sellibitze
1
@Mehrdad: Lihat bagian pustaka standar C ++ tentang arti "MoveConstructible". Beberapa iterator mungkin tidak memiliki konstruktor bergerak, tetapi masih berupa MoveConstructible. Berhati-hatilah dengan berbagai definisi orang "bergerak" yang ada dalam pikiran.
sellibitze

Jawaban:

110

Jawaban Herb (sebelum diedit) benar-benar memberi contoh yang baik dari jenis yang seharusnya tidak menjadi bergerak: std::mutex.

Jenis mutex asli OS (misalnya pthread_mutex_tpada platform POSIX) mungkin bukan "invarian lokasi" yang berarti alamat objek adalah bagian dari nilainya. Misalnya, OS mungkin menyimpan daftar pointer ke semua objek mutex yang diinisialisasi. Jika std::mutexberisi jenis mutex OS asli sebagai anggota data dan alamat jenis asli harus tetap diperbaiki (karena OS menyimpan daftar penunjuk ke mutexnya) maka salah satu dari std::mutexmereka harus menyimpan jenis mutex asli di heap sehingga akan tetap di lokasi yang sama ketika dipindahkan antar std::mutexobjek atau std::mutextidak boleh bergerak. Menyimpannya di heap tidak mungkin, karena a std::mutexmemiliki constexprkonstruktor dan harus memenuhi syarat untuk inisialisasi konstan (yaitu inisialisasi statis) sehinggastd::mutexdijamin akan dibangun sebelum eksekusi program dimulai, sehingga konstruktornya tidak dapat menggunakannya new. Jadi satu-satunya pilihan yang tersisa adalah std::mutexmenjadi tidak tergoyahkan.

Alasan yang sama berlaku untuk jenis lain yang berisi sesuatu yang membutuhkan alamat tetap. Jika alamat sumber daya harus tetap, jangan pindahkan!

Ada argumen lain untuk tidak bergerak, std::mutexyaitu akan sangat sulit melakukannya dengan aman, karena Anda perlu tahu bahwa tidak ada yang mencoba mengunci mutex pada saat mutex sedang dipindahkan. Karena mutex adalah salah satu blok bangunan yang dapat Anda gunakan untuk mencegah balapan data, akan sangat disayangkan jika mereka tidak aman terhadap balapan itu sendiri! Dengan immovable, std::mutexAnda tahu satu-satunya hal yang dapat dilakukan siapa pun padanya setelah dibuat dan sebelum dihancurkan adalah dengan menguncinya dan membukanya, dan operasi tersebut secara eksplisit dijamin aman untuk thread dan tidak memperkenalkan data race. Argumen yang sama ini berlaku untuk std::atomic<T>objek: kecuali mereka dapat dipindahkan secara atomis, tidak mungkin untuk memindahkannya dengan aman, utas lain mungkin mencoba memanggilcompare_exchange_strongpada objek tepat pada saat itu sedang dipindahkan. Jadi kasus lain di mana jenis tidak boleh dipindahkan adalah di mana mereka adalah blok bangunan tingkat rendah dari kode bersamaan yang aman dan harus memastikan atomisitas dari semua operasi pada mereka. Jika nilai objek dapat dipindahkan ke objek baru kapan saja, Anda perlu menggunakan variabel atom untuk melindungi setiap variabel atom sehingga Anda tahu apakah aman untuk menggunakannya atau telah dipindahkan ... dan variabel atom untuk melindungi variabel atom itu, dan seterusnya ...

Saya pikir saya akan menggeneralisasi untuk mengatakan bahwa ketika sebuah objek hanyalah sepotong memori murni, bukan jenis yang bertindak sebagai pemegang nilai atau abstraksi nilai, tidak masuk akal untuk memindahkannya. Tipe mendasar seperti inttidak bisa bergerak: memindahkannya hanyalah salinan. Anda tidak dapat merobek nyali dari sebuah int, Anda dapat menyalin nilainya dan kemudian mengaturnya ke nol, tetapi itu masih intdengan nilai, itu hanya byte memori. Tapi intmasih bisa dipindahkandalam istilah bahasa karena salinan adalah operasi pemindahan yang valid. Namun untuk jenis yang tidak dapat disalin, jika Anda tidak ingin atau tidak dapat memindahkan bagian memori dan Anda juga tidak dapat menyalin nilainya, maka itu tidak dapat dipindahkan. Mutex atau variabel atom adalah lokasi memori tertentu (diperlakukan dengan properti khusus) sehingga tidak masuk akal untuk dipindahkan, dan juga tidak dapat disalin, jadi tidak dapat dipindahkan.

Jonathan Wakely
sumber
17
+1 contoh yang kurang eksotis dari sesuatu yang tidak dapat dipindahkan karena memiliki alamat khusus adalah node dalam struktur grafik berarah.
Potatoswatter
3
Jika mutex tidak dapat disalin dan tidak dapat dipindahkan, bagaimana cara menyalin atau memindahkan objek yang berisi mutex? (Seperti kelas aman utas dengan
mutexnya
4
@ tr3w, Anda tidak bisa, kecuali jika Anda membuat mutex di heap dan menyimpannya melalui unique_ptr atau serupa
Jonathan Wakely
2
@ tr3w: Tidakkah Anda akan memindahkan seluruh kelas kecuali bagian mutex?
pengguna541686
3
@BenVoigt, tetapi objek baru akan memiliki mutexnya sendiri. Saya pikir yang dia maksud adalah memiliki operasi pemindahan yang ditentukan pengguna yang memindahkan semua anggota kecuali anggota mutex. Lalu bagaimana jika benda lama itu sudah kadaluarsa? Mutexnya kedaluwarsa dengan itu.
Jonathan Wakely
57

Jawaban singkat: Jika suatu jenis dapat disalin, ia juga harus dapat dipindahkan. Namun, kebalikannya tidak benar: beberapa tipe seperti std::unique_ptrdapat dipindahkan namun tidak masuk akal untuk menyalinnya; ini secara alami adalah tipe yang hanya bergerak.

Jawaban yang sedikit lebih panjang mengikuti ...

Ada dua jenis tipe utama (di antara yang lebih bertujuan khusus seperti ciri):

  1. Jenis nilai seperti, seperti intatau vector<widget>. Ini mewakili nilai-nilai, dan secara alami harus dapat disalin. Dalam C ++ 11, secara umum Anda harus memikirkan pemindahan sebagai pengoptimalan salinan, sehingga semua jenis yang dapat disalin secara alami harus dapat dipindah ... pemindahan hanyalah cara yang efisien untuk melakukan penyalinan dalam kasus yang sering umum yang tidak Anda lakukan ' Saya tidak membutuhkan objek aslinya lagi dan hanya akan menghancurkannya.

  2. Tipe mirip referensi yang ada dalam hierarki pewarisan, seperti kelas dasar dan kelas dengan fungsi anggota yang dilindungi atau virtual. Ini biasanya dipegang oleh penunjuk atau referensi, sering kali base*atau base&, dan karenanya tidak menyediakan konstruksi salinan untuk menghindari pemotongan; jika Anda ingin mendapatkan objek lain seperti yang sudah ada, Anda biasanya memanggil fungsi virtual seperti clone. Ini tidak memerlukan konstruksi pemindahan atau penugasan karena dua alasan: Mereka tidak dapat disalin, dan mereka sudah memiliki operasi "pemindahan" alami yang lebih efisien - Anda cukup menyalin / memindahkan penunjuk ke objek dan objek itu sendiri tidak harus pindah ke lokasi memori baru sama sekali.

Sebagian besar jenis termasuk dalam salah satu dari dua kategori itu, tetapi ada jenis jenis lain juga yang juga berguna, hanya lebih jarang. Secara khusus di sini, jenis yang mengekspresikan kepemilikan unik suatu sumber daya, seperti std::unique_ptr, secara alami merupakan jenis yang hanya bergerak, karena tidak seperti nilai (tidak masuk akal untuk menyalinnya) tetapi Anda menggunakannya secara langsung (tidak selalu dengan penunjuk atau referensi) dan ingin memindahkan objek jenis ini dari satu tempat ke tempat lain.

Herb Sutter
sumber
61
Bisakah Herb Sutter yang asli berdiri? :)
fredoverflow
6
Ya, saya beralih dari menggunakan satu akun Google OAuth ke yang lain dan tidak mau repot mencari cara untuk menggabungkan dua info masuk yang memberi saya di sini. (Namun argumen lain melawan OAuth di antara yang jauh lebih menarik.) Saya mungkin tidak akan menggunakan yang lain lagi, jadi ini yang akan saya gunakan untuk saat ini untuk posting SO sesekali.
Herb Sutter
7
Saya pikir itu std::mutextidak tergoyahkan, karena mutex POSIX digunakan oleh alamat.
Anak Anjing
9
@SChepurin: Sebenarnya, itu disebut HerbOverflow, kalau begitu.
sbi
26
Ini mendapatkan banyak suara positif, tidak ada yang memperhatikan bahwa ia mengatakan kapan suatu jenis harus hanya bergerak, yang bukan pertanyaannya? :)
Jonathan Wakely
18

Sebenarnya ketika saya mencari-cari, saya menemukan beberapa tipe di C ++ 11 tidak dapat dipindahkan:

  • semua mutexjenis ( recursive_mutex, timed_mutex, recursive_timed_mutex,
  • condition_variable
  • type_info
  • error_category
  • locale::facet
  • random_device
  • seed_seq
  • ios_base
  • basic_istream<charT,traits>::sentry
  • basic_ostream<charT,traits>::sentry
  • semua atomictipe
  • once_flag

Rupanya ada diskusi di Clang: https://groups.google.com/forum/?fromgroups=#!topic/comp.std.c++/pCO1Qqb3Xa4

billz
sumber
1
... iterator tidak boleh dipindahkan ?! Apa sebabnya?
pengguna541686
ya, saya pikir iterators / iterator adaptorsharus diedit karena C ++ 11 memiliki move_iterator?
billz
Oke sekarang saya hanya bingung. Apakah Anda berbicara tentang iterator yang menggerakkan target mereka , atau tentang memindahkan iterator itu sendiri ?
pengguna541686
1
Begitu juga std::reference_wrapper. Oke, yang lainnya memang sepertinya tidak bisa dipindahkan.
Christian Rau
1
Ini tampaknya jatuh ke dalam tiga kategori: 1. tingkat rendah yang berhubungan dengan konkurensi jenis (teori atom, mutexes), 2. dasar kelas polimorfik ( ios_base, type_info, facet), 3. berbagai macam hal-hal aneh ( sentry). Mungkin satu-satunya kelas yang tidak dapat digerakkan yang akan ditulis oleh programmer rata-rata ada di kategori kedua.
Philipp
0

Alasan lain yang saya temukan - kinerja. Katakanlah Anda memiliki kelas 'a' yang memiliki nilai. Anda ingin mengeluarkan antarmuka yang memungkinkan pengguna mengubah nilai untuk waktu terbatas (untuk cakupan).

Cara untuk mencapai ini adalah dengan mengembalikan objek 'scope guard' dari 'a' yang mengembalikan nilainya ke destruktornya, seperti:

class a 
{ 
    int value = 0;

  public:

    struct change_value_guard 
    { 
        friend a;
      private:
        change_value_guard(a& owner, int value) 
            : owner{ owner } 
        { 
            owner.value = value;
        }
        change_value_guard(change_value_guard&&) = delete;
        change_value_guard(const change_value_guard&) = delete;
      public:
        ~change_value_guard()
        {
            owner.value = 0;
        }
      private:
        a& owner;
    };

    change_value_guard changeValue(int newValue)
    { 
        return{ *this, newValue };
    }
};

int main()
{
    a a;
    {
        auto guard = a.changeValue(2);
    }
}

Jika saya membuat change_value_guard dapat dipindahkan, saya harus menambahkan 'jika' ke destruktornya yang akan memeriksa apakah penjaga telah dipindahkan - itu adalah tambahan jika, dan berdampak pada kinerja.

Ya, tentu, itu mungkin dapat dioptimalkan oleh pengoptimal yang waras, tetapi tetap bagus bahasanya (ini membutuhkan C ++ 17 meskipun, untuk dapat mengembalikan jenis yang tidak dapat dipindahkan membutuhkan penghapusan salinan yang dijamin) tidak memerlukan kami untuk membayar itu jika kita tidak akan memindahkan penjaga selain mengembalikannya dari fungsi pembuatan (prinsip jangan-bayar-untuk-apa-jangan-gunakan).

saarraz1.dll
sumber