Stroustrup berkata, "Jangan segera menciptakan basis yang unik untuk semua kelas Anda (kelas Object). Biasanya, Anda dapat melakukan yang lebih baik tanpa itu untuk banyak / sebagian besar kelas." (The C ++ Bahasa Pemrograman Edisi Keempat, Bagian 1.3.4)
Mengapa kelas-dasar-untuk-segalanya umumnya merupakan ide yang buruk, dan kapan masuk akal untuk membuatnya?
c++
object-oriented
object-oriented-design
Matthew James Briggs
sumber
sumber
Jawaban:
Karena apa yang akan dimiliki objek itu untuk fungsionalitas? Di java semua kelas Base miliki adalah toString, kode hash & kesetaraan dan variabel kondisi + monitor.
ToString hanya berguna untuk debugging.
kode hash hanya berguna jika Anda ingin menyimpannya dalam koleksi berbasis hash (preferensi dalam C ++ adalah untuk melewatkan fungsi hashing ke wadah sebagai param template atau untuk menghindari
std::unordered_*
sama sekali dan sebagai gantinya menggunakanstd::vector
dan polos daftar unordered).kesetaraan tanpa objek dasar dapat dibantu pada waktu kompilasi, jika mereka tidak memiliki tipe yang sama maka mereka tidak dapat sama. Dalam C ++ ini adalah kesalahan waktu kompilasi.
variabel monitor dan kondisi lebih baik dimasukkan secara eksplisit dalam kasus per kasus.
Namun ketika ada lebih banyak yang perlu dilakukan maka ada kasus penggunaan.
Sebagai contoh di QT ada
QObject
kelas root yang membentuk dasar afinitas thread, hierarki kepemilikan orangtua-anak dan mekanisme slot sinyal. Ini juga memaksa digunakan oleh pointer untuk QObjects, Namun banyak Kelas di Qt tidak mewarisi QObject karena mereka tidak memerlukan slot sinyal (terutama tipe nilai dari beberapa deskripsi).sumber
Object
._hashCode
tidak 'menggunakan wadah yang berbeda' melainkan menunjuk keluar bahwa C ++std::unordered_map
tidak hashing menggunakan argumen templat, alih-alih membutuhkan kelas elemen itu sendiri untuk menyediakan implementasi. Yaitu, seperti semua wadah bagus lainnya & manajer sumber daya di C ++, ini tidak mengganggu; itu tidak mencemari semua objek dengan fungsi atau data kalau-kalau seseorang mungkin membutuhkannya dalam beberapa konteks nanti.Karena tidak ada fungsi yang dibagikan oleh semua objek. Tidak ada yang bisa dimasukkan ke dalam antarmuka ini yang masuk akal untuk semua kelas.
sumber
Setiap kali Anda membangun hierarki warisan benda yang tinggi, Anda cenderung mengalami masalah dengan Fragile Base Class (Wikipedia.) .
Memiliki banyak hierarki warisan kecil yang terpisah (berbeda, terisolasi) mengurangi peluang untuk mengalami masalah ini.
Menjadikan semua objek Anda bagian dari hierarki warisan tunggal praktis menjamin bahwa Anda akan mengalami masalah ini.
sumber
cout.print(x).print(0.5).print("Bye\n")
- tidak bergantungoperator<<
.Karena:
Menerapkan segala macam
virtual
fungsi memperkenalkan tabel virtual, yang membutuhkan overhead ruang per-objek yang tidak diperlukan atau diinginkan dalam banyak (kebanyakan?) Situasi.Menerapkan secara
toString
nonvirtual akan sangat tidak berguna, karena satu-satunya hal yang dapat dikembalikan adalah alamat objek, yang sangat tidak ramah pengguna, dan yang sudah diakses oleh penelepon, tidak seperti di Jawa.Demikian pula, nonvirtual
equals
atauhashCode
hanya bisa menggunakan alamat untuk membandingkan objek, yang lagi-lagi sangat tidak berguna dan sering bahkan benar-benar salah - tidak seperti di Jawa, objek sering disalin dalam C ++, dan karenanya membedakan "identitas" suatu objek bahkan tidak selalu bermakna atau bermanfaat. (mis. seorang yangint
benar - benar tidak boleh memiliki identitas selain nilainya ... dua bilangan bulat dengan nilai yang sama harus sama.)sumber
open
kata kunci baru . Saya tidak berpikir itu melampaui beberapa kertas sekalipun.shared_ptr<Foo>
untuk melihat apakah itu jugashared_ptr<Bar>
(atau juga dengan jenis pointer lainnya), bahkan jikaFoo
danBar
merupakan kelas yang tidak terkait yang tidak saling mengenal satu sama lain. Membutuhkan hal seperti itu bekerja dengan "pointer mentah", mengingat sejarah bagaimana hal-hal seperti itu digunakan, akan mahal, tetapi untuk hal-hal yang akan disimpan tumpukan pula, biaya tambahan akan menjadi minimal.Memiliki satu objek root membatasi apa yang dapat Anda lakukan dan apa yang dapat dilakukan oleh kompiler, tanpa banyak hasil.
Kelas root yang sama memungkinkan untuk membuat container-of-anything dan mengekstraksi apa yang mereka miliki dengan
dynamic_cast
, tetapi jika Anda membutuhkan container-of-apa pun maka sesuatu yang miripboost::any
dapat dilakukan tanpa kelas root yang sama. Danboost::any
juga mendukung primitif - bahkan dapat mendukung optimasi buffer kecil dan membuat mereka hampir "unboxed" dalam bahasa Java.C ++ mendukung dan berkembang pada tipe nilai. Baik literal, dan programmer tipe nilai tertulis. Wadah C ++ secara efisien menyimpan, mengurutkan, hash, mengkonsumsi, dan menghasilkan jenis nilai.
Pewarisan, khususnya jenis warisan kelas dasar gaya Java yang menyiratkan monolitik, memerlukan jenis "penunjuk" atau "referensi" berbasis toko bebas. Pegangan / penunjuk / referensi Anda ke data memiliki penunjuk ke antarmuka kelas, dan secara polimorfis dapat mewakili hal lain.
Meskipun ini berguna dalam beberapa situasi, setelah Anda menikah dengan pola dengan "kelas dasar umum", Anda telah mengunci seluruh basis kode Anda ke dalam biaya dan bagasi dari pola ini, bahkan ketika itu tidak berguna.
Hampir selalu Anda tahu lebih banyak tentang jenis daripada "itu adalah objek" di salah satu situs panggilan, atau dalam kode yang menggunakannya.
Jika fungsinya sederhana, menulis fungsi sebagai templat memberi Anda polimorfisme berbasis waktu kompilasi tipe bebek di mana informasi di situs pemanggil tidak dibuang. Jika fungsi ini lebih kompleks, penghapusan tipe dapat dilakukan dimana operasi seragam pada jenis yang ingin Anda lakukan (katakanlah, serialisasi dan deserialisasi) dapat dibangun dan disimpan (pada waktu kompilasi) untuk dikonsumsi (pada saat run time) oleh kode dalam unit terjemahan yang berbeda.
Misalkan Anda memiliki beberapa perpustakaan di mana Anda ingin semuanya serializable. Salah satu pendekatan adalah memiliki kelas dasar:
Sekarang setiap bit kode yang Anda tulis bisa
serialization_friendly
.Kecuali bukan
std::vector
, jadi sekarang Anda perlu menulis setiap wadah. Dan bukan bilangan bulat yang Anda dapatkan dari perpustakaan bignum itu. Dan bukan tipe yang Anda tulis yang menurut Anda tidak perlu serialisasi. Dan bukan atuple
, atau aint
atau adouble
, atau astd::ptrdiff_t
.Kami mengambil pendekatan lain:
yang terdiri dari, yah, tidak melakukan apa-apa, tampaknya. Kecuali sekarang kita dapat memperluas
write_to
dengan mengesampingkanwrite_to
sebagai fungsi bebas di namespace dari suatu tipe atau metode dalam tipe tersebut.Kami bahkan dapat menulis sedikit kode tipe penghapusan:
dan sekarang kita dapat mengambil tipe yang sewenang-wenang dan memasukkannya secara otomatis ke sebuah
can_serialize
antarmuka yang memungkinkan Anda memohonserialize
pada titik selanjutnya melalui antarmuka virtual.Begitu:
adalah fungsi yang mengambil apa pun yang bisa bersambung, bukan
dan yang pertama, tidak seperti kedua, dapat menangani
int
,std::vector<std::vector<Bob>>
secara otomatis.Tidak perlu banyak untuk menulisnya, terutama karena hal semacam ini adalah sesuatu yang jarang ingin Anda lakukan, tetapi kami memperoleh kemampuan untuk memperlakukan sesuatu sebagai serializable tanpa memerlukan jenis dasar.
Terlebih lagi, kita sekarang dapat menjadikan
std::vector<T>
serializable sebagai warga negara kelas satu hanya dengan mengesampingkanwrite_to( my_buffer*, std::vector<T> const& )
- dengan kelebihan itu, itu dapat diteruskan ke acan_serialize
dan serializabilty daristd::vector
disimpan dalam tabel dan diakses oleh.write_to
.Singkatnya, C ++ cukup kuat sehingga Anda dapat menerapkan keuntungan dari satu kelas dasar on-the-fly saat diperlukan, tanpa harus membayar harga hierarki warisan paksa saat tidak diperlukan. Dan saat-saat ketika basis tunggal (palsu atau tidak) diperlukan cukup langka.
Ketika tipe sebenarnya identitas mereka, dan Anda tahu apa itu, peluang optimasi berlimpah. Data disimpan secara lokal dan berdekatan (yang sangat penting untuk keramahan cache pada prosesor modern), kompiler dapat dengan mudah memahami apa operasi yang diberikan (alih-alih memiliki penunjuk metode virtual buram yang harus dilompati, untuk mengarah ke kode yang tidak diketahui pada prosesor sisi lain) yang memungkinkan instruksi disusun kembali secara optimal, dan pasak bundar yang lebih sedikit dipalu menjadi lubang bundar.
sumber
Ada banyak jawaban bagus di atas, dan fakta jelas bahwa apa pun yang akan Anda lakukan dengan kelas-dasar-semua-objek dapat dilakukan dengan lebih baik dengan cara lain seperti yang ditunjukkan oleh jawaban @ ratchetfreak dan komentar tentang itu sangat penting, tetapi ada alasan lain, yaitu untuk menghindari membuat berlian warisanketika pewarisan berganda digunakan. Jika Anda memiliki fungsionalitas dalam kelas dasar universal, segera setelah Anda mulai menggunakan beberapa pewarisan Anda harus mulai menentukan varian mana yang ingin Anda akses, karena itu bisa kelebihan beban berbeda di jalur berbeda dari rantai pewarisan. Dan basisnya tidak bisa virtual, karena ini akan sangat tidak efisien (membutuhkan semua objek untuk memiliki tabel virtual dengan potensi biaya yang sangat besar dalam penggunaan memori dan lokalitas). Ini akan menjadi mimpi buruk logistik dengan sangat cepat.
sumber
Bahkan Microsoft awal C ++ kompiler dan perpustakaan (saya tahu tentang Visual C ++, 16 bit) memiliki kelas seperti itu
CObject
.Namun Anda harus tahu bahwa pada saat itu "templat" tidak didukung oleh kompiler C ++ sederhana ini sehingga kelas seperti
std::vector<class T>
tidak dimungkinkan. Sebaliknya implementasi "vektor" hanya bisa menangani satu jenis kelas sehingga ada kelas yang sebanding denganstd::vector<CObject>
hari ini. KarenaCObject
itu adalah kelas dasar dari hampir semua kelas (sayangnya tidakCString
- setarastring
dengan kompiler modern) Anda dapat menggunakan kelas ini untuk menyimpan hampir semua jenis objek.Karena kompiler modern mendukung templat, kasus penggunaan "kelas dasar generik" ini tidak lagi diberikan.
Anda harus memikirkan fakta bahwa menggunakan kelas dasar generik seperti itu akan memakan biaya (sedikit) memori dan runtime - misalnya dalam panggilan ke konstruktor. Jadi ada kekurangan saat menggunakan kelas seperti itu tetapi setidaknya ketika menggunakan kompiler C ++ modern hampir tidak ada kasus penggunaan untuk kelas seperti itu.
sumber
TObject
sebelum MFC ada. Jangan salahkan Microsoft untuk bagian desain itu, sepertinya ide bagus untuk hampir semua orang di masa itu.Saya akan menyarankan alasan lain yang berasal dari Jawa.
Karena Anda tidak dapat membuat kelas dasar untuk semuanya setidaknya tidak tanpa setumpuk boiler-plate.
Anda mungkin bisa lolos dengan itu untuk kelas Anda sendiri - tetapi Anda mungkin akan menemukan bahwa Anda akhirnya menduplikasi banyak kode. Misalnya "Saya tidak bisa menggunakan di
std::vector
sini karena tidak menerapkanIObject
- Saya lebih baik membuat turunan baruIVectorObject
yang melakukan hal yang benar ...".Ini akan menjadi kasus setiap kali Anda berurusan dengan built-in atau kelas perpustakaan standar atau kelas dari perpustakaan lain.
Sekarang jika itu dibangun ke bahasa Anda akan berakhir dengan hal-hal seperti
Integer
danint
kebingungan yang ada di java, atau perubahan besar ke sintaks bahasa. (Ingat, saya pikir beberapa bahasa lain telah melakukan pekerjaan yang bagus dengan membangunnya ke dalam setiap jenis - ruby sepertinya contoh yang lebih baik.)Juga perhatikan bahwa jika kelas dasar Anda tidak run-time polymorphic (yaitu menggunakan fungsi virtual), Anda bisa mendapatkan manfaat yang sama dari menggunakan kerangka kerja seperti sifat.
mis. alih-alih
.toString()
Anda dapat memiliki yang berikut: (CATATAN: Saya tahu Anda bisa melakukan ini lebih rapi menggunakan perpustakaan yang ada dll, itu hanya contoh ilustrasi.)sumber
Boleh dibilang "batal" memenuhi banyak peran kelas dasar universal. Anda dapat menggunakan pointer apa saja ke a
void*
. Anda kemudian dapat membandingkan pointer tersebut. Anda dapatstatic_cast
kembali ke kelas asli.Namun apa yang tidak dapat Anda lakukan dengan
void
yang dapat Anda lakukanObject
adalah menggunakan RTTI untuk mencari tahu jenis objek yang Anda miliki. Ini pada akhirnya ke bagaimana tidak semua objek di C ++ memiliki RTTI, dan memang mungkin untuk memiliki objek dengan lebar nol.sumber
[[no_unique_address]]
, yang dapat digunakan oleh kompiler untuk memberikan lebar nol kepada sub-objek anggota.[[no_unique_address]]
akan memungkinkan kompiler untuk variabel anggota EBO.Java mengambil filosofi desain bahwa Perilaku Tidak Terdefinisi seharusnya tidak ada . Kode seperti:
akan menguji apakah
felix
memiliki subtipeCat
antarmuka yang mengimplementasikanWoofer
; jika ya, ia akan melakukan pemeran dan memanggilwoof()
dan jika tidak, itu akan melempar pengecualian. Perilaku kode sepenuhnya ditentukan apakahfelix
menerapkanWoofer
atau tidak .C ++ mengambil filosofi bahwa jika suatu program tidak boleh mencoba beberapa operasi, tidak masalah apa pun kode yang dihasilkan akan dilakukan jika operasi itu dicoba, dan komputer tidak boleh membuang waktu mencoba membatasi perilaku dalam kasus yang "harus" tidak pernah muncul. Dalam C ++, menambahkan operator tipuan yang tepat sehingga untuk melemparkan
*Cat
ke*Woofer
, kode akan menghasilkan perilaku yang ditentukan ketika para pemain sah, tetapi Perilaku tidak terdefinisi saat tidak .Memiliki tipe pangkalan yang sama untuk beberapa hal memungkinkan untuk memvalidasi gips di antara turunan dari tipe dasar itu, dan juga untuk melakukan operasi uji coba, tetapi memvalidasi gips lebih mahal daripada sekadar mengasumsikan bahwa mereka sah dan berharap tidak ada hal buruk yang terjadi. Filosofi C ++ adalah bahwa validasi seperti itu membutuhkan "pembayaran untuk sesuatu yang Anda [biasanya] tidak perlu".
Masalah lain yang berhubungan dengan C ++, tetapi tidak akan menjadi masalah untuk bahasa baru, adalah bahwa jika beberapa programmer masing-masing membuat basis bersama, menurunkan kelas mereka sendiri dari itu, dan menulis kode untuk bekerja dengan hal-hal dari kelas dasar yang sama, kode seperti itu tidak akan dapat bekerja dengan objek yang dikembangkan oleh programmer yang menggunakan kelas dasar yang berbeda. Jika bahasa baru mengharuskan semua objek tumpukan memiliki format tajuk bersama, dan tidak pernah mengizinkan objek tumpukan yang tidak, maka metode yang memerlukan referensi ke objek tumpukan dengan header seperti itu akan menerima referensi ke objek tumpukan apa pun siapa pun pernah bisa membuat.
Secara pribadi, saya berpikir bahwa memiliki sarana umum untuk menanyakan objek "apakah Anda dapat dikonversi ke tipe X" adalah fitur yang sangat penting dalam bahasa / kerangka kerja, tetapi jika fitur tersebut tidak dibangun ke dalam bahasa sejak awal, sulit untuk tambahkan nanti. Secara pribadi, saya pikir kelas dasar seperti itu harus ditambahkan ke perpustakaan standar pada kesempatan pertama, dengan rekomendasi yang kuat bahwa semua objek yang akan digunakan secara polimorfik harus diwarisi dari basis itu. Memiliki pemrogram masing-masing mengimplementasikan "tipe dasar" mereka sendiri akan membuat objek yang lewat di antara kode orang yang berbeda lebih sulit, tetapi memiliki tipe dasar yang sama yang diwarisi oleh banyak programmer akan membuatnya lebih mudah.
TAMBAHAN
Dengan menggunakan templat, dimungkinkan untuk mendefinisikan "pemegang objek arbitrer" dan menanyakannya tentang jenis objek yang terkandung di dalamnya; paket Boost berisi hal yang disebut
any
. Jadi, meskipun C ++ tidak memiliki tipe standar "type-checkable reference to anything", dimungkinkan untuk membuatnya. Ini tidak memecahkan masalah yang disebutkan dengan tidak memiliki sesuatu dalam standar bahasa, yaitu ketidakcocokan antara implementasi programer yang berbeda, tetapi itu menjelaskan bagaimana C ++ bertahan tanpa memiliki tipe dasar dari mana semuanya diturunkan: dengan memungkinkan untuk membuat sesuatu yang bertindak seperti satu.sumber
Woofer
merupakan antarmuka danCat
dapat diwarisi, para pemain akan sah karena mungkin ada (jika tidak sekarang, mungkin di masa depan)WoofingCat
yang mewarisi dariCat
dan mengimplementasikanWoofer
. Perhatikan bahwa di bawah model kompilasi / penautan Java, pembuatan aWoofingCat
tidak akan memerlukan akses ke kode sumber untukCat
atauWoofer
.Cat
ke aWoofer
dan akan menjawab pertanyaan "apakah Anda convertible ke tipe X". C ++ akan memungkinkan Anda untuk memaksa para pemeran, karena hei, mungkin Anda benar-benar tahu apa yang Anda lakukan, tetapi itu juga akan membantu Anda jika bukan itu yang ingin Anda lakukan.dynamic_cast
perilaku yang telah ditentukan jika menunjuk ke objek polimorfik, dan Perilaku Tidak Terdefinisi jika tidak, jadi dari perspektif semantik ...Symbian C ++ sebenarnya memiliki kelas dasar universal, CBase, untuk semua objek yang berperilaku dengan cara tertentu (terutama jika mereka mengalokasikan tumpukan). Ini memberikan destruktor virtual, memusatkan memori kelas pada konstruksi, dan menyembunyikan copy constructor.
Alasan di balik itu adalah bahasa untuk sistem embedded dan kompiler dan spesifikasi C ++ benar-benar sangat buruk 10 tahun yang lalu.
Tidak semua kelas diwarisi dari ini, hanya beberapa.
sumber