Apakah duplikasi kode merupakan kejahatan yang diperlukan dalam C?

16

Saya agak baru untuk C, dan saya bertanya-tanya apakah duplikasi kode adalah kejahatan yang diperlukan ketika datang untuk menulis struktur data umum dan C secara umum?

Saya bisa mencoba menulis implementasi generik hash mapuntuk contoh, tapi saya selalu menemukan hasil akhirnya menjadi berantakan. Saya juga bisa menulis implementasi khusus hanya untuk kasus penggunaan khusus ini, menjaga kode tetap jelas dan mudah dibaca dan debug. Yang terakhir tentu saja akan menyebabkan duplikasi kode.

Apakah implementasi generik merupakan norma, atau apakah Anda menulis implementasi yang berbeda untuk setiap kasus penggunaan?

Gerstmann
sumber
11
Pertentangan moral yang Anda gambarkan tidak unik untuk C. Anekdot, saya menemukan bahwa bilah untuk membuat implementasi generik cukup tinggi, kecuali Anda memiliki audiensi untuk itu. Jumlah upaya yang diperlukan untuk membuat implementasi generik yang baik jauh melebihi solusi-titik, IME.
Robert Harvey
1
@ kevincline: Yah, Java memang memiliki obat generik. Memang, ini adalah jenis obat generik "jangan perhatikan pria di balik tirai".
Robert Harvey
1
Dan sebelum obat generik, Anda bisa lulus di Object dan lulus di Float, Double, Integer dan Long, masing-masing.
Daniel Kaplan
3
Deja vu adalah pemeriksaan kode bau dan preferensi untuk KERING adalah akar dari semua praktik terbaik yang layak diperhatikan IMO. Tetapi saya telah berusaha keras dalam bahasa-bahasa yang berorientasi pada OOP, berusaha terlalu keras untuk menghindari duplikasi demi kepentingannya sendiri. Jika Anda tidak mengikat masalah yang tidak terkait satu sama lain secara sia-sia atau menghancurkan keterbacaan demi dua fungsi serupa menjadi satu, saya akan menggunakan naluri dalam sebagian besar bahasa apa pun.
Erik Reppen

Jawaban:

27

C membuatnya sulit untuk menulis kode generik. Tidak seperti C ++, yang memberi Anda templat dan fungsi virtual, C hanya memiliki 3 mekanisme untuk menulis kode generik:

  1. void* pointer
  2. Makro preprosesor
  3. Pointer fungsi

void* pointer jauh dari ideal, karena Anda kehilangan semua jenis keamanan yang disediakan oleh kompiler, yang dapat mengakibatkan perilaku undefined sulit-untuk-men-debug yang dihasilkan dari gips tipe tidak valid.

Makro preprocessor memiliki kelemahan yang telah dicatat dengan baik - ekspansi preprocessor pada dasarnya hanyalah mekanisme menemukan / mengganti yang terjadi sebelum fase kompilasi, yang lagi-lagi dapat mengakibatkan kesalahan sulit untuk debug. Contoh pola dasar adalah sesuatu seperti:, di #define add(x) (x+x)mana xdapat ditambahkan dua kali jika Anda menelepon add(i++). Anda dapat menulis kode umum gaya template seluruhnya menggunakan makro-C, tetapi hasilnya benar - benar mengerikan dan sulit dipertahankan .

Pointer fungsi menyediakan cara yang baik untuk menulis kode generik, tetapi sayangnya mereka tidak memberi Anda tipe umum - mereka hanya memberikan kemungkinan run-time polimorfisme (itulah sebabnya, misalnya, perpustakaan standar qsortmasih memerlukan fungsi yang membutuhkan void*pointer.)

Anda juga bisa mengimplementasikan hierarki kelas dalam C menggunakan struct, seperti yang dilakukan di perpustakaan GLib yang menyediakan GObjectkelas dasar umum . Tetapi ini mengalami masalah yang sama seperti menggunakan void*pointer, karena Anda masih perlu mengandalkan casting manual yang berpotensi tidak aman untuk up-cast dan down-cast.

Jadi ya, C membuatnya sulit untuk menulis kode yang generik DAN aman / mudah dirawat, yang sayangnya dapat mengakibatkan duplikasi kode. Proyek C besar sering menggunakan bahasa scripting untuk menghasilkan kode berulang selama proses pembuatan.

Charles Salvia
sumber
1
Seseorang juga dapat menghasilkan kode C berulang menggunakan bahasa templating eksternal / alat atau skrip shell atau menggunakan semut ...
Job
Apa URL Rusia yang dikaburkan oleh Google yang telah Anda tempatkan di jawaban Anda? Mengklik di dalamnya menyebabkan prompt "terbuka" muncul di browser saya. Apakah ini aman?
Robert Harvey
2
@RobertHarvey Pemindaian antivirus dan keamanan saya pikir itu baik-baik saja. Ini adalah file Header bahasa C, ekstensi .h. Tampaknya sah.
maple_shaft
@maple_shaft: Oke. Saya telah menghapus Googlization di sekitar tautan.
Robert Harvey
Ini hanya file .h (C-header) teks biasa
Charles Salvia
15

Saya tidak bisa berbicara untuk orang lain, tetapi dalam pengalaman pribadi saya dengan C, duplikasi kode belum banyak masalah. Apakah ini karena ukuran proyek, atau set sampel yang terpesona, saya tidak bisa mengatakannya. Namun, ada tiga aturan praktis yang saya ikuti yang menurut saya berlaku. Tanpa urutan tertentu, mereka ...

  1. Tulis untuk apa yang dibutuhkan. Generalitas dapat datang kemudian JIKA dibutuhkan.
  2. Jika diperlukan secara umum, void pointer, function pointer, dan ukuran struktur dapat sangat berharga. Qsort rutin (), misalnya, menggunakan ketiganya.
  3. Berikan bobot untuk membuat kode Anda jelas.
Sparky
sumber
5

Perlu dicatat bahwa Anda mungkin tidak memerlukan solusi sepenuhnya generik untuk mengurangi duplikasi kode. Kadang-kadang refactoring biasa dan sedikit generalisasi metode sudah cukup.

Saat menulis solusi umum untuk khalayak luas, Anda harus mempertimbangkan:

  1. Cara baru apa yang mungkin konsumen gunakan kode Anda, dan bagaimana Anda mengakomodasi mereka?
  2. Kesalahan apa yang harus saya tangkap? Kesalahan apa yang seharusnya tidak saya tangkap?
  3. Seberapa kuat kebutuhan API? Berapa banyak kelebihan yang harus saya sediakan?
  4. Apa langkah-langkah keamanan yang harus saya lakukan, agar kode tidak dapat digunakan untuk tujuan jahat?

Eric Lippert menulis seluruh artikel tentang keputusan desain tunggal dalam kode .NET framework. Secara keseluruhan, seringkali lebih mudah untuk hanya refactor.

Robert Harvey
sumber
3

Saya biasanya menggunakan implementasi generik, seperti glib , maka jika casting berakhir terlalu mengganggu, buat pembungkus tipe kecil khusus untuk itu. Namun, banyak casting yang diharapkan dalam C, seperti penggunaan void*tipe generik, jadi apa yang akan dianggap "berantakan" dalam bahasa lain hanya khas C. Cukup menakutkan, itu akan terlihat jauh lebih alami ketika Anda dapatkan lebih banyak pengalaman dalam bahasa tersebut.

Karl Bielefeldt
sumber
2
"Apa yang akan dianggap 'berantakan' dalam bahasa lain hanya khas C. Cukup menakutkan, itu akan terlihat jauh lebih alami ketika Anda mendapatkan lebih banyak pengalaman dalam bahasa itu." Ha ha ha! C selalu menjadi salah satu bahasa favorit saya, tapi itu masih cukup lucu!
GlenPeterson
0

Masalah Anda tentang penggunaan kembali tidak unik untuk C. Namun, beberapa implementasi HashMap yang sangat baik dan sangat dapat digunakan kembali ada di luar sana yang dapat Anda gunakan sebagai dasar. Satu yang sudah ada selama satu atau dua dekade adalah di Jawa: http://docs.oracle.com/javase/7/docs/api/java/util/HashMap.html

Semua koleksi Java, termasuk HashMap sedang ditulis ulang untuk Java 8 untuk menyingkirkan gagasan iterator, dan sebaliknya mengharuskan Anda untuk meneruskan fungsi ke koleksi sehingga dapat beralih secara internal. Ini adalah kemenangan besar untuk menulis kode konkuren - koleksi dapat mengelola konkurensi secara internal sehingga klien tidak perlu melakukannya. Yang mengatakan, sebagian besar programmer tidak terbiasa melewati pointer fungsi. Ini berfungsi dengan baik menjelaskan kemana arah HashMap: http://cr.openjdk.java.net/~briangoetz/lambda/collections-overview.html

Sintaksis Scala mungkin membuatnya sulit untuk menerjemahkan sesuatu seperti ini ke dalam C, tapi di sini adalah contoh dari kumpulan yang sudah bekerja dengan cara yang sama seperti koleksi Java 8 - tidak dapat diubah secara default, menyediakan berbagai metode tranformasi, menggunakan pointer fungsi dan lainnya. hal-hal keren seperti aplikasi parsial: http://www.scala-lang.org/api/current/index.html#scala.collection.Map

Jika Anda melakukan banyak hal dengan HashMaps dan struktur data lebih lanjut lainnya, Anda mungkin ingin melihat bahasa yang mendukungnya.

Kembali ke ide duplikasi kode secara umum, Anda dapat mengkodekan sangat cepat dengan memotong dan menempel, tetapi menghasilkan kode yang mengerikan. Saya akan mengatakan itu dapat diterima dalam kode dibuang seperti laporan satu kali. Terkadang OK juga ketika tidak ada kode down-stream-nya, seperti kode UI yang hanya membangun layar yang tidak bergantung pada yang lain, tetapi sebenarnya itu adalah area yang sangat abu-abu.

Saya tidak berpikir itu terlalu banyak penyederhanaan untuk mengatakan bahwa setiap kali Anda mendapati diri Anda menduplikasi kode, Anda harus membuat fungsi. Bug saja yang Anda perbaiki dalam satu salinan kode dan bukan yang lain cukup alasan untuk membuat suatu fungsi.

GlenPeterson
sumber
1
- "Saya tidak berpikir itu terlalu berlebihan untuk mengatakan bahwa setiap kali Anda mendapati diri Anda menduplikasi kode, Anda harus membuat fungsi" - Dalam C yang merupakan penyederhanaan berlebihan. Cobalah membuat Stack in C untuk integer, dobel, dan string tanpa menduplikasi fungsi push dan pop untuk setiap tipe data.
mike30
@ Mike Apakah itu tidak mungkin menggunakan pointer kosong?
sakisk
@ Mike - poin yang bagus. Tetapi titik baliknya adalah Anda bisa mengimplementasikan Stack untuk setiap tipe data sedemikian rupa sehingga Anda tidak perlu mengimplementasikan Stack lagi. Jadi tujuannya bukan numberOfImplementations = 1, tetapi lebih seperti 5. Anda mungkin dapat membuat intimplementasi implementasinya selama char... Sudah begitu lama sejak saya menggunakan C sehingga saya tidak ingat.
GlenPeterson
1
@faif. Ya, tetapi kebutuhan untuk menggunakan void pointer membuat poin untuk C. Casting pointer lebih lambat dan kurang elegan daripada alternatif yang ditemukan di C ++. Duplikat peringkat kode agak lebih tinggi pada daftar opsi yang memungkinkan ketika pengkodean dalam C.
mike30
@ mike30 Apakah ada bukti / referensi tentang: "Casting pointer di C lebih lambat daripada alternatif yang ditemukan di C ++?"
sakisk
0

Saya bertanya-tanya apakah duplikasi kode adalah kejahatan yang diperlukan ketika datang untuk menulis struktur data umum dan C secara umum?

Di C, mutlak bagi saya, sebagai seseorang yang memantul antara C dan C ++. Saya pasti menduplikasi lebih banyak hal-hal sepele setiap hari di C daripada di C ++, tetapi sengaja, dan saya tidak selalu melihatnya sebagai "jahat" karena setidaknya ada beberapa manfaat praktis - saya pikir itu adalah kesalahan untuk mempertimbangkan semua hal benar-benar "baik" atau "jahat" - hampir semuanya adalah masalah pertukaran. Memahami kompromi-kompromi itu dengan jelas adalah kunci untuk tidak menghindari keputusan yang disesalkan di belakang, dan hanya melabeli hal-hal sebagai "baik" atau "jahat" umumnya mengabaikan semua seluk-beluk seperti itu.

Sementara masalahnya tidak unik untuk C seperti yang ditunjukkan orang lain, itu mungkin jauh lebih diperburuk dalam C karena kurangnya sesuatu yang lebih elegan daripada macro atau batal pointer untuk obat generik, kecanggungan dari OOP non-sepele, dan fakta bahwa C perpustakaan standar tidak disertai dengan wadah apa pun. Di C ++, seseorang yang menerapkan daftar tertautnya sendiri mungkin mendapatkan gerombolan orang yang marah yang menuntut mengapa mereka tidak menggunakan perpustakaan standar, kecuali mereka siswa. Di C, Anda akan mengundang gerombolan yang marah jika Anda tidak dapat dengan percaya diri meluncurkan implementasi daftar tertaut yang elegan dalam tidur Anda karena seorang programmer C sering diharapkan setidaknya dapat melakukan hal-hal semacam itu setiap hari. Itu' Bukan karena obsesi aneh pada daftar yang ditautkan bahwa Linus Torvalds menggunakan implementasi pencarian dan penghapusan SLL menggunakan tipuan ganda sebagai kriteria untuk mengevaluasi seorang programmer yang mengerti bahasa dan memiliki "selera yang baik". Itu karena programmer C mungkin diminta untuk mengimplementasikan logika seperti itu seribu kali dalam karir mereka. Dalam hal ini untuk C, itu seperti koki mengevaluasi keterampilan juru masak baru dengan membuat mereka hanya menyiapkan beberapa telur untuk melihat apakah mereka setidaknya memiliki penguasaan hal-hal dasar yang harus mereka lakukan sepanjang waktu.

Misalnya, saya mungkin menerapkan struktur data "daftar bebas terindeks" dasar ini selusin kali lipat dalam C secara lokal untuk setiap situs yang menggunakan strategi alokasi ini (hampir semua struktur tertaut saya untuk menghindari mengalokasikan satu simpul pada satu waktu dan membagi dua memori. biaya tautan pada 64-bit):

masukkan deskripsi gambar di sini

Tetapi dalam C hanya dibutuhkan sejumlah kecil kode ke reallocarray yang dapat ditumbuhkan dan menyatukan beberapa memori darinya menggunakan pendekatan yang diindeks ke daftar bebas ketika menerapkan struktur data baru yang menggunakan yang ini.

Sekarang saya memiliki hal yang sama diimplementasikan dalam C ++ dan di sana saya hanya mengimplementasikannya sekali sebagai templat kelas. Tetapi ini adalah implementasi yang jauh lebih kompleks di sisi C ++ dengan ratusan baris kode dan beberapa dependensi eksternal yang juga menjangkau ratusan baris kode. Dan alasan utama itu jauh lebih rumit adalah karena saya harus mengkodekannya terhadap ide yang Tmungkin berupa tipe data apa pun. Itu bisa melempar pada waktu tertentu (kecuali ketika menghancurkannya, yang harus saya lakukan secara eksplisit seperti dengan wadah perpustakaan standar), saya harus memikirkan perataan yang tepat untuk mengalokasikan memori untukT (walaupun untungnya ini dibuat lebih mudah di C ++ 11 dan seterusnya), bisa jadi non-sepele dibangun / dirusak (membutuhkan penempatan dtor dtor manual dan baru), saya harus menambahkan metode yang tidak semuanya akan membutuhkan tetapi beberapa hal akan perlu, dan saya harus menambahkan iterator, iterator yang bisa berubah dan hanya-baca (const), dan seterusnya dan seterusnya.

Array yang Dapat Tumbuh Bukanlah Ilmu Roket

Di C ++ orang membuatnya terdengar seperti std::vectorkarya ilmuwan roket, dioptimalkan hingga mati, tetapi tidak berkinerja lebih baik daripada C array dinamis yang dikodekan terhadap tipe data tertentu yang hanya digunakan reallocuntuk meningkatkan kapasitas array pada push back dengan selusin baris kode. Perbedaannya adalah bahwa dibutuhkan implementasi yang sangat kompleks untuk membuat hanya urutan akses acak yang dapat ditumbuhkan yang sepenuhnya sesuai dengan standar, menghindari penggunaan ctors pada elemen yang tidak dimasukkan, kecuali aman, menyediakan iterator akses acak const dan non-const, gunakan tipe ciri-ciri untuk mendisplambasikan mengisi ctors dari range ctors untuk tipe integral tertentuT, berpotensi memperlakukan POD yang berbeda dengan menggunakan jenis sifat, dll. dll. Pada titik itu Anda memang membutuhkan implementasi yang sangat kompleks hanya untuk membuat array dinamis yang dapat ditumbuhkan, tetapi hanya karena ia berusaha menangani setiap kasus penggunaan yang mungkin dapat dibayangkan. Di sisi positifnya, Anda bisa mendapatkan banyak jarak tempuh dari semua upaya ekstra itu jika Anda benar-benar perlu menyimpan POD dan UDT non-sepele, gunakan algoritma generik berbasis iterator yang bekerja pada struktur data yang sesuai, manfaat dari penanganan pengecualian dan RAII, setidaknya kadang-kadang menimpa std::allocatordengan pengalokasi kustom Anda sendiri, dll. Itu pasti terbayar di perpustakaan standar ketika Anda mempertimbangkan berapa banyak manfaatstd::vector telah ada di seluruh dunia orang yang telah menggunakannya, tetapi itu untuk sesuatu yang diterapkan di perpustakaan standar yang dirancang untuk menargetkan kebutuhan seluruh dunia.

Implementasi Sederhana Menangani Kasus Penggunaan Sangat Spesifik

Sebagai hasil dari hanya menangani kasus penggunaan yang sangat spesifik dengan "daftar bebas terindeks" saya, meskipun menerapkan daftar gratis ini selusin kali di sisi C dan memiliki beberapa kode sepele yang digandakan sebagai hasilnya, saya mungkin telah menulis lebih sedikit kode total dalam C untuk mengimplementasikan itu selusin kali daripada saya harus mengimplementasikannya hanya satu kali dalam C ++, dan saya harus menghabiskan lebih sedikit waktu mempertahankan selusin implementasi C daripada saya harus mempertahankan bahwa satu implementasi C ++. Salah satu alasan utama sisi C sangat sederhana adalah bahwa saya biasanya bekerja dengan POD dalam C setiap kali saya menggunakan teknik ini dan saya biasanya tidak memerlukan lebih banyak fungsi daripada insertdanerasedi situs spesifik tempat saya menerapkan ini secara lokal. Pada dasarnya saya hanya dapat mengimplementasikan subset fungsi paling remaja dari versi C ++, karena saya bebas untuk membuat lebih banyak asumsi tentang apa yang saya lakukan dan tidak butuhkan dari desain ketika saya mengimplementasikannya untuk penggunaan yang sangat spesifik. kasus.

Sekarang versi C ++ jauh lebih bagus dan aman untuk digunakan, tetapi itu masih merupakan PITA utama untuk diimplementasikan dan membuat pengecualian-aman dan dua arah yang sesuai dengan iterator, misalnya, dengan cara-cara menghasilkan satu umum, implementasi yang dapat digunakan kembali mungkin memerlukan biaya lebih banyak waktu daripada yang sebenarnya dihemat dalam kasus ini. Dan banyak dari biaya penerapannya dengan cara yang digeneralisasi tidak hanya terbuang di muka, tetapi berulang kali dalam bentuk hal-hal seperti peningkatan waktu pembangunan yang dibayarkan berulang setiap hari.

Bukan Serangan pada C ++!

Tapi ini bukan serangan pada C ++ karena saya suka C ++, tetapi ketika datang ke struktur data, saya datang untuk mendukung C ++ terutama untuk struktur data yang benar-benar non-sepele yang ingin saya luangkan jauh lebih banyak waktu di muka untuk diterapkan di cara yang sangat umum, membuat pengecualian-aman terhadap semua jenis yang mungkin T, membuat standar-compliant dan iterable, dll, di mana jenis biaya dimuka benar-benar terbayar dalam bentuk satu ton jarak tempuh.

Namun itu juga mempromosikan pola pikir desain yang sangat berbeda. Dalam C ++ jika saya ingin membuat Oktree untuk deteksi tabrakan, saya memiliki kecenderungan ingin menggeneralisasikannya ke tingkat ke-n. Saya tidak hanya ingin membuatnya menyimpan jerat segitiga terindeks. Mengapa saya harus membatasi hanya pada satu tipe data yang dapat bekerja ketika saya memiliki mekanisme pembuatan kode yang sangat kuat di ujung jari saya yang menghilangkan semua penalti abstraksi saat runtime? Saya ingin menyimpan bola prosedural, kubus, voxel, permukaan NURBs, titik awan, dll dll dan mencoba membuatnya baik untuk semuanya, karena tergoda untuk ingin mendesain seperti itu ketika Anda memiliki template di ujung jari Anda. Saya bahkan mungkin tidak ingin membatasi untuk deteksi tabrakan - bagaimana dengan raytracing, picking, dll? C ++ membuatnya awalnya terlihat "agak mudah" untuk menggeneralisasi struktur data ke tingkat ke-n. Dan itulah yang saya gunakan untuk merancang indeks spasial seperti itu di C ++. Saya mencoba mendesain mereka untuk menangani kebutuhan kelaparan seluruh dunia, dan apa yang saya dapatkan sebagai pertukaran biasanya adalah "jack of all trade" dengan kode yang sangat kompleks untuk menyeimbangkannya dengan semua kemungkinan kasus penggunaan yang bisa dibayangkan.

Yang cukup lucu, saya mendapatkan lebih banyak penggunaan kembali dari indeks spasial yang telah saya terapkan dalam C selama bertahun-tahun, dan bukan karena kesalahan C ++, tetapi hanya menambang dalam bahasa yang membuat saya tergoda untuk melakukannya. Ketika saya kode sesuatu seperti sebuah octree di C, saya memiliki kecenderungan untuk membuatnya bekerja dengan poin dan senang dengan itu, karena bahasa membuatnya terlalu sulit untuk bahkan berusaha untuk menggeneralisasikannya ke tingkat n. Tetapi karena kecenderungan-kecenderungan itu, saya cenderung merancang hal-hal selama bertahun-tahun yang sebenarnya lebih efisien dan dapat diandalkan dan sangat cocok untuk tugas-tugas tertentu yang ada, karena mereka tidak repot-repot menjadi umum ke tingkat ke-n. Mereka menjadi ace dalam satu kategori khusus alih-alih jack semua perdagangan. Sekali lagi itu bukan karena kesalahan C ++ tetapi hanya kecenderungan manusia yang saya miliki ketika saya menggunakannya sebagai lawan dari C.

Tapi bagaimanapun, saya suka kedua bahasa tetapi ada kecenderungan yang berbeda. Dalam CI memiliki kecenderungan untuk tidak menggeneralisasi cukup. Dalam C ++ saya memiliki kecenderungan untuk menggeneralisasi terlalu banyak. Menggunakan keduanya telah membantu saya untuk menyeimbangkan diri.

Apakah implementasi generik merupakan norma, atau apakah Anda menulis implementasi yang berbeda untuk setiap kasus penggunaan?

Untuk hal-hal sepele seperti daftar indeks 32-bit yang terhubung secara tunggal menggunakan node dari array atau array yang merealokasi sendiri (setara dengan analog std::vectordalam C ++) atau, katakanlah, sebuah octree yang hanya menyimpan poin dan bertujuan untuk tidak melakukan apa-apa lagi, saya tidak t repot untuk menggeneralisasi ke titik menyimpan semua tipe data. Saya menerapkan ini untuk menyimpan tipe data tertentu (meskipun mungkin abstrak dan menggunakan pointer fungsi dalam beberapa kasus, tetapi setidaknya lebih spesifik daripada bebek mengetik dengan polimorfisme statis).

Dan saya sangat senang dengan sedikit redundansi dalam kasus-kasus tersebut asalkan saya unit mengujinya secara menyeluruh. Jika saya tidak menguji unit, maka redundansi mulai terasa jauh lebih tidak nyaman, karena Anda mungkin memiliki kode berlebihan yang dapat menduplikasi kesalahan, mis. Lalu bahkan jika jenis kode yang Anda tulis tidak akan pernah membutuhkan perubahan desain apa pun, mungkin masih perlu diubah karena rusak. Saya cenderung menulis unit test yang lebih menyeluruh untuk kode C yang saya tulis sebagai alasan.

Untuk hal-hal nontrivial, itu biasanya ketika saya meraih C ++, tetapi jika saya mengimplementasikannya di C, saya akan mempertimbangkan menggunakan void*pointer saja , mungkin menerima ukuran tipe untuk mengetahui berapa banyak memori yang dialokasikan untuk setiap elemen, dan mungkin copy/destroyfungsi pointer untuk menyalin secara mendalam dan menghancurkan data jika tidak mudah dibangun / dirusak. Sebagian besar waktu saya tidak mengganggu dan tidak menggunakan terlalu banyak C untuk membuat struktur data dan algoritma yang paling kompleks.

Jika Anda menggunakan satu struktur data cukup sering dengan tipe data tertentu, Anda juga bisa membungkus versi tipe-aman di atas yang hanya bekerja dengan bit dan byte dan fungsi pointer dan void*, misalnya, untuk menerapkan kembali keamanan jenis melalui pembungkus C.

Saya bisa mencoba menulis implementasi generik untuk peta hash misalnya, tetapi saya selalu menemukan hasil akhirnya menjadi berantakan. Saya juga bisa menulis implementasi khusus hanya untuk kasus penggunaan khusus ini, menjaga kode tetap jelas dan mudah dibaca dan debug. Yang terakhir tentu saja akan menyebabkan duplikasi kode.

Tabel hash agak rapuh karena mungkin sepele untuk diterapkan atau sangat kompleks tergantung pada seberapa kompleks kebutuhan Anda sehubungan dengan hash, pengulangan, jika Anda perlu secara otomatis membuat tabel tumbuh sendiri secara implisit atau dapat mengantisipasi ukuran tabel di maju, apakah Anda menggunakan pengalamatan terbuka atau rantai terpisah, dll. Tetapi satu hal yang perlu diingat adalah bahwa jika Anda merancang tabel hash dengan sempurna untuk kebutuhan situs tertentu, seringkali tidak akan terlalu rumit dalam implementasi dan sering dimenangkan Sangat berlebihan ketika dirancang khusus untuk kebutuhan tersebut. Setidaknya itulah alasan saya memberi diri saya jika saya mengimplementasikan sesuatu secara lokal. Jika tidak, Anda mungkin hanya menggunakan metode yang dijelaskan di atas dengan void*dan fungsi pointer untuk menyalin / menghancurkan sesuatu dan menggeneralisasikannya.

Seringkali tidak butuh banyak usaha atau banyak kode untuk mengalahkan struktur data yang sangat umum jika alternatif Anda sangat sempit diterapkan untuk kasus penggunaan yang tepat. Sebagai contoh, itu benar-benar sepele untuk mengalahkan kinerja menggunakan mallocuntuk setiap node (bukan untuk menyatukan sekelompok memori untuk banyak node) sekali dan untuk semua dengan kode Anda tidak perlu mengunjungi kembali untuk kasus penggunaan yang sangat, sangat tepat bahkan ketika implementasi baru mallockeluar. Mungkin perlu seumur hidup untuk mengalahkannya dan membuat kode yang tidak kalah rumitnya sehingga Anda harus mencurahkan sebagian besar hidup Anda untuk mempertahankan dan menjaganya agar tetap terbaru jika Anda ingin mencocokkan umumnya.

Sebagai contoh lain, saya sering merasa sangat mudah untuk mengimplementasikan solusi yang 10 kali lebih cepat atau lebih dari solusi VFX yang ditawarkan oleh Pixar atau Dreamworks. Saya bisa melakukannya dalam tidur saya. Tapi itu bukan karena implementasi saya lebih unggul - jauh, jauh dari itu. Mereka benar-benar inferior bagi kebanyakan orang. Mereka hanya unggul untuk kasus penggunaan saya yang sangat, sangat spesifik. Versi saya jauh, jauh lebih umum diterapkan daripada Pixar atau Dreamwork. Ini perbandingan yang sangat tidak adil karena solusi mereka benar-benar brilian dibandingkan dengan solusi sederhana saya yang bodoh, tapi itu intinya. Perbandingannya tidak harus adil. Jika semua yang Anda butuhkan adalah beberapa hal yang sangat spesifik, Anda tidak perlu membuat struktur data menangani daftar hal-hal yang tidak Anda butuhkan.

Bit dan Bytes Homogen

Satu hal yang dapat dieksploitasi di C karena memiliki kurangnya keamanan jenis yang melekat adalah ide menyimpan hal-hal secara homogen berdasarkan karakteristik bit dan byte. Ada lebih banyak blur di sana sebagai hasil antara pengalokasi memori dan struktur data.

Tetapi menyimpan banyak hal berukuran variabel, atau bahkan hal-hal yang hanya bisa berukuran variabel, seperti polimorfik Dogdan Cat, sulit dilakukan secara efisien. Anda tidak dapat menggunakan asumsi bahwa mereka bisa berukuran variabel dan menyimpannya secara berdekatan dalam wadah akses acak sederhana karena langkah untuk berpindah dari satu elemen ke elemen berikutnya bisa berbeda. Sebagai hasil untuk menyimpan daftar yang berisi anjing dan kucing, Anda mungkin harus menggunakan 3 contoh struktur data / pengalokasi yang terpisah (satu untuk anjing, satu untuk kucing, dan satu untuk daftar polimorfik dari pointer dasar atau pointer pintar, atau lebih buruk , alokasikan masing-masing anjing dan kucing ke pengalokasi untuk tujuan umum dan sebarkan ke seluruh memori), yang menjadi mahal dan menimbulkan bagian kesalahan cache berlipat ganda.

Jadi salah satu strategi untuk digunakan dalam C, meskipun ia datang pada kekayaan jenis dan keamanan yang dikurangi, adalah untuk menggeneralisasi pada tingkat bit dan byte. Anda mungkin dapat mengasumsikan bahwa Dogsdan Catsmembutuhkan jumlah bit dan byte yang sama, memiliki bidang yang sama, pointer yang sama ke tabel pointer fungsi. Tetapi sebagai gantinya Anda kemudian dapat mengkodekan struktur data yang lebih sedikit, tetapi sama pentingnya, menyimpan semua hal ini secara efisien dan berdekatan. Anda memperlakukan anjing dan kucing seperti serikat analogis dalam kasus itu (atau Anda mungkin benar-benar menggunakan serikat pekerja).

Dan itu memang membutuhkan biaya besar untuk mengetik keamanan. Jika ada satu hal yang saya lewatkan lebih dari hal lain di C, ini adalah tipe safety. Semakin dekat ke tingkat perakitan di mana struktur hanya menunjukkan berapa banyak memori yang dialokasikan dan bagaimana setiap bidang data disejajarkan. Tapi itu sebenarnya alasan utama saya untuk menggunakan C. Jika Anda benar-benar mencoba untuk mengontrol tata letak memori dan di mana semuanya dialokasikan dan di mana semuanya disimpan relatif satu sama lain, sering kali membantu untuk hanya memikirkan hal-hal pada tingkat bit dan byte, dan berapa banyak bit dan byte yang Anda butuhkan untuk menyelesaikan masalah tertentu. Di sana, kebodohan sistem tipe C benar-benar dapat menjadi bermanfaat daripada cacat. Biasanya itu akan menghasilkan tipe data yang jauh lebih sedikit untuk dihadapi,

Duplikasi Ilusi / Jelas

Sekarang saya telah menggunakan "duplikasi" dalam arti longgar untuk hal-hal yang bahkan mungkin tidak berlebihan. Saya telah melihat orang membedakan istilah seperti duplikasi "insidental / jelas" dari "duplikasi aktual". Menurut saya, tidak ada perbedaan yang jelas dalam banyak kasus. Saya menemukan perbedaan lebih seperti "potensi keunikan" vs "potensi duplikasi" dan itu bisa berjalan baik. Seringkali tergantung pada bagaimana Anda ingin desain dan implementasi Anda berkembang dan seberapa sempurna mereka akan disesuaikan untuk kasus penggunaan tertentu. Tetapi saya sering menemukan bahwa apa yang kelihatannya duplikasi kode kemudian ternyata tidak lagi menjadi redundan setelah beberapa iterasi perbaikan.

Ambil implementasi array growable sederhana menggunakan realloc, setara analog std::vector<int>. Awalnya mungkin berlebihan dengan, katakanlah, gunakan std::vector<int>di C ++. Tetapi Anda mungkin menemukan, melalui pengukuran, bahwa mungkin bermanfaat untuk melakukan pra-alokasi 64 byte terlebih dahulu untuk memungkinkan enam belas bilangan bulat 32-bit dimasukkan tanpa memerlukan alokasi tumpukan. Sekarang tidak lagi mubazir, setidaknya tidak dengan std::vector<int>. Dan kemudian Anda mungkin berkata, "Tapi saya hanya bisa menggeneralisasi ini ke yang baru SmallVector<int, 16>, dan Anda bisa. Tapi kemudian katakanlah Anda menemukan itu berguna karena ini adalah untuk array yang sangat kecil, berumur pendek untuk melipatgandakan kapasitas array pada alokasi heap daripada meningkat 1,5 (kira-kira jumlah yang banyakvectorimplementasi digunakan) sambil bekerja dengan asumsi bahwa kapasitas array selalu merupakan kekuatan dua. Sekarang wadah Anda benar-benar berbeda, dan mungkin tidak ada wadah seperti itu. Dan mungkin Anda bisa mencoba untuk menggeneralisasi perilaku seperti itu dengan menambahkan lebih banyak parameter templat untuk mengubahsuaikan preallocation heavior, menyesuaikan perilaku realokasi, dll. Dll, tetapi pada saat itu Anda mungkin menemukan sesuatu yang sangat sulit untuk digunakan dibandingkan dengan belasan baris C sederhana kode.

Dan Anda bahkan mungkin mencapai titik di mana Anda memerlukan struktur data yang mengalokasikan 256-bit memori yang disejajarkan dan empuk, menyimpan secara eksklusif POD untuk instruksi AVX 256, mengalokasikan 128 byte untuk menghindari alokasi tumpukan untuk ukuran input kecil yang umum, menggandakan kapasitas ketika penuh, dan memungkinkan overwrite yang aman dari elemen tambahan melebihi ukuran array tetapi tidak melebihi kapasitas array. Pada titik itu, jika Anda masih berusaha menggeneralisasi solusi untuk menghindari duplikasi sejumlah kecil kode C, semoga dewa-dewa pemrogram mengampuni jiwa Anda.

Jadi ada juga saat-saat seperti ini di mana apa yang awalnya mulai tampak berlebihan mulai tumbuh, ketika Anda menyesuaikan solusi untuk lebih baik dan lebih baik dan lebih cocok dengan kasus penggunaan tertentu, menjadi sesuatu yang sepenuhnya unik dan tidak berlebihan sama sekali. Tapi itu hanya untuk hal-hal di mana Anda dapat menyesuaikannya dengan sempurna untuk kasus penggunaan tertentu. Terkadang kita hanya membutuhkan hal yang "layak" yang digeneralisasikan untuk tujuan kita, dan di sana saya mendapat manfaat paling banyak dari struktur data yang sangat umum. Tetapi untuk hal-hal luar biasa yang dibuat dengan sempurna untuk use case tertentu, gagasan "tujuan umum" dan "dibuat sempurna untuk tujuan saya" mulai menjadi terlalu tidak sesuai.

POD dan Primitif

Sekarang di C, saya sering menemukan alasan untuk menyimpan POD dan terutama primitif ke dalam struktur data bila memungkinkan. Itu mungkin tampak seperti anti-pola, tetapi saya benar-benar menemukannya secara tidak sengaja membantu dalam meningkatkan pemeliharaan kode atas jenis hal yang saya lakukan lebih sering di C ++.

Contoh sederhana adalah menginternir string pendek (seperti biasanya kasus dengan string yang digunakan untuk kunci pencarian - mereka cenderung sangat pendek). Mengapa repot berurusan dengan semua string panjang variabel ini yang ukurannya bervariasi saat runtime, menyiratkan konstruksi dan kehancuran non-sepele (karena kita mungkin perlu menumpuk mengalokasikan dan membebaskan)? Bagaimana kalau hanya menyimpan hal-hal ini dalam struktur data pusat, seperti trie aman-benang atau tabel hash yang dirancang hanya untuk pemanggilan string, dan kemudian merujuk ke string tersebut dengan yang lama int32_tatau:

struct IternedString 
{
    int32_t index;
};

... di tabel hash kita, pohon merah-hitam, lewati daftar, dll, jika kita tidak memerlukan penyortiran leksikografis? Sekarang semua struktur data kami yang kami kodekan untuk bekerja dengan bilangan bulat 32-bit sekarang dapat menyimpan kunci-kunci string yang diinternir yang secara efektif hanya 32-bit ints. Dan saya telah menemukan setidaknya dalam kasus penggunaan saya (mungkin saja domain saya karena saya bekerja di bidang-bidang seperti raytracing, pemrosesan mesh, pemrosesan gambar, sistem partikel, mengikat ke bahasa scripting, implementasi kit GUI multithreaded tingkat rendah, dll - hal-hal tingkat rendah tetapi tidak tingkat rendah seperti OS), bahwa kode secara kebetulan menjadi lebih efisien dan sederhana hanya menyimpan indeks untuk hal-hal seperti ini. Itu membuatnya begitu saya sering bekerja, katakanlah 75% dari waktu, dengan adil int32_tdanfloat32 dalam struktur data non-sepele saya, atau hanya menyimpan hal-hal yang ukurannya sama (hampir selalu 32-bit).

Dan tentu saja jika itu berlaku untuk kasus Anda, Anda dapat menghindari memiliki sejumlah implementasi struktur data untuk tipe data yang berbeda, karena Anda akan bekerja dengan sangat sedikit di tempat pertama.

Pengujian dan Keandalan

Satu hal terakhir yang saya tawarkan, dan mungkin tidak untuk semua orang, adalah mendukung tes tertulis untuk struktur data tersebut. Buat mereka benar-benar hebat dalam sesuatu. Pastikan mereka sangat andal.

Beberapa duplikasi kode kecil menjadi jauh lebih dapat dimaafkan dalam kasus-kasus itu, karena duplikasi kode hanya merupakan beban pemeliharaan jika Anda harus membuat perubahan cascading ke kode yang digandakan. Anda menghilangkan salah satu alasan utama untuk kode redundan tersebut untuk berubah dengan memastikan itu sangat andal dan sangat cocok untuk apa yang tidak perlu dilakukan.

Perasaan estetika saya telah berubah selama bertahun-tahun. Saya tidak lagi kesal karena saya melihat satu perpustakaan mengimplementasikan produk titik atau logika SLL sepele yang sudah diterapkan di yang lain. Saya hanya merasa kesal ketika segala sesuatu diuji dengan buruk dan tidak dapat diandalkan, dan saya telah menemukan bahwa pola pikir yang jauh lebih produktif. Saya telah benar-benar berurusan dengan basis kode yang menggandakan bug melalui kode duplikat, dan telah melihat kasus terburuk dari pengkodean copy-and-paste membuat apa yang seharusnya menjadi perubahan sepele ke satu tempat pusat berubah menjadi perubahan cascading rawan kesalahan ke banyak. Namun banyak dari waktu itu, itu adalah hasil dari pengujian yang buruk, dari kode gagal menjadi dapat diandalkan dan baik pada apa yang dilakukannya di tempat pertama. Sebelumnya ketika saya bekerja di basis kode warisan kereta, pikiran saya menghubungkan semua bentuk duplikasi kode memiliki kemungkinan yang sangat tinggi untuk menduplikasi bug dan membutuhkan perubahan cascading. Namun perpustakaan miniatur yang melakukan satu hal dengan sangat baik dan andal akan menemukan sangat sedikit alasan untuk berubah di masa depan, bahkan jika ada beberapa kode yang tampak berlebihan di sana-sini. Prioritas saya tidak pada waktu itu ketika duplikasi menjengkelkan saya lebih dari kualitas buruk dan kurangnya pengujian. Hal-hal yang terakhir ini harus menjadi prioritas utama.

Duplikasi Kode Untuk Minimalis?

Ini adalah pemikiran lucu yang muncul di kepala saya, tetapi pertimbangkan sebuah kasus di mana kita mungkin menemukan perpustakaan C dan C ++ yang kira-kira melakukan hal yang sama: keduanya memiliki fungsi yang kira-kira sama, jumlah penanganan kesalahan yang sama, satu tidak signifikan lebih efisien daripada yang lain, dll. Dan yang paling penting, keduanya dilaksanakan secara kompeten, teruji dengan baik, dan dapat diandalkan. Sayangnya saya harus berbicara secara hipotetis di sini karena saya belum pernah menemukan sesuatu yang mendekati perbandingan berdampingan yang sempurna. Tetapi hal-hal terdekat yang pernah saya temukan untuk perbandingan berdampingan ini sering memiliki pustaka C menjadi jauh, jauh lebih kecil dari padanan C ++ (terkadang 1/10 dari ukuran kodenya).

Dan saya percaya alasan untuk itu adalah karena, sekali lagi, untuk menyelesaikan masalah secara umum yang menangani berbagai kasus penggunaan terluas, alih-alih satu kasus penggunaan yang tepat mungkin memerlukan ratusan hingga ribuan baris kode, sedangkan yang terakhir mungkin hanya memerlukan satu lusin. Terlepas dari redundansi, dan terlepas dari kenyataan bahwa pustaka standar C sangat buruk dalam hal menyediakan struktur data standar, sering kali menghasilkan lebih sedikit kode di tangan manusia untuk menyelesaikan masalah yang sama, dan saya pikir itu terutama disebabkan dengan perbedaan dalam kecenderungan manusia antara dua bahasa ini. Satu mempromosikan pemecahan hal-hal terhadap use case yang sangat spesifik, yang lain cenderung untuk mempromosikan solusi yang lebih abstrak dan generik terhadap berbagai kasus penggunaan terluas, tetapi hasil akhir dari ini tidak

Saya sedang melihat raytracer seseorang di github hari yang lalu dan itu diimplementasikan dalam C ++ dan diperlukan begitu, begitu banyak kode untuk mainan raytracer. Dan saya tidak menghabiskan banyak waktu untuk melihat kode tetapi ada muatan kapal struktur tujuan umum di sana yang menangani cara, jauh lebih dari apa yang dibutuhkan raytracer. Dan saya mengenali gaya pengkodean itu karena saya dulu menggunakan C ++ dengan cara yang sama dalam mode super bottom-up, dengan fokus membuat pustaka yang penuh dengan struktur data yang sangat umum dulu yang bergerak jauh di atas dan di luar yang langsung masalah di tangan dan kemudian menangani masalah yang sebenarnya kedua. Tetapi sementara struktur-struktur umum itu mungkin menghilangkan beberapa redudansi di sana-sini dan menikmati banyak penggunaan kembali dalam konteks baru, sebagai gantinya mereka menggelembungkan proyek dengan sangat besar dengan menukar sedikit redundansi dengan muatan kode / fungsi yang tidak perlu, dan yang terakhir tidak selalu lebih mudah untuk dipelihara daripada yang sebelumnya. Sebaliknya, saya sering merasa lebih sulit untuk mempertahankan, karena sulit untuk mempertahankan desain sesuatu yang umum yang harus memperketat keputusan desain dengan berbagai kebutuhan terluas yang mungkin.


sumber