Haruskah saya lebih memilih petunjuk atau referensi dalam data anggota?


141

Ini adalah contoh sederhana untuk mengilustrasikan pertanyaan:

class A {};

class B
{
    B(A& a) : a(a) {}
    A& a;
};

class C
{
    C() : b(a) {} 
    A a;
    B b; 
};

Jadi B bertanggung jawab untuk memperbarui bagian dari C. Saya menjalankan kode melalui lint dan bertanya tentang anggota referensi: lint # 1725 . Ini berbicara tentang mengurus salinan default dan tugas yang cukup adil, tetapi salinan default dan tugas juga buruk dengan petunjuk, jadi ada sedikit keuntungan di sana.

Saya selalu mencoba menggunakan referensi di mana saya bisa karena pointer telanjang memperkenalkan secara tidak pasti tentang siapa yang bertanggung jawab untuk menghapus pointer itu. Saya lebih suka menyematkan objek berdasarkan nilai tetapi jika saya membutuhkan pointer, saya menggunakan auto_ptr di data anggota kelas yang memiliki pointer, dan meneruskan objek tersebut sebagai referensi.

Saya biasanya hanya akan menggunakan pointer dalam data anggota ketika pointer bisa menjadi null atau bisa berubah. Apakah ada alasan lain untuk lebih memilih petunjuk daripada referensi untuk anggota data?

Benarkah untuk mengatakan bahwa objek yang berisi referensi tidak boleh dialihkan, karena referensi tidak boleh diubah setelah diinisialisasi?


"tetapi penyalinan dan penetapan default juga buruk dengan pointer": Ini tidak sama: Pointer dapat diubah kapan pun Anda mau jika bukan const. Referensi biasanya selalu konstan! (Anda akan melihat ini jika Anda mencoba untuk mengubah anggota Anda menjadi "A & const a;". Kompilator (setidaknya GCC) akan memperingatkan Anda bahwa referensi tetaplah konstan bahkan tanpa kata kunci "const".
mmmmmmmm

Masalah utama dengan ini adalah jika seseorang melakukan B b (A ()), Anda kacau karena Anda berakhir dengan referensi yang menggantung.
log0

Jawaban:


69

Hindari anggota referensi, karena mereka membatasi apa yang dapat dilakukan oleh implementasi kelas (termasuk, seperti yang Anda sebutkan, mencegah implementasi operator tugas) dan tidak memberikan manfaat pada apa yang dapat disediakan kelas.

Contoh soal:

  • Anda dipaksa untuk menginisialisasi referensi di setiap daftar penginisialisasi konstruktor: tidak ada cara untuk memfaktorkan inisialisasi ini ke dalam fungsi lain ( hingga C ++ 0x, tetap saja edit: C ++ sekarang memiliki konstruktor yang mendelegasikan )
  • referensi tidak bisa dipantulkan atau menjadi nol. Ini bisa menjadi keuntungan, tetapi jika kode perlu diubah untuk memungkinkan rebinding atau anggota menjadi nol, semua penggunaan anggota harus diubah.
  • tidak seperti anggota penunjuk, referensi tidak dapat dengan mudah diganti dengan penunjuk atau iterator cerdas karena pemfaktoran ulang mungkin memerlukan
  • Setiap kali referensi digunakan, itu terlihat seperti tipe nilai ( .operator dll), tetapi berperilaku seperti penunjuk (dapat menjuntai) - jadi misalnya, Panduan Gaya Google melarangnya

68
Semua hal yang Anda sebutkan adalah hal yang baik untuk dihindari, jadi jika referensi membantu dalam hal ini - itu baik dan tidak buruk. Menginisialisasi daftar adalah tempat terbaik untuk memasukkan data. Sangat sering Anda harus menyembunyikan operator penugasan, dengan referensi yang tidak perlu. "Tidak bisa dipulihkan" - bagus, penggunaan kembali variabel adalah ide yang buruk.
Mykola Golubyev

4
@Mykola: Saya setuju dengan Anda. Saya lebih suka anggota diinisialisasi dalam daftar penginisialisasi, saya menghindari penunjuk nol dan tentu saja tidak baik untuk variabel mengubah artinya. Di mana pendapat kami berbeda adalah bahwa saya tidak perlu atau ingin kompiler menerapkannya untuk saya. Itu tidak membuat kelas lebih mudah untuk ditulis, saya tidak menemukan bug di area ini yang akan ditangkap, dan kadang-kadang saya menghargai fleksibilitas yang saya dapatkan dari kode yang secara konsisten menggunakan anggota penunjuk (penunjuk cerdas jika sesuai).
James Hopkin

Saya pikir tidak harus menyembunyikan tugas adalah argumen palsu karena Anda tidak perlu menyembunyikan tugas jika kelas berisi penunjuk yang toh tidak bertanggung jawab untuk dihapus - default akan melakukannya. Saya pikir ini adalah jawaban terbaik karena memberikan alasan bagus mengapa petunjuk harus lebih disukai daripada referensi dalam data anggota.
markh44

4
James, itu tidak membuat kode lebih mudah untuk ditulis tetapi itu membuat kode lebih mudah dibaca. Dengan referensi sebagai anggota data Anda tidak perlu bertanya-tanya apakah itu bisa menjadi null pada saat digunakan. Ini berarti Anda dapat melihat kode dengan konteks yang lebih sedikit.
Len Holgate

4
Itu membuat pengujian unit dan mengejek jauh lebih sulit, tidak mungkin hampir tergantung pada berapa banyak yang digunakan, dikombinasikan dengan 'mempengaruhi ledakan grafik' adalah alasan yang cukup kuat IMHO untuk tidak pernah menggunakannya.
Chris Huang-Leaver

160

Aturan praktis saya sendiri:

  • Gunakan anggota referensi ketika Anda ingin kehidupan objek Anda bergantung pada kehidupan objek lain : ini adalah cara eksplisit untuk mengatakan bahwa Anda tidak mengizinkan objek menjadi hidup tanpa instance valid kelas lain - karena tidak ada tugas dan kewajiban untuk mendapatkan inisialisasi referensi melalui konstruktor. Ini adalah cara yang baik untuk mendesain kelas Anda tanpa berasumsi apa pun tentang itu misalnya menjadi anggota atau bukan dari kelas lain. Anda hanya berasumsi bahwa hidup mereka terkait langsung dengan kejadian lain. Ini memungkinkan Anda untuk mengubah nanti bagaimana Anda menggunakan instance kelas Anda (dengan new, sebagai instance lokal, sebagai anggota kelas, yang dihasilkan oleh kumpulan memori di manajer, dll.)
  • Gunakan pointer dalam kasus lain : Ketika Anda ingin anggota diubah nanti, gunakan pointer atau pointer const untuk memastikan hanya membaca instance menunjuk. Jika jenis itu seharusnya dapat disalin, Anda tetap tidak dapat menggunakan referensi. Terkadang Anda juga perlu menginisialisasi anggota setelah panggilan fungsi khusus (init () misalnya) dan kemudian Anda tidak punya pilihan selain menggunakan pointer. TAPI: gunakan pernyataan di semua fungsi anggota Anda untuk dengan cepat mendeteksi status penunjuk yang salah!
  • Dalam kasus di mana Anda ingin umur objek bergantung pada umur objek eksternal, dan Anda juga perlu tipe itu dapat disalin, maka gunakan anggota penunjuk tetapi argumen referensi dalam konstruktor. Dengan cara itu Anda menunjukkan pada konstruksi bahwa umur objek ini bergantung tentang masa pakai argumen TETAPI penerapannya menggunakan petunjuk agar tetap dapat disalin. Selama anggota ini hanya diubah oleh salinan, dan jenis Anda tidak memiliki konstruktor default, jenis tersebut harus memenuhi kedua sasaran.

1
Saya pikir milik Anda dan Mykola adalah jawaban yang benar dan mempromosikan pemrograman tanpa pointer (baca lebih sedikit pointer).
amit

37

Objek jarang harus memungkinkan menetapkan dan hal-hal lain seperti perbandingan. Jika Anda mempertimbangkan beberapa model bisnis dengan objek seperti 'Departemen', 'Karyawan', 'Direktur', sulit membayangkan sebuah kasus ketika satu karyawan akan ditugaskan ke yang lain.

Jadi untuk objek bisnis, sangat baik untuk mendeskripsikan hubungan satu-ke-satu dan satu-ke-banyak sebagai referensi dan bukan penunjuk.

Dan mungkin tidak masalah untuk mendeskripsikan hubungan satu atau nol sebagai penunjuk.

Jadi tidak ada 'kami tidak dapat menetapkan' maka faktor.
Banyak programmer yang terbiasa dengan pointer dan itulah mengapa mereka akan menemukan argumen untuk menghindari penggunaan referensi.

Memiliki pointer sebagai anggota akan memaksa Anda atau anggota tim Anda untuk memeriksa pointer berulang kali sebelum digunakan, dengan komentar "berjaga-jaga". Jika sebuah pointer bisa menjadi nol maka pointer mungkin digunakan sebagai jenis bendera, yang buruk, karena setiap objek harus memainkan perannya sendiri.


2
+1: Sama yang saya alami. Hanya beberapa kelas penyimpanan data generik yang memerlukan assignemtn dan copy-c'tor. Dan untuk kerangka kerja objek bisnis tingkat tinggi harus ada teknik lain untuk menyalin yang juga menangani hal-hal seperti "jangan salin bidang unik" dan "tambahkan ke induk lain" dll. Jadi, Anda menggunakan kerangka kerja tingkat tinggi untuk menyalin objek bisnis Anda dan melarang tugas tingkat rendah.
mmmmmmmm

2
+1: Beberapa poin kesepakatan: mereferensikan anggota yang mencegah penetapan operator penugasan bukanlah argumen penting. Saat Anda menyatakan dengan benar dengan cara yang berbeda, tidak semua objek memiliki semantik nilai. Saya juga setuju bahwa kami tidak ingin 'jika (p)' tersebar di seluruh kode tanpa alasan logis. Tetapi pendekatan yang benar untuk itu adalah melalui invarian kelas: kelas yang terdefinisi dengan baik tidak akan menyisakan ruang untuk keraguan tentang apakah anggota dapat bernilai nol. Jika ada pointer yang bisa null dalam kode, saya berharap itu akan dikomentari dengan baik.
James Hopkin

@JamesHopkin Saya setuju bahwa kelas yang dirancang dengan baik dapat menggunakan petunjuk secara efektif. Selain melindungi terhadap NULL, referensi juga menjamin bahwa referensi Anda telah diinisialisasi (pointer tidak perlu diinisialisasi, jadi bisa menunjuk ke hal lain). Referensi juga memberi tahu Anda tentang kepemilikan - yaitu objek tidak memiliki anggota. Jika Anda secara konsisten menggunakan referensi untuk anggota agregat, Anda akan memiliki tingkat keyakinan yang sangat tinggi bahwa anggota penunjuk adalah objek gabungan (meskipun tidak banyak kasus di mana anggota gabungan diwakili oleh penunjuk).
weberc2


6

Dalam beberapa kasus penting, penugasan sama sekali tidak diperlukan. Ini seringkali merupakan pembungkus algoritme ringan yang memfasilitasi kalkulasi tanpa meninggalkan ruang lingkup. Objek tersebut adalah kandidat utama untuk anggota referensi karena Anda dapat yakin bahwa objek tersebut selalu memiliki referensi yang valid dan tidak perlu disalin.

Dalam kasus seperti itu, pastikan untuk membuat operator penugasan (dan seringkali juga konstruktor salinan) tidak dapat digunakan (dengan mewarisi dari boost::noncopyableatau mendeklarasikannya sebagai pribadi).

Namun, karena poin pengguna telah dikomentari, hal yang sama tidak berlaku untuk sebagian besar objek lainnya. Di sini, menggunakan anggota referensi bisa menjadi masalah besar dan umumnya harus dihindari.


5

Karena semua orang tampaknya membagikan aturan umum, saya akan menawarkan dua:

  • Jangan sekali-kali menggunakan rujukan sebagai anggota kelas. Saya tidak pernah melakukannya dalam kode saya sendiri (kecuali untuk membuktikan kepada diri saya sendiri bahwa saya benar dalam aturan ini) dan tidak dapat membayangkan kasus di mana saya akan melakukannya. Semantiknya terlalu membingungkan, dan itu sebenarnya bukan untuk apa referensi dirancang.

  • Selalu, selalu, gunakan referensi saat meneruskan parameter ke fungsi, kecuali untuk tipe dasar, atau saat algoritme memerlukan salinan.

Aturan-aturan ini sederhana, dan mendukung saya. Saya meninggalkan membuat aturan tentang penggunaan pointer pintar (tapi tolong, bukan auto_ptr) sebagai anggota kelas kepada orang lain.


2
Tidak terlalu setuju dengan yang pertama, tetapi ketidaksepakatan bukanlah masalah untuk menghargai alasannya. +1
David RodrĂ­guez - dribeas

(Kami tampaknya kehilangan argumen ini dengan pemilih yang baik dari SO, meskipun)
James Hopkin

2
Saya menolak karena "Jangan pernah gunakan referensi sebagai anggota kelas" dan alasan berikut tidak tepat bagi saya. Seperti yang dijelaskan dalam jawaban saya (dan mengomentari jawaban teratas) dan dari pengalaman saya sendiri, ada beberapa kasus di mana itu hanya hal yang baik. Saya berpikir seperti Anda sampai saya dipekerjakan di sebuah perusahaan di mana mereka menggunakannya dengan cara yang baik dan itu menjelaskan kepada saya bahwa ada kasus-kasus yang dapat membantu. Sebenarnya menurut saya masalah sebenarnya lebih pada sintaks dan implikasi tersembunyi yang membuat penggunaan referensi sebagai anggota membutuhkan programmer untuk memahami apa yang dia lakukan.
Klaim

1
Neil> Saya setuju tapi bukan "opini" yang membuat saya menolak, itu adalah pernyataan "tidak pernah". Karena berdebat di sini sepertinya tidak membantu, saya akan membatalkan pilihan saya.
Klaim

2
Jawaban ini dapat ditingkatkan dengan menjelaskan bagaimana semantik anggota referensi membingungkan. Alasan ini penting karena, secara sepintas, pointer tampak lebih ambigu secara semantik (mungkin tidak diinisialisasi, dan tidak memberi tahu Anda apa pun tentang kepemilikan objek yang mendasarinya).
weberc2

4

Ya untuk: Apakah benar mengatakan bahwa objek yang berisi referensi tidak boleh dialihkan, karena referensi tidak boleh diubah setelah diinisialisasi?

Aturan praktis saya untuk anggota data:

  • jangan pernah menggunakan referensi, karena itu mencegah penugasan
  • jika kelas Anda bertanggung jawab untuk menghapus, gunakan boost's scoped_ptr (yang lebih aman daripada auto_ptr)
  • jika tidak, gunakan pointer atau pointer const

2
Saya bahkan tidak dapat menyebutkan kasus ketika saya memerlukan penugasan objek bisnis.
Mykola Golubyev

1
+1: Saya hanya akan menambahkan untuk berhati-hati dengan anggota auto_ptr - setiap kelas yang memiliki mereka harus memiliki tugas dan menyalin konstruksi yang didefinisikan secara eksplisit (yang dihasilkan sangat tidak mungkin menjadi apa yang diperlukan)
James Hopkin

auto_ptr mencegah penugasan (masuk akal) juga.

2
@Mykola: Saya juga membuat pengalaman bahwa 98% objek bisnis saya tidak memerlukan tugas atau salinan. Saya mencegah ini dengan makro kecil yang saya tambahkan ke tajuk kelas-kelas yang membuat salinan c'tors dan operator = () pribadi tanpa implementasi. Jadi di 98% objek saya juga menggunakan referensi dan itu aman.
mmmmmmmm

1
Referensi sebagai anggota dapat digunakan untuk secara eksplisit menyatakan bahwa kehidupan instance kelas secara langsung bergantung pada kehidupan instance kelas lain (atau yang sama). "Jangan pernah menggunakan referensi, karena itu mencegah tugas" adalah asumsi bahwa semua kelas harus mengizinkan tugas. Ini seperti mengasumsikan bahwa semua kelas harus mengizinkan operasi penyalinan ...
Klaim

2

Saya biasanya hanya akan menggunakan pointer dalam data anggota ketika pointer bisa menjadi null atau bisa berubah. Apakah ada alasan lain untuk lebih memilih petunjuk daripada referensi untuk anggota data?

Iya. Keterbacaan kode Anda. Sebuah pointer membuatnya lebih jelas bahwa anggota adalah referensi (ironisnya :)), dan bukan objek yang terkandung, karena ketika Anda menggunakannya, Anda harus membatalkan referensi itu. Saya tahu beberapa orang berpikir itu kuno, tetapi saya masih berpikir bahwa itu hanya mencegah kebingungan dan kesalahan.


1
Lakos juga memperdebatkan hal ini dalam "Desain Perangkat Lunak C ++ Skala Besar".
Johan Kotlinski

Saya yakin Code Complete juga mendukung hal ini dan saya tidak menentangnya. Ini tidak terlalu menarik ...
markh44

2

Saya tidak menyarankan anggota data referensi karena Anda tidak pernah tahu siapa yang akan berasal dari kelas Anda dan apa yang mungkin ingin mereka lakukan. Mereka mungkin tidak ingin menggunakan objek yang direferensikan, tetapi menjadi referensi Anda telah memaksa mereka untuk memberikan objek yang valid. Saya telah melakukan ini sendiri cukup untuk berhenti menggunakan anggota data referensi.


0

Jika saya memahami pertanyaan dengan benar ...

Referensi sebagai parameter fungsi alih-alih pointer: Seperti yang Anda tunjukkan, pointer tidak menjelaskan siapa yang memiliki pembersihan / inisialisasi pointer. Lebih suka titik bersama ketika Anda menginginkan sebuah pointer, itu adalah bagian dari C ++ 11, dan weak_ptr ketika validitas data tidak dijamin selama masa kelas menerima menunjuk ke data .; juga merupakan bagian dari C ++ 11. Menggunakan referensi sebagai parameter fungsi menjamin bahwa referensi tersebut tidak null. Anda harus menumbangkan fitur bahasa untuk menyiasati ini, dan kami tidak peduli dengan pembuat kode meriam lepas.

Referensi variabel anggota: Seperti di atas dalam hal validitas data. Ini menjamin bahwa data yang diarahkan ke data, kemudian direferensikan, valid.

Memindahkan tanggung jawab validitas variabel ke poin sebelumnya dalam kode tidak hanya membersihkan kode selanjutnya (kelas A dalam contoh Anda), tetapi juga menjelaskannya kepada orang yang menggunakan.

Dalam contoh Anda, yang agak membingungkan (saya benar-benar akan mencoba menemukan implementasi yang lebih jelas), A yang digunakan oleh B dijamin seumur hidup B, karena B adalah anggota A, jadi referensi memperkuat ini dan (mungkin) lebih jelas.

Dan untuk berjaga-jaga (yang kemungkinannya rendah karena tidak masuk akal dalam konteks kode Anda), alternatif lain, menggunakan parameter A non referensi, non penunjuk, akan menyalin A, dan membuat paradigma tidak berguna - I benar-benar tidak berpikir yang Anda maksud ini sebagai alternatif.

Selain itu, ini juga menjamin Anda tidak dapat mengubah data yang direferensikan, di mana penunjuk dapat dimodifikasi. Sebuah pointer const akan bekerja, hanya jika direferensikan / pointer ke data tidak bisa berubah.

Pointer berguna jika parameter A untuk B tidak dijamin akan disetel, atau dapat ditetapkan ulang. Dan terkadang penunjuk lemah terlalu bertele-tele untuk implementasi, dan banyak orang yang tidak tahu apa itu weak_ptr, atau hanya tidak menyukai mereka.

Sobek jawaban ini, tolong :)

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.