C ++: Pointer pintar, Pointer mentah, No Pointer? [Tutup]

48

Dalam ruang lingkup pengembangan game di C ++, apa pola pilihan Anda terkait penggunaan pointer (baik itu tidak ada, mentah, tercakup, dibagikan, atau di antara pintar dan bodoh)?

Anda mungkin mempertimbangkan

  • kepemilikan objek
  • kemudahan penggunaan
  • menyalin kebijakan
  • atas
  • referensi siklik
  • platform target
  • gunakan dengan wadah
jmp97
sumber

Jawaban:

32

Setelah mencoba berbagai pendekatan, hari ini saya menemukan diri saya selaras dengan Panduan Gaya Google C ++ :

Jika Anda benar-benar membutuhkan pointer semantik, scoped_ptr sangat bagus. Anda hanya harus menggunakan std :: tr1 :: shared_ptr dalam kondisi yang sangat spesifik, seperti ketika objek harus dipegang oleh wadah STL. Anda seharusnya tidak pernah menggunakan auto_ptr. [...]

Secara umum, kami lebih suka bahwa kami merancang kode dengan kepemilikan objek yang jelas. Kepemilikan objek paling jelas diperoleh dengan menggunakan objek secara langsung sebagai bidang atau variabel lokal, tanpa menggunakan pointer sama sekali. [..]

Meskipun mereka tidak disarankan, referensi penghitung referensi terkadang merupakan cara paling sederhana dan paling elegan untuk menyelesaikan masalah.

jmp97
sumber
14
Hari ini, Anda mungkin ingin menggunakan std :: unique_ptr daripada scoped_ptr.
Klaim
24

Saya juga mengikuti jalur pemikiran "kepemilikan yang kuat". Saya ingin dengan jelas menggambarkan bahwa "kelas ini memiliki anggota ini" jika sesuai.

Saya jarang menggunakan shared_ptr. Jika saya melakukannya, saya menggunakan liberal weak_ptrkapan saja saya bisa sehingga saya bisa memperlakukannya seperti pegangan untuk objek daripada meningkatkan jumlah referensi.

Saya menggunakan scoped_ptrsemua tempat. Ini menunjukkan kepemilikan yang jelas. Satu-satunya alasan saya tidak hanya membuat objek seperti itu anggota adalah karena Anda dapat meneruskan mendeklarasikannya jika mereka berada di scoped_ptr.

Jika saya perlu daftar objek, saya gunakan ptr_vector. Ini lebih efisien dan memiliki efek samping lebih sedikit daripada menggunakan vector<shared_ptr>. Saya pikir Anda mungkin tidak dapat meneruskan menyatakan jenis di ptr_vector (sudah lama), tetapi semantik itu membuatnya layak menurut pendapat saya. Pada dasarnya jika Anda menghapus objek dari daftar itu akan dihapus secara otomatis. Ini juga menunjukkan kepemilikan yang jelas.

Jika saya perlu referensi ke sesuatu, saya mencoba menjadikannya referensi daripada pointer telanjang. Terkadang ini tidak praktis (yaitu setiap kali Anda membutuhkan referensi setelah objek dibangun). Either way, referensi menunjukkan dengan jelas bahwa Anda tidak memiliki objek, dan jika Anda mengikuti semantik pointer bersama di tempat lain maka pointer telanjang umumnya tidak menimbulkan kebingungan tambahan (terutama jika Anda mengikuti aturan "no manual delete") .

Dengan metode ini, satu permainan iPhone yang saya kerjakan hanya dapat memiliki satu deletepanggilan, dan itu berada di jembatan Obj-C ke C ++ yang saya tulis.

Secara umum saya berpendapat bahwa manajemen memori terlalu penting untuk diserahkan kepada manusia. Jika Anda dapat menghapus secara otomatis, Anda harus melakukannya. Jika overhead dari shared_ptr terlalu mahal pada saat dijalankan (dengan asumsi Anda mematikan dukungan threading, dll), Anda mungkin harus menggunakan sesuatu yang lain (yaitu pola bucket) untuk menurunkan alokasi dinamis Anda.

Tetrad
sumber
1
Ringkasan yang luar biasa. Apakah yang Anda maksud shared_ptr sebenarnya bertentangan dengan Anda menyebutkan smart_ptr?
jmp97
Ya, maksud saya shared_ptr. Saya akan memperbaikinya.
Tetrad
10

Gunakan alat yang tepat untuk pekerjaan itu.

Jika program Anda dapat membuang pengecualian, pastikan kode Anda adalah pengecualian. Menggunakan smart pointer, RAII, dan menghindari konstruksi 2 fase adalah titik awal yang baik.

Jika Anda memiliki referensi siklik tanpa semantik kepemilikan yang jelas, Anda dapat mempertimbangkan untuk menggunakan perpustakaan pengumpulan sampah atau refactoring desain Anda.

Perpustakaan yang baik akan memungkinkan Anda untuk kode ke konsep bukan tipe sehingga tidak masalah dalam banyak kasus jenis pointer yang Anda gunakan di luar masalah manajemen sumber daya.

Jika Anda bekerja di lingkungan multi-utas, pastikan Anda memahami jika objek Anda berpotensi dibagikan di seluruh utas. Salah satu alasan utama untuk mempertimbangkan menggunakan boost :: shared_ptr atau std :: tr1 :: shared_ptr adalah karena ia menggunakan jumlah referensi yang aman.

Jika Anda khawatir tentang alokasi jumlah referensi yang terpisah, ada banyak cara untuk mengatasi hal ini. Dengan menggunakan boost :: shared_ptr library Anda dapat mengumpulkan alokasi counter referensi atau menggunakan boost :: make_shared (preferensi saya) yang mengalokasikan objek dan jumlah referensi dalam satu alokasi sehingga mengurangi sebagian besar masalah cache miss yang dimiliki orang. Anda dapat menghindari hit kinerja memperbarui penghitungan referensi dalam kode kritis kinerja dengan memegang referensi ke objek di tingkat paling atas dan membagikan referensi langsung ke objek.

Jika Anda memerlukan kepemilikan bersama tetapi tidak ingin membayar biaya penghitungan referensi atau pengumpulan sampah, pertimbangkan untuk menggunakan objek yang tidak dapat diubah atau salinan idiom tulis.

Ingatlah bahwa jauh-jauh kemenangan kinerja terbesar Anda akan berada pada tingkat arsitektur, diikuti oleh tingkat algoritma, dan sementara masalah tingkat rendah ini sangat penting mereka harus ditangani hanya setelah Anda mengatasi masalah utama. Jika Anda berurusan dengan masalah kinerja di tingkat cache yang hilang maka Anda memiliki banyak masalah yang juga harus Anda sadari seperti berbagi palsu yang tidak ada hubungannya dengan pointer per katakan.

Jika Anda menggunakan pointer pintar hanya untuk berbagi sumber daya seperti tekstur atau model, pertimbangkan perpustakaan yang lebih khusus seperti Boost.Flyweight.

Setelah standar baru menjadi semantik langkah yang diadopsi, referensi nilai, dan penerusan yang sempurna akan membuat bekerja dengan benda dan wadah mahal jauh lebih mudah dan lebih efisien. Sampai saat itu jangan menyimpan pointer dengan semantik salinan destruktif, seperti auto_ptr atau unique_ptr, dalam sebuah Container (konsep standar). Pertimbangkan untuk menggunakan pustaka Boost.Pointer Container atau menyimpan pointer cerdas kepemilikan bersama di Kontainer. Dalam kode kritis kinerja, Anda dapat mempertimbangkan menghindari kedua hal ini demi wadah yang mengganggu seperti yang ada di Boost.Intrusive.

Platform target seharusnya tidak terlalu memengaruhi keputusan Anda. Perangkat tertanam, ponsel pintar, telepon bodoh, PC, dan konsol semuanya dapat menjalankan kode dengan baik. Persyaratan proyek seperti anggaran memori ketat atau tidak ada alokasi dinamis yang pernah / setelah dimuat adalah masalah yang lebih valid dan harus memengaruhi pilihan Anda.

Kain triko vol
sumber
3
Penanganan pengecualian pada konsol bisa sedikit cerdik - XDK khususnya semacam pengecualian-bermusuhan.
Crashworks
1
Platform target benar-benar harus memengaruhi desain Anda. Perangkat keras yang mengubah data Anda kadang-kadang dapat memiliki pengaruh besar pada kode-sumber Anda. Arsitektur PS3 adalah contoh konkret di mana Anda benar-benar perlu membawa perangkat keras ke dalam merancang sumber daya dan manajemen memori serta penyaji Anda.
Simon
Saya hanya sedikit tidak setuju, khususnya yang berkaitan dengan GC. Sebagian besar waktu, referensi siklik bukan masalah untuk skema yang dihitung referensi. Umumnya masalah kepemilikan siklik ini muncul karena orang tidak berpikir dengan benar tentang kepemilikan benda. Hanya karena sebuah objek perlu menunjuk ke sesuatu, bukan berarti ia harus memiliki pointer itu. Contoh yang sering dikutip adalah pointer belakang di pohon, tetapi orang tua ke pointer di pohon dapat dengan aman menjadi pointer mentah tanpa mengorbankan keselamatan.
Tim Seguine
4

Jika Anda menggunakan C ++ 0x, gunakan std::unique_ptr<T>.

Tidak memiliki overhead kinerja, tidak seperti std::shared_ptr<T>yang memiliki referensi menghitung overhead. Unique_ptr memiliki penunjuknya, dan Anda dapat mentransfer kepemilikan sekitar dengan semantik langkah C ++ 0x . Anda tidak dapat menyalinnya - hanya memindahkannya.

Ini juga dapat digunakan dalam wadah, misalnya std::vector<std::unique_ptr<T>>, yang kompatibel dengan biner dan kinerjanya identik std::vector<T*>, tetapi tidak akan membocorkan memori jika Anda menghapus elemen atau menghapus vektor. Ini juga memiliki kompatibilitas yang lebih baik dengan algoritma STL daripada ptr_vector.

IMO untuk banyak tujuan, ini adalah wadah yang ideal: akses acak, kecuali keamanan, mencegah kebocoran memori, overhead rendah untuk realokasi vektor (hanya bergerak di sekitar pointer di belakang layar). Sangat berguna untuk banyak keperluan.

AshleysBrain
sumber
3

Adalah praktik yang baik untuk mendokumentasikan kelas mana yang memiliki petunjuk apa. Lebih disukai, Anda hanya menggunakan objek normal, dan tidak ada petunjuk kapan pun Anda bisa.

Namun, ketika Anda perlu melacak sumber daya, melewati pointer adalah satu-satunya pilihan. Ada beberapa kasus:

  • Anda mendapatkan pointer dari tempat lain, tetapi tidak mengelolanya: cukup gunakan pointer normal dan dokumentasikan sehingga tidak ada pembuat kode setelah Anda mencoba menghapusnya.
  • Anda mendapatkan pointer dari tempat lain, dan Anda melacaknya: gunakan scoped_ptr.
  • Anda mendapatkan pointer dari tempat lain, dan Anda melacaknya tetapi perlu metode khusus untuk menghapusnya: gunakan shared_ptr dengan metode penghapusan khusus.
  • Anda memerlukan pointer dalam wadah STL: itu akan disalin sehingga Anda perlu meningkatkan :: shared_ptr.
  • Banyak kelas membagikan pointer, dan tidak jelas siapa yang akan menghapusnya: shared_ptr (kasus di atas sebenarnya merupakan kasus khusus dari poin ini).
  • Anda membuat pointer sendiri dan hanya Anda yang membutuhkannya: jika Anda benar-benar tidak dapat menggunakan objek normal: scoped_ptr.
  • Anda membuat pointer dan akan membagikannya dengan kelas lain: shared_ptr.
  • Anda membuat pointer dan meneruskannya: gunakan pointer normal dan dokumentasikan antarmuka Anda sehingga pemilik baru tahu bahwa ia harus mengelola sendiri sumber dayanya!

Saya pikir itu cukup banyak mencakup bagaimana saya mengelola sumber daya saya sekarang. Biaya memori dari pointer seperti shared_ptr umumnya dua kali lipat dari biaya memori dari pointer normal. Saya tidak berpikir bahwa overhead ini terlalu besar, tetapi jika Anda memiliki sumber daya yang rendah Anda harus mempertimbangkan merancang game Anda untuk mengurangi jumlah pointer cerdas. Pada kasus lain, saya hanya mendesain prinsip-prinsip yang baik seperti peluru di atas dan profiler akan memberi tahu saya di mana saya akan membutuhkan lebih banyak kecepatan.

Nef
sumber
1

Ketika datang untuk meningkatkan pointer secara khusus, saya pikir mereka harus dihindari selama implementasi mereka tidak persis apa yang Anda butuhkan. Mereka datang dengan biaya yang lebih besar dari yang diperkirakan orang pada awalnya. Mereka menyediakan antarmuka yang memungkinkan Anda untuk melewati bagian vital dan penting dari memori dan manajemen sumber daya Anda.

Mengenai pengembangan perangkat lunak apa pun, saya pikir penting untuk memikirkan data Anda. Sangat penting bagaimana data Anda direpresentasikan dalam memori. Alasan untuk ini adalah bahwa kecepatan CPU telah meningkat pada tingkat yang jauh lebih besar daripada waktu akses memori. Ini sering menjadikan cache memori sebagai hambatan utama dari sebagian besar game komputer modern. Dengan menyejajarkan data Anda secara linear dalam memori sesuai dengan urutan akses, jauh lebih bersahabat dengan cache. Solusi semacam ini sering mengarah pada desain yang lebih bersih, kode lebih sederhana dan kode pasti yang lebih mudah untuk di-debug. Pointer pintar dengan mudah menyebabkan alokasi sumber daya memori dinamis sering, ini menyebabkan mereka tersebar di seluruh memori.

Ini bukan optimasi prematur, ini adalah keputusan sehat yang dapat dan harus diambil sedini mungkin. Ini adalah pertanyaan tentang pemahaman arsitektur perangkat keras yang akan dijalankan oleh perangkat lunak Anda dan ini penting.

Sunting: Ada beberapa hal yang perlu dipertimbangkan mengenai kinerja dari shared-pointer:

  • Penghitung referensi adalah tumpukan dialokasikan.
  • Jika Anda menggunakan keamanan utas diaktifkan, penghitungan referensi dilakukan melalui operasi yang saling terkait.
  • Melewati pointer dengan nilai mengubah jumlah referensi, yang berarti operasi yang saling terkait kemungkinan besar menggunakan akses acak dalam memori (mengunci + kemungkinan cache yang hilang).
Simon
sumber
2
Anda kehilangan saya dengan 'dihindari bagaimanapun caranya.' Lalu Anda menjelaskan jenis pengoptimalan yang jarang menjadi perhatian untuk game dunia nyata. Sebagian besar pengembangan game ditandai oleh masalah pengembangan (keterlambatan, bug, pemutaran, dll) bukan oleh kurangnya kinerja cache CPU. Jadi saya sangat tidak setuju dengan gagasan bahwa saran ini bukan optimasi prematur.
kevin42
2
Saya harus setuju dengan desain awal tata letak data. Penting untuk mendapatkan kinerja apa pun dari konsol / perangkat seluler modern dan merupakan sesuatu yang tidak boleh diabaikan.
Olly
1
Ini adalah masalah yang saya lihat di salah satu studio AAA yang saya kerjakan. Anda juga dapat mendengarkan Kepala Arsitek di Insomniac Games, Mike Acton. Saya tidak mengatakan bahwa boost adalah perpustakaan yang buruk, itu tidak hanya cocok untuk game berkinerja tinggi.
Simon
1
@ kevin42: Koherensi cache mungkin merupakan sumber utama optimasi tingkat rendah dalam pengembangan game saat ini. @Simon: Sebagian besar implementasi shared_ptr menghindari kunci pada platform apa pun yang mendukung bandingkan-dan-tukar, yang mencakup Linux dan PC Windows, dan saya percaya termasuk Xbox.
1
@ Jo Wreschnig: Itu benar, cache-miss masih kemungkinan besar menyebabkan inisialisasi shared-pointer (copy, buat dari pointer lemah dll). L2 cache-miss pada PC modern seperti 200 siklus dan pada PPC (xbox360 / ps3) lebih tinggi. Dengan gim yang intens, Anda mungkin memiliki hingga 1000 objek gim, mengingat setiap objek gim dapat memiliki cukup banyak sumber daya, kami sedang melihat masalah di mana penskalaannya merupakan masalah utama. Ini kemungkinan akan menyebabkan masalah pada akhir siklus pengembangan (saat Anda akan mencapai jumlah objek permainan yang tinggi).
Simon
0

Saya cenderung menggunakan smart pointer di mana-mana. Saya tidak yakin apakah ini ide yang benar-benar bagus, tetapi saya malas, dan saya tidak dapat melihat kelemahan nyata [kecuali jika saya ingin melakukan aritmatika pointer C-style]. Saya menggunakan boost :: shared_ptr karena saya tahu saya bisa menyalinnya - jika dua entitas berbagi gambar, maka jika satu mati yang lain tidak akan kehilangan gambar juga.

Kelemahan dari ini adalah jika satu objek menghapus sesuatu yang ia tunjuk dan miliki, tetapi sesuatu yang lain juga menunjuk ke sana, maka itu tidak dihapus.

Bebek Komunis
sumber
1
Saya telah menggunakan share_ptr hampir di mana-mana juga - tetapi hari ini saya mencoba memikirkan apakah saya benar-benar membutuhkan kepemilikan bersama untuk sebagian data. Jika tidak, mungkin masuk akal untuk menjadikan data tersebut sebagai non-pointer anggota ke struktur data induk. Saya menemukan bahwa kepemilikan yang jelas menyederhanakan desain.
jmp97
0

Manfaat manajemen memori dan dokumentasi yang disediakan oleh smart pointer yang baik berarti saya menggunakannya secara teratur. Namun ketika profiler memasang pipa dan memberi tahu saya penggunaan khusus yang menghabiskan biaya, saya akan kembali ke manajemen pointer yang lebih neolitik.

tenpn
sumber
0

Saya tua, oldskool, dan penghitung siklus. Dalam pekerjaan saya sendiri, saya menggunakan pointer mentah dan tidak ada alokasi dinamis saat runtime (kecuali kolam itu sendiri). Semuanya dikumpulkan, dan kepemilikannya sangat ketat dan tidak pernah dapat dipindahtangankan, jika benar-benar diperlukan saya menulis pengalokasi blok kecil khusus. Saya memastikan bahwa ada keadaan selama pertandingan untuk setiap kelompok untuk membersihkan dirinya sendiri. Ketika sesuatu menjadi berbulu, saya membungkus benda-benda di pegangan sehingga saya bisa memindahkannya, tetapi saya lebih suka tidak. Wadah adalah adat dan tulang yang sangat telanjang. Saya juga tidak menggunakan kembali kode.
Walaupun saya tidak akan pernah memperdebatkan keutamaan dari semua pointer cerdas dan wadah dan iterator dan yang lainnya, saya dikenal karena mampu membuat kode dengan sangat cepat (dan cukup dapat diandalkan - meskipun tidak disarankan bagi orang lain untuk masuk ke kode saya karena alasan yang agak jelas, seperti serangan jantung dan mimpi buruk abadi).

Di tempat kerja, tentu saja, semuanya berbeda, kecuali saya membuat prototipe, yang untungnya saya bisa melakukan banyak hal.

Kaj
sumber
0

Hampir tidak ada meskipun ini diakui sebagai jawaban yang aneh, dan mungkin tidak cocok untuk semua orang.

Tetapi saya merasa jauh lebih berguna dalam kasus pribadi saya untuk menyimpan semua instance dari tipe tertentu dalam urutan pusat, akses acak (thread-safe), dan sebagai gantinya bekerja dengan indeks 32-bit (alamat relatif, yaitu) , daripada petunjuk absolut.

Sebagai permulaan:

  1. Ini membagi dua persyaratan memori pointer analog pada platform 64-bit. Sejauh ini saya tidak pernah membutuhkan lebih dari ~ 4,29 miliar contoh tipe data tertentu.
  2. Itu memastikan bahwa semua instance dari jenis tertentu, T ,, tidak akan pernah terlalu tersebar di memori. Itu cenderung mengurangi kesalahan cache untuk semua jenis pola akses, bahkan melintasi struktur yang ditautkan seperti pohon jika node dihubungkan bersama menggunakan indeks daripada pointer.
  3. Data paralel menjadi mudah untuk diasosiasikan menggunakan array paralel murah (atau array jarang), bukan pohon atau tabel hash.
  4. Set persimpangan dapat ditemukan dalam waktu linier atau lebih baik menggunakan, katakanlah, bitet paralel.
  5. Kita bisa radix mengurutkan indeks dan mendapatkan pola akses sekuensial yang sangat ramah cache.
  6. Kita dapat melacak berapa banyak contoh bagaimana tipe data tertentu telah dialokasikan.
  7. Minimalkan jumlah tempat yang harus berurusan dengan hal-hal seperti keselamatan pengecualian, jika Anda peduli tentang hal-hal semacam itu.

Yang mengatakan, kenyamanan adalah kelemahan serta keamanan jenis. Kita tidak bisa mengakses sebuah instance dari Ttanpa akses ke kedua kontainer dan indeks. Dan data lama yang biasa tidak int32_tmemberi tahu kita tentang tipe data apa yang dirujuknya, jadi tidak ada tipe yang aman. Kami tidak sengaja dapat mencoba mengakses Barmenggunakan indeks untuk Foo. Untuk mengurangi masalah kedua saya sering melakukan hal semacam ini:

struct FooIndex
{
    int32_t index;
};

Kelihatannya konyol tapi itu memberi saya kembali jenis keamanan sehingga orang tidak dapat secara tidak sengaja mencoba mengakses Barmelalui indeksFoo tanpa kesalahan kompiler. Untuk sisi kenyamanan, saya hanya menerima sedikit ketidaknyamanan.

Hal lain yang dapat menjadi ketidaknyamanan utama bagi orang-orang adalah bahwa saya tidak dapat menggunakan polimorfisme berbasis warisan gaya OOP, karena itu akan membutuhkan penunjuk dasar yang dapat menunjuk ke semua jenis subtipe berbeda dengan ukuran dan persyaratan penyelarasan yang berbeda. Tapi saya tidak menggunakan banyak warisan hari ini - lebih suka pendekatan ECS.

Adapun shared_ptr, saya mencoba untuk tidak menggunakannya terlalu banyak. Sebagian besar waktu saya merasa tidak masuk akal untuk berbagi kepemilikan, dan melakukan hal itu secara sembarangan dapat menyebabkan kebocoran logis. Seringkali setidaknya pada level tinggi, satu hal cenderung menjadi milik satu hal. Di mana saya sering merasa tergoda untuk menggunakannya shared_ptradalah memperpanjang umur suatu objek di tempat-tempat yang tidak terlalu banyak berurusan dengan kepemilikan, seperti hanya fungsi lokal di utas untuk memastikan objek tidak hancur sebelum utas selesai menggunakannya.

Untuk mengatasi masalah itu, alih-alih menggunakan shared_ptratau GC atau sesuatu seperti itu, saya sering memilih tugas jangka pendek yang berjalan dari kumpulan utas, dan membuatnya jadi jika utas itu meminta untuk menghancurkan suatu objek, bahwa penghancuran yang sebenarnya ditangguhkan ke brankas. waktu ketika sistem dapat memastikan bahwa tidak ada utas yang perlu mengakses jenis objek tersebut.

Saya kadang-kadang masih menggunakan penghitungan ulang tetapi memperlakukannya seperti strategi upaya terakhir. Dan ada beberapa kasus di mana benar-benar masuk akal untuk berbagi kepemilikan, seperti penerapan struktur data yang persisten, dan di sana saya merasa masuk akal untuk shared_ptrsegera meraihnya.

Jadi bagaimanapun, saya kebanyakan menggunakan indeks, dan menggunakan pointer mentah dan pintar hemat. Saya suka indeks dan jenis pintu yang terbuka ketika Anda tahu benda Anda disimpan secara bersebelahan, dan tidak tersebar di ruang memori.

user77245
sumber