Apakah idiom pImpl benar-benar digunakan dalam praktik?


165

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 privateobjek a classdan 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)?


Apakah ini pada dasarnya sama dengan memutuskan bahwa X adalah antarmuka (abstrak), dan Ximpl adalah implementasinya? struct XImpl : public X. Itu terasa lebih alami bagi saya. Apakah ada masalah lain yang saya lewatkan?
Aaron McDaid

@AaronMcDaid: Ini mirip, tetapi memiliki keuntungan bahwa (a) fungsi anggota tidak harus virtual, dan (b) Anda tidak memerlukan pabrik, atau definisi kelas implementasi, untuk membuat instantiate.
Mike Seymour

2
@AaronMcDaid Idi mucikari menghindari panggilan fungsi virtual. Ini juga sedikit lebih C ++ - ish (untuk beberapa konsepsi C ++ - ish); Anda memanggil konstruktor, bukan fungsi pabrik. Saya telah menggunakan keduanya, tergantung pada apa yang ada di basis kode yang ada --- idiom jerawat (awalnya disebut idiom kucing Cheshire, dan mendahului deskripsi Herb tentang itu setidaknya 5 tahun) tampaknya memiliki sejarah yang lebih panjang dan lebih banyak digunakan dalam C ++, tetapi sebaliknya, keduanya bekerja.
James Kanze

30
Dalam C ++, pimpl harus diimplementasikan dengan const unique_ptr<XImpl>alih - alih XImpl*.
Neil G

1
"belum pernah melihat pendekatan semacam ini sebelumnya, baik di perusahaan tempat saya bekerja, maupun dalam proyek sumber terbuka". Qt hampir tidak pernah menggunakannya BUKAN.
ManuelSchneid3r

Jawaban:


132

Jadi, saya bertanya-tanya apakah teknik ini benar-benar digunakan dalam praktek? Haruskah saya menggunakannya di mana-mana, atau dengan hati-hati?

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 XImpltanpa merusak kompatibilitas biner dengan klien Anda (yang berarti macet!). Karena tata letak biner Xkelas tidak berubah ketika Anda menambahkan bidang baru ke Ximplkelas, aman untuk menambahkan fungsionalitas baru ke perpustakaan dalam pembaruan versi kecil.

Tentu saja, Anda juga dapat menambahkan metode publik / swasta non-virtual ke X/ XImpltanpa 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 Xperlu dibangun kembali ketika Anda menambahkan / menghapus bidang dan / atau metode ke XImplkelas (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 mengalokasikan X(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.cppperlu dikompilasi ulang bahkan ketika metode pribadi X::foo()ditambahkan Xdan X.hdiubah, meskipun XClient1.cpptidak 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.


Apakah teknik ini direkomendasikan untuk digunakan dalam sistem embedded (di mana kinerjanya sangat penting)?

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!


16
+1 karena banyak digunakan di perusahaan tempat saya bekerja, dan untuk alasan yang sama.
Benoit

9
juga, kompatibilitas biner
Ambroz Bizjak

9
Di perpustakaan Qt metode ini juga digunakan dalam situasi penunjuk pintar. Jadi QString menjaga isinya sebagai kelas yang tidak dapat diubah secara internal. Ketika kelas publik "disalin" pointer anggota pribadi disalin bukan seluruh kelas pribadi. Kelas-kelas privat ini kemudian juga menggunakan pointer pintar, sehingga Anda pada dasarnya mendapatkan pengumpulan sampah dengan sebagian besar kelas, selain kinerja yang sangat meningkat karena menyalin pointer daripada menyalin kelas penuh
Timothy Baldridge

8
Terlebih lagi, dengan pimpl idiom Qt dapat mempertahankan kompatibilitas maju dan mundur biner dalam satu versi utama (dalam banyak kasus). IMO sejauh ini merupakan alasan paling signifikan untuk menggunakannya.
whitequark

1
Ini juga berguna untuk mengimplementasikan kode khusus platform, karena Anda dapat menyimpan API yang sama.
doc

49

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:

  • membantu menjaga kompatibilitas biner dari perpustakaan bersama
  • menyembunyikan detail internal tertentu
  • mengurangi siklus kompilasi ulang

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):

  • Peningkatan penggunaan memori karena alokasi lebih banyak daripada dengan varian naif
  • peningkatan upaya pemeliharaan (Anda harus menulis setidaknya fungsi penerusan)
  • kehilangan kinerja (kompiler mungkin tidak dapat menyejajarkan hal-hal sebagaimana dengan implementasi naif kelas 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 statpanggilan linux . Di sini struct dari header C mungkin berbeda, tergantung pada apa #definesyang ditetapkan. Dan karena header pembungkus saya tidak dapat mengontrol semuanya, saya hanya #include <sys/stat.h>di .cxxfile saya dan menghindari masalah ini.


2
Itu harus hampir selalu digunakan untuk antarmuka sistem, untuk membuat sistem kode antarmuka independen. FileKelas saya (yang mengekspos banyak informasi statakan kembali di bawah Unix) menggunakan antarmuka yang sama di bawah Windows dan Unix, misalnya.
James Kanze

5
@JamesKanze: Bahkan di sana saya pribadi pertama-tama akan duduk sebentar dan berpikir tentang apakah tidak cukup untuk memiliki beberapa #ifdefs 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.
PlasmaHH

31

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).


4
Anda harus lebih tepat: sama sekali tidak ada masalah saat menggunakan kelas PIMPL sebagai argumen tipe template. Hanya jika kelas implementasi itu sendiri perlu parametrized pada argumen templat kelas luar, itu tidak bisa disembunyikan dari header antarmuka lagi, bahkan jika itu masih kelas privat. Jika Anda dapat menghapus argumen templat, Anda tentu masih dapat melakukan PIMPL "benar". Dengan penghapusan tipe Anda juga bisa melakukan PIMPL di kelas basis non-templat, dan kemudian minta kelas templat berasal dari itu.
Pasang kembali Monica

22

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 ++:

enkapsulasi adalah untuk kode, bukan orang ( sumber )

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.


12

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.


"Diperlukan tingkat tipuan ekstra untuk mengakses metode implementasi." Ini?
xaxxon

@xaxxon ya, benar. jerawat lebih lambat jika metodenya rendah. jangan pernah menggunakannya untuk hal-hal yang hidup dalam lingkaran yang ketat, misalnya.
Erik Aronesty

@xaxxon Saya akan mengatakan dalam kasus umum tingkat tambahan diperlukan. Jika inlining dilakukan daripada tidak. Tetapi inlinning tidak akan menjadi opsi dalam kode yang dikompilasi dalam dll yang berbeda.
Ghita

5

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.


4

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:

  1. Dengan asumsi hanya ada satu implementasi dari antarmuka ini, itu lebih jelas dengan tidak menggunakan implementasi abstrak kelas / konkret

  2. Jika Anda memiliki paket kelas (modul) sehingga beberapa kelas mengakses "impl" yang sama tetapi pengguna modul hanya akan menggunakan kelas "terbuka".

  3. Tidak ada v-table jika ini dianggap hal yang buruk.

Kerugian yang saya temukan dari pImpl (di mana antarmuka abstrak berfungsi lebih baik)

  1. Meskipun Anda mungkin hanya memiliki satu implementasi "produksi", dengan menggunakan antarmuka abstrak, Anda juga dapat membuat aplikasi "tiruan" yang berfungsi dalam pengujian unit.

  2. (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).

  3. 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).


3

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,


3

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,


2

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.


9
itu bodoh tetapi diketik ... mengapa Anda tidak bisa mengikuti kode di debugger?
UncleZeiv

2
Secara umum, untuk debug ke kode Qt Anda perlu membangun Qt sendiri. Setelah Anda melakukannya, tidak ada masalah melangkah ke metode PIMPL, dan memeriksa konten data PIMPL.
Pasang kembali Monica

0

Satu manfaat yang dapat saya lihat adalah memungkinkan programmer untuk mengimplementasikan operasi tertentu dengan cara yang cukup cepat:

X( X && move_semantics_are_cool ) : pImpl(NULL) {
    this->swap(move_semantics_are_cool);
}
X& swap( X& rhs ) {
    std::swap( pImpl, rhs.pImpl );
    return *this;
}
X& operator=( X && move_semantics_are_cool ) {
    return this->swap(move_semantics_are_cool);
}
X& operator=( const X& rhs ) {
    X temporary_copy(rhs);
    return this->swap(temporary_copy);
}

PS: Saya harap saya tidak salah paham soal semantik.

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.