Saya membaca buku "Exceptional C ++" oleh Herb Sutter, dan dalam buku itu saya telah belajar tentang idiom pImpl. Pada dasarnya, idenya adalah membuat struktur untuk private
objek a class
dan mengalokasikannya secara dinamis untuk mengurangi waktu kompilasi (dan juga menyembunyikan implementasi pribadi dengan cara yang lebih baik).
Sebagai contoh:
class X
{
private:
C c;
D d;
} ;
dapat diubah menjadi:
class X
{
private:
struct XImpl;
XImpl* pImpl;
};
dan, dalam CPP, definisi:
struct X::XImpl
{
C c;
D d;
};
Tampaknya ini cukup menarik, tetapi saya belum pernah melihat pendekatan semacam ini sebelumnya, baik di perusahaan tempat saya bekerja, maupun dalam proyek open source yang pernah saya lihat kode sumbernya. Jadi, saya bertanya-tanya apakah teknik ini benar-benar digunakan dalam praktek?
Haruskah saya menggunakannya di mana-mana, atau dengan hati-hati? Dan apakah teknik ini direkomendasikan untuk digunakan dalam sistem embedded (di mana kinerja sangat penting)?
sumber
struct XImpl : public X
. Itu terasa lebih alami bagi saya. Apakah ada masalah lain yang saya lewatkan?const unique_ptr<XImpl>
alih - alihXImpl*
.Jawaban:
Tentu saja itu digunakan. Saya menggunakannya dalam proyek saya, di hampir setiap kelas.
Alasan untuk menggunakan idiom PIMPL:
Kompatibilitas biner
Saat Anda mengembangkan pustaka, Anda dapat menambah / memodifikasi bidang
XImpl
tanpa merusak kompatibilitas biner dengan klien Anda (yang berarti macet!). Karena tata letak binerX
kelas tidak berubah ketika Anda menambahkan bidang baru keXimpl
kelas, aman untuk menambahkan fungsionalitas baru ke perpustakaan dalam pembaruan versi kecil.Tentu saja, Anda juga dapat menambahkan metode publik / swasta non-virtual ke
X
/XImpl
tanpa melanggar kompatibilitas biner, tetapi itu setara dengan header standar / teknik implementasi.Menyembunyikan data
Jika Anda sedang mengembangkan perpustakaan, terutama yang eksklusif, mungkin diinginkan untuk tidak mengungkapkan apa perpustakaan / teknik implementasi lain yang digunakan untuk mengimplementasikan antarmuka publik perpustakaan Anda. Baik karena masalah Kekayaan Intelektual, atau karena Anda percaya bahwa pengguna mungkin tergoda untuk mengambil asumsi berbahaya tentang implementasi atau hanya memecahkan enkapsulasi dengan menggunakan trik casting yang mengerikan. PIMPL memecahkan / mengurangi itu.
Waktu kompilasi
Waktu kompilasi berkurang, karena hanya file sumber (implementasi) yang
X
perlu dibangun kembali ketika Anda menambahkan / menghapus bidang dan / atau metode keXImpl
kelas (yang memetakan untuk menambahkan bidang / metode pribadi dalam teknik standar). Dalam praktiknya, ini adalah operasi yang umum.Dengan header standar / teknik implementasi (tanpa PIMPL), ketika Anda menambahkan bidang baru
X
, setiap klien yang pernah mengalokasikanX
(baik di stack, atau di heap) perlu dikompilasi ulang, karena harus menyesuaikan ukuran alokasi. Yah, setiap klien yang tidak pernah mengalokasikan X juga perlu dikompilasi ulang, tetapi itu hanya overhead (kode yang dihasilkan di sisi klien akan sama).Terlebih lagi, dengan header standar / pemisahan implementasi
XClient1.cpp
perlu dikompilasi ulang bahkan ketika metode pribadiX::foo()
ditambahkanX
danX.h
diubah, meskipunXClient1.cpp
tidak mungkin memanggil metode ini karena alasan enkapsulasi! Seperti di atas, ini murni overhead dan terkait dengan bagaimana sistem C ++ membangun kehidupan nyata bekerja.Tentu saja, kompilasi ulang tidak diperlukan ketika Anda hanya memodifikasi implementasi metode (karena Anda tidak menyentuh header), tetapi itu setara dengan header standar / teknik implementasi.
Itu tergantung pada seberapa kuat target Anda. Namun satu-satunya jawaban untuk pertanyaan ini adalah: mengukur dan mengevaluasi apa yang Anda dapatkan dan kehilangan. Juga, pertimbangkan bahwa jika Anda tidak menerbitkan perpustakaan yang dimaksudkan untuk digunakan dalam sistem embedded oleh klien Anda, hanya keuntungan waktu kompilasi yang berlaku!
sumber
Tampaknya banyak perpustakaan di luar sana menggunakannya untuk tetap stabil di API mereka, setidaknya untuk beberapa versi.
Tetapi untuk semua hal, Anda tidak boleh menggunakan apa pun di mana pun tanpa hati-hati. Selalu berpikir sebelum menggunakannya. Mengevaluasi keuntungan apa yang diberikannya kepada Anda, dan jika mereka sepadan dengan harga yang Anda bayar.
Keuntungan yang bisa Anda berikan adalah:
Itu mungkin atau mungkin bukan keuntungan nyata bagi Anda. Seperti untuk saya, saya tidak peduli tentang waktu rekompilasi beberapa menit. Pengguna akhir biasanya juga tidak, karena mereka selalu mengkompilasinya sekali dan dari awal.
Kerugian yang mungkin ada (juga di sini, tergantung pada implementasinya dan apakah itu kerugian nyata bagi Anda):
Jadi hati-hati berikan semuanya nilai, dan evaluasi untuk diri sendiri. Bagi saya, hampir selalu ternyata menggunakan idiom jerawat tidak sepadan dengan usaha. Hanya ada satu kasus di mana saya secara pribadi menggunakannya (atau setidaknya sesuatu yang serupa):
Pembungkus C ++ saya untuk
stat
panggilan linux . Di sini struct dari header C mungkin berbeda, tergantung pada apa#defines
yang ditetapkan. Dan karena header pembungkus saya tidak dapat mengontrol semuanya, saya hanya#include <sys/stat.h>
di.cxx
file saya dan menghindari masalah ini.sumber
File
Kelas saya (yang mengekspos banyak informasistat
akan kembali di bawah Unix) menggunakan antarmuka yang sama di bawah Windows dan Unix, misalnya.#ifdef
s untuk membuat bungkusnya setipis mungkin. Tetapi setiap orang memiliki tujuan yang berbeda, yang penting adalah meluangkan waktu untuk memikirkannya alih-alih mengikuti sesuatu secara membabi buta.Setuju dengan semua yang lain tentang barang, tetapi izinkan saya memberi bukti batas: tidak bekerja dengan baik dengan templat .
Alasannya adalah bahwa instantiasi template memerlukan deklarasi lengkap yang tersedia di mana instantiasi berlangsung. (Dan itulah alasan utama Anda tidak melihat metode templat didefinisikan ke dalam file CPP)
Anda masih dapat merujuk ke subclass templetised, tetapi karena Anda harus memasukkan semuanya, setiap keuntungan dari "decoupling implementasi" pada kompilasi (menghindari untuk memasukkan semua kode spesifik platoform di mana-mana, memperpendek kompilasi) hilang.
Merupakan paradigma yang baik untuk OOP klasik (berbasis warisan) tetapi tidak untuk pemrograman generik (berbasis spesialisasi).
sumber
Orang lain telah memberikan teknis naik / turun, tetapi saya pikir hal berikut ini perlu diperhatikan:
Pertama dan terutama, jangan dogmatis. Jika pImpl berfungsi untuk situasi Anda, gunakan - jangan gunakan hanya karena "lebih baik OO karena benar - benar menyembunyikan implementasi" dll. Mengutip FAQ C ++:
Sekadar memberi Anda contoh perangkat lunak open source di mana ia digunakan dan mengapa: OpenThreads, pustaka threading yang digunakan oleh OpenSceneGraph . Gagasan utamanya adalah menghapus dari header (mis.
<Thread.h>
) Semua kode platform khusus, karena variabel status internal (mis. Ulir) berbeda dari platform ke platform. Dengan cara ini seseorang dapat mengkompilasi kode terhadap pustaka Anda tanpa mengetahui keanehan platform lainnya, karena semuanya tersembunyi.sumber
Saya terutama akan mempertimbangkan PIMPL untuk kelas yang diekspos untuk digunakan sebagai API oleh modul lain. Ini memiliki banyak manfaat, karena membuat kompilasi dari perubahan yang dibuat dalam implementasi PIMPL tidak mempengaruhi sisa proyek. Juga, untuk kelas API mereka mempromosikan kompatibilitas biner (perubahan dalam implementasi modul tidak mempengaruhi klien dari modul tersebut, mereka tidak harus dikompilasi ulang karena implementasi baru memiliki antarmuka biner yang sama - antarmuka yang diekspos oleh PIMPL).
Mengenai penggunaan PIMPL untuk setiap kelas, saya akan mempertimbangkan kehati-hatian karena semua manfaat itu dikenakan biaya: diperlukan tingkat tipuan ekstra untuk mengakses metode implementasi.
sumber
Saya pikir ini adalah salah satu alat paling mendasar untuk decoupling.
Saya menggunakan pimpl (dan banyak idiom lain dari Exceptional C ++) pada proyek embedded (SetTopBox).
Tujuan khusus idoim ini dalam proyek kami adalah untuk menyembunyikan tipe yang digunakan kelas XImpl. Secara khusus kami menggunakannya untuk menyembunyikan detail implementasi untuk perangkat keras yang berbeda, di mana header yang berbeda akan ditarik. Kami memiliki implementasi yang berbeda dari kelas XImpl untuk satu platform dan berbeda untuk yang lainnya. Layout kelas X tetap sama terlepas dari platfrom.
sumber
Saya dulu sering menggunakan teknik ini tetapi kemudian menemukan diri saya menjauh darinya.
Tentu saja merupakan ide bagus untuk menyembunyikan detail implementasi dari pengguna kelas Anda. Namun Anda juga bisa melakukannya dengan membuat pengguna kelas menggunakan antarmuka abstrak dan untuk detail implementasi menjadi kelas konkret.
Keuntungan dari pImpl adalah:
Dengan asumsi hanya ada satu implementasi dari antarmuka ini, itu lebih jelas dengan tidak menggunakan implementasi abstrak kelas / konkret
Jika Anda memiliki paket kelas (modul) sehingga beberapa kelas mengakses "impl" yang sama tetapi pengguna modul hanya akan menggunakan kelas "terbuka".
Tidak ada v-table jika ini dianggap hal yang buruk.
Kerugian yang saya temukan dari pImpl (di mana antarmuka abstrak berfungsi lebih baik)
Meskipun Anda mungkin hanya memiliki satu implementasi "produksi", dengan menggunakan antarmuka abstrak, Anda juga dapat membuat aplikasi "tiruan" yang berfungsi dalam pengujian unit.
(Masalah terbesar). Sebelum hari-hari unique_ptr dan pemindahan Anda telah membatasi pilihan tentang cara menyimpan pImpl. Pointer mentah dan Anda memiliki masalah tentang kelas Anda yang tidak dapat disalin. Auto_ptr lama tidak akan berfungsi dengan kelas yang dinyatakan dengan maju (toh tidak pada semua kompiler). Jadi orang-orang mulai menggunakan shared_ptr yang bagus dalam membuat kelas Anda dapat disalin tetapi tentu saja kedua salinan memiliki shared_ptr yang sama yang mungkin tidak Anda harapkan (modifikasi satu dan keduanya dimodifikasi). Jadi solusinya sering menggunakan pointer mentah untuk bagian dalam dan membuat kelas tidak dapat disalin dan mengembalikan shared_ptr sebagai gantinya. Jadi dua panggilan ke yang baru. (Sebenarnya 3 diberikan shared_ptr lama memberi Anda yang kedua).
Secara teknis tidak benar-benar const-benar karena constness tidak disebarkan ke pointer anggota.
Secara umum saya karena itu telah pindah pada tahun-tahun dari pImpl dan menjadi penggunaan antarmuka abstrak (dan metode pabrik untuk membuat instance).
sumber
Seperti banyak kata lain, idiom Pimpl memungkinkan untuk mencapai penyembunyian informasi lengkap dan independensi kompilasi, sayangnya dengan biaya kehilangan kinerja (penunjuk arah tambahan) dan kebutuhan memori tambahan (penunjuk anggota itu sendiri). Biaya tambahan dapat menjadi penting dalam pengembangan perangkat lunak tertanam, khususnya dalam skenario di mana memori harus dihemat sebanyak mungkin. Menggunakan kelas abstrak C ++ sebagai antarmuka akan menghasilkan manfaat yang sama dengan biaya yang sama. Ini menunjukkan sebenarnya kekurangan besar C ++ di mana, tanpa berulang ke antarmuka seperti-C (metode global dengan pointer buram sebagai parameter), tidak mungkin untuk memiliki informasi yang benar bersembunyi dan kompilasi independensi tanpa kekurangan sumber daya tambahan: ini terutama karena deklarasi kelas, yang harus dimasukkan oleh penggunanya,
sumber
Berikut adalah skenario aktual yang saya temui, di mana idiom ini banyak membantu. Saya baru-baru ini memutuskan untuk mendukung DirectX 11, serta dukungan DirectX 9 saya yang ada, dalam mesin game. Mesin sudah membungkus sebagian besar fitur DX, sehingga tidak ada antarmuka DX yang digunakan secara langsung; mereka hanya didefinisikan di header sebagai anggota pribadi. Mesin menggunakan DLL sebagai ekstensi, menambahkan keyboard, mouse, joystick, dan dukungan skrip, sebanyak seminggu ekstensi lainnya. Sementara sebagian besar DLL itu tidak menggunakan DX secara langsung, mereka membutuhkan pengetahuan dan hubungan dengan DX hanya karena mereka menarik header yang mengekspos DX. Dalam menambahkan DX 11, kompleksitas ini meningkat secara dramatis, namun tidak perlu. Memindahkan anggota DX ke Pimpl yang hanya ditentukan di sumber menghilangkan pemaksaan ini. Di atas pengurangan dependensi perpustakaan ini,
sumber
Ini digunakan dalam praktik di banyak proyek. Kegunaannya sangat tergantung pada jenis proyek. Salah satu proyek yang lebih menonjol menggunakan ini adalah Qt , di mana ide dasarnya adalah menyembunyikan implementasi atau kode platform spesifik dari pengguna (pengembang lain yang menggunakan Qt).
Ini adalah ide yang mulia tetapi ada kelemahan nyata untuk ini: debugging Selama kode yang disembunyikan di implemetations pribadi adalah kualitas premium, ini semua baik-baik saja, tetapi jika ada bug di sana, maka pengguna / pengembang memiliki masalah, karena itu hanya penunjuk bodoh untuk implementasi tersembunyi, bahkan jika ia memiliki kode sumber implementasi.
Jadi seperti dalam hampir semua keputusan desain ada pro dan kontra.
sumber
Satu manfaat yang dapat saya lihat adalah memungkinkan programmer untuk mengimplementasikan operasi tertentu dengan cara yang cukup cepat:
PS: Saya harap saya tidak salah paham soal semantik.
sumber