Semantik copy-on-write semantik

10

Saya bertanya-tanya apa manfaat yang mungkin dimiliki copy-on-write? Secara alami, saya tidak mengharapkan pendapat pribadi, tetapi skenario praktis dunia nyata di mana itu bisa bermanfaat secara teknis dan praktis dengan cara yang nyata. Dan secara kasat mata maksud saya lebih dari sekadar menyelamatkan Anda dari mengetik &karakter.

Untuk memperjelas, pertanyaan ini adalah dalam konteks tipe data, di mana penugasan atau penyalinan konstruksi membuat salinan dangkal implisit, tetapi modifikasi untuk itu membuat salinan mendalam tersirat dan menerapkan perubahan untuk itu alih-alih objek aslinya.

Alasan saya bertanya adalah sepertinya saya tidak menemukan manfaat memiliki KK sebagai perilaku implisit default. Saya menggunakan Qt, yang telah menerapkan SAP untuk banyak tipe data, praktis semua yang memiliki beberapa penyimpanan yang dialokasikan secara dinamis. Tetapi bagaimana itu benar-benar bermanfaat bagi pengguna?

Sebuah contoh:

QString s("some text");
QString s1 = s; // now both s and s1 internally use the same resource

qDebug() << s1; // const operation, nothing changes
s1[o] = z; // s1 "detaches" from s, allocates new storage and modifies first character
           // s is still "some text"

Apa yang kita menangkan dengan menggunakan KK dalam contoh ini?

Jika semua yang ingin kita lakukan adalah menggunakan operasi const, s1redundan, mungkin juga digunakan s.

Jika kami berniat mengubah nilainya, maka SAP hanya menunda salinan sumber daya hingga operasi non-const yang pertama, dengan biaya (walaupun minimal) menambah hitungan ref untuk pembagian implisit dan melepaskan dari penyimpanan bersama. Memang terlihat seperti semua overhead yang terlibat dalam KK tidak ada gunanya.

Tidak jauh berbeda dalam konteks lewat parameter - jika Anda tidak ingin mengubah nilai, lulus sebagai referensi const, jika Anda ingin memodifikasi, Anda juga membuat salinan yang tersirat jika Anda tidak ingin memodifikasi objek asli, atau lulus dengan referensi jika Anda ingin memodifikasinya. Lagi-lagi SAP tampak seperti overhead yang tidak perlu yang tidak mencapai apa-apa, dan hanya menambahkan batasan bahwa Anda tidak dapat mengubah nilai asli bahkan jika Anda mau, karena setiap perubahan akan terlepas dari objek asli.

Jadi tergantung pada apakah Anda tahu tentang SAP atau tidak menyadari hal itu, dapat mengakibatkan kode dengan maksud tidak jelas dan overhead yang tidak perlu, atau perilaku yang benar-benar membingungkan yang tidak sesuai dengan harapan dan membuat Anda menggaruk-garuk kepala.

Bagi saya tampaknya ada solusi yang lebih efisien dan lebih mudah dibaca apakah Anda ingin menghindari salinan yang tidak perlu, atau Anda bermaksud membuatnya. Jadi, di mana manfaat praktis dari KK? Saya berasumsi pasti ada beberapa manfaat karena di dalamnya digunakan dalam kerangka kerja yang populer dan kuat.

Selain itu, dari apa yang saya baca, SAP sekarang secara eksplisit dilarang di pustaka standar C ++. Tidak tahu apakah penipu yang saya lihat ada hubungannya dengan itu, tapi bagaimanapun, pasti ada alasan untuk ini.

dtech
sumber

Jawaban:

15

Salin saat menulis digunakan dalam situasi di mana Anda sangat sering membuat salinan objek dan tidak memodifikasinya. Dalam situasi itu, ia membayar untuk dirinya sendiri.

Seperti yang Anda sebutkan, Anda bisa melewatkan objek const, dan dalam banyak kasus itu sudah cukup. Namun, const hanya menjamin bahwa penelepon tidak dapat mengubahnya (kecuali mereka const_cast, tentu saja). Itu tidak menangani kasus multithreading dan tidak menangani kasus-kasus di mana ada panggilan balik (yang mungkin bermutasi objek asli). Melewati objek Kontrak Karya berdasarkan nilai menempatkan tantangan dalam mengelola detail ini pada pengembang API, bukan pengguna API.

Aturan baru untuk C + 11 melarang SAP untuk std::stringkhususnya. Iterator pada string harus dibatalkan jika buffer dukungan dilepaskan. Jika iterator diimplementasikan sebagai char*(Berbeda dengan a string*dan indeks), iterator ini tidak lagi valid. Komunitas C ++ harus memutuskan seberapa sering iterator dapat divalidasi, dan keputusannya adalah yang operator[]seharusnya tidak menjadi salah satu dari kasus tersebut. operator[]pada std::stringpengembalian a char&, yang dapat dimodifikasi. Dengan demikian, operator[]perlu melepaskan string, membatalkan iterator. Ini dianggap sebagai perdagangan yang buruk, dan tidak seperti fungsi seperti end()dan cend(), tidak ada cara untuk meminta versi const operator[]pendek dari const casting string. ( terkait ).

SAPI masih hidup dan berada di luar STL. Secara khusus, saya telah menemukannya sangat berguna dalam kasus-kasus di mana tidak masuk akal bagi pengguna API saya untuk berharap bahwa ada beberapa objek kelas berat di belakang apa yang tampak sebagai objek yang sangat ringan. Saya mungkin ingin menggunakan KK di latar belakang untuk memastikan mereka tidak perlu khawatir dengan detail implementasi tersebut.

Cort Ammon
sumber
Mutasi string yang sama di banyak utas sepertinya merupakan desain yang sangat buruk, terlepas dari apakah Anda menggunakan iterator atau []operator. Jadi, SAP memungkinkan desain yang buruk - itu kedengarannya tidak terlalu bermanfaat :) Poin di paragraf terakhir tampaknya valid, tapi saya sendiri bukan penggemar berat perilaku implisit - orang cenderung menganggapnya biasa saja, dan kemudian memiliki kesulitan mencari tahu mengapa kode tidak bekerja seperti yang diharapkan, dan terus bertanya-tanya sampai mereka mencari tahu apa yang tersembunyi di balik perilaku implisit.
dtech
Adapun titik menggunakan const_castsepertinya dapat merusak SAP semudah itu dapat menembus lewat referensi const. Misalnya, QString::constData()mengembalikan a const QChar *- const_castitu dan SAPI runtuh - Anda akan mengubah data objek asli.
dtech
Jika Anda dapat mengembalikan data dari SAP, Anda harus melepaskan sebelum melakukannya, atau mengembalikan data dalam bentuk yang masih sadar SAP (yang char*jelas tidak sadar). Adapun perilaku implisit, saya pikir Anda benar, ada masalah dengannya. Desain API adalah keseimbangan konstan antara dua ekstrem. Terlalu implisit, dan orang-orang mulai mengandalkan perilaku khusus seolah-olah itu adalah bagian dari spesifikasi. Terlalu eksplisit, dan API menjadi terlalu berat saat Anda mengekspos terlalu banyak detail mendasar yang tidak terlalu penting, dan tiba-tiba ditulis ke dalam spesifikasi API Anda.
Cort Ammon
Saya percaya stringkelas mendapat perilaku SAP karena desainer kompiler memperhatikan bahwa sejumlah besar kode menyalin string daripada menggunakan const-reference. Jika mereka menambahkan SAP, mereka bisa mengoptimalkan kasus ini dan membuat lebih banyak orang bahagia (dan itu legal, hingga C ++ 11). Saya menghargai posisi mereka: sementara saya selalu lulus string saya dengan referensi const, saya telah melihat semua sampah sintaksis yang hanya mengurangi keterbacaan. Saya benci menulis const std::shared_ptr<const std::string>&hanya untuk menangkap semantik yang benar!
Cort Ammon
5

Untuk string dan semacamnya sepertinya akan pesimis kasus penggunaan yang lebih umum daripada tidak, karena kasus umum untuk string sering adalah string kecil, dan di sana overhead SAP cenderung lebih besar daripada biaya hanya menyalin string kecil. Optimasi buffer kecil jauh lebih masuk akal bagi saya di sana untuk menghindari alokasi tumpukan dalam kasus seperti itu daripada salinan string.

Namun, jika Anda memiliki objek yang lebih berat, seperti android, dan Anda ingin menyalinnya dan hanya mengganti lengan cybernetic-nya, COW tampaknya cukup masuk akal sebagai cara untuk mempertahankan sintaks yang dapat berubah sambil menghindari kebutuhan untuk menyalin seluruh android hanya untuk beri salinan lengan yang unik. Menjadikannya tidak berubah sebagai struktur data yang persisten pada saat itu mungkin lebih unggul, tetapi "SAP parsial" yang diterapkan pada masing-masing bagian Android tampaknya masuk akal untuk kasus ini.

Dalam kasus seperti itu, dua salinan Android akan berbagi / misalnya batang tubuh yang sama, kaki, kepala, leher, bahu, panggul, dll. Satu-satunya data yang akan berbeda di antara mereka dan tidak dibagi adalah lengan yang dibuat unik untuk android kedua pada menimpa lengannya.


sumber
Ini semua baik, tetapi tidak menuntut KK, dan masih memiliki banyak implikasi berbahaya. Juga, ada kelemahannya - Anda mungkin sering ingin melakukan instancing objek, dan saya tidak bermaksud mengetik instancing, tetapi salin objek sebagai instance, jadi ketika Anda memodifikasi objek sumber, salinan juga diperbarui. SAP hanya mengecualikan kemungkinan itu, karena setiap perubahan pada objek "bersama" melepaskannya.
dtech
Kebenaran IMO seharusnya tidak "mudah" untuk dicapai, bukan dengan perilaku implisit. Contoh yang benar dari kebenaran adalah kebenaran CONST, karena itu eksplisit dan tidak memberikan ruang untuk ambiguitas atau efek samping yang tidak terlihat. Memiliki sesuatu seperti ini "mudah" dan otomatis tidak pernah membangun tingkat pemahaman ekstra tentang cara kerja, yang tidak hanya penting untuk produktivitas secara keseluruhan, tetapi cukup banyak menghilangkan kemungkinan perilaku yang tidak diinginkan, alasan yang mungkin sulit untuk menentukan . Segala sesuatu yang dimungkinkan secara implisit dengan KK juga mudah dicapai secara eksplisit, dan lebih jelas.
dtech
Pertanyaan saya dilatarbelakangi oleh dilema apakah akan memberikan KK secara default dalam bahasa yang saya kerjakan. Setelah mempertimbangkan pro dan kontra, saya memutuskan untuk tidak memilikinya secara default, tetapi sebagai pengubah yang dapat diterapkan untuk tipe baru atau yang sudah ada. Sepertinya yang terbaik dari kedua dunia, Anda masih dapat memiliki kesaksian tentang KK ketika Anda secara eksplisit menginginkannya.
dtech
@ddriver Apa yang kita miliki adalah sesuatu yang mirip dengan bahasa pemrograman dengan paradigma nodal, kecuali untuk kesederhanaan jenis node semantik nilai penggunaan dan tidak ada semantik tipe referensi (mungkin agak mirip std::vector<std::string>sebelum kita miliki emplace_backdan memindahkan semantik di C ++ 11) . Tapi pada dasarnya kami juga menggunakan instancing. Sistem simpul mungkin atau mungkin tidak mengubah data. Kami memiliki hal-hal seperti pass-through node yang tidak melakukan apa-apa dengan input tetapi hanya mengeluarkan salinan (mereka ada untuk organisasi pengguna programnya). Dalam kasus tersebut, semua data disalin dangkal untuk tipe kompleks ...
@ddriver Copy-on-write kami secara efektif jenis proses penyalinan "membuat contoh unik secara implisit pada perubahan" . Itu membuat mustahil untuk memodifikasi yang asli. Jika objek Adisalin dan tidak ada yang dilakukan untuk objek B, itu adalah salinan dangkal murah untuk tipe data yang kompleks seperti jerat. Sekarang jika kita memodifikasi B, data yang kita modifikasi Bmenjadi unik melalui COW, tetapi Atidak tersentuh (kecuali untuk beberapa jumlah referensi atom).