Pimpl idiom vs antarmuka kelas virtual murni

118

Saya bertanya-tanya apa yang membuat seorang programmer memilih idiom Pimpl atau kelas virtual murni dan warisan.

Saya memahami bahwa idiom pimpl hadir dengan satu tipuan ekstra eksplisit untuk setiap metode publik dan overhead pembuatan objek.

Kelas virtual murni di sisi lain hadir dengan indireksi implisit (vtable) untuk implementasi yang mewarisi dan saya memahami bahwa tidak ada overhead pembuatan objek.
EDIT : Tetapi Anda membutuhkan pabrik jika Anda membuat objek dari luar

Apa yang membuat kelas virtual murni kurang diminati dibandingkan idiom pimpl?

Arkaitz Jimenez
sumber
3
Pertanyaan bagus, hanya ingin menanyakan hal yang sama. Lihat juga boost.org/doc/libs/1_41_0/libs/smart_ptr/sp_techniques.html
Frank

Jawaban:

60

Saat menulis kelas C ++, sebaiknya pikirkan apakah kelas itu akan dibuat

  1. Jenis Nilai

    Disalin berdasarkan nilai, identitas tidak pernah penting. Ini tepat untuk menjadi kunci dalam peta std ::. Contoh, kelas "string", atau kelas "tanggal", atau kelas "bilangan kompleks". Untuk "menyalin" contoh kelas seperti itu masuk akal.

  2. Jenis Entitas

    Identitas itu penting. Selalu diteruskan oleh referensi, tidak pernah dengan "nilai". Seringkali, tidak masuk akal untuk "menyalin" contoh kelas sama sekali. Jika memang masuk akal, metode "Klon" polimorfik biasanya lebih tepat. Contoh: Kelas Socket, kelas Database, kelas "kebijakan", apa pun yang akan menjadi "penutup" dalam bahasa fungsional.

Baik pImpl dan kelas dasar abstrak murni adalah teknik untuk mengurangi ketergantungan waktu kompilasi.

Namun, saya hanya pernah menggunakan pImpl untuk mengimplementasikan tipe Nilai (tipe 1), dan hanya kadang-kadang ketika saya benar-benar ingin meminimalkan ketergantungan kopling dan waktu kompilasi. Seringkali, itu tidak sepadan dengan repotnya. Seperti yang Anda tunjukkan dengan benar, ada lebih banyak overhead sintaksis karena Anda harus menulis metode penerusan untuk semua metode publik. Untuk kelas tipe 2, saya selalu menggunakan kelas dasar abstrak murni dengan metode pabrik terkait.

Paul Hollingsworth
sumber
6
Silakan lihat komentar Paul de Vrieze untuk jawaban ini . Pimpl dan Pure Virtual berbeda secara signifikan jika Anda berada di perpustakaan dan ingin menukar .so / .dll Anda tanpa membangun kembali klien. Klien menautkan ke bagian depan pimpl dengan nama sehingga menyimpan tanda tangan metode lama sudah cukup. OTOH dalam kasus abstrak murni mereka secara efektif ditautkan oleh indeks vtable sehingga metode penyusunan ulang atau penyisipan di tengah akan merusak kompatibilitas.
SnakE
1
Anda hanya dapat menambahkan (atau mengurutkan ulang) metode di front end kelas Pimpl untuk mempertahankan komparabilitas biner. Secara logika, Anda masih mengubah antarmuka dan tampaknya agak cerdik. Jawabannya di sini adalah keseimbangan yang masuk akal yang juga dapat membantu pengujian unit melalui "injeksi ketergantungan"; tetapi jawabannya selalu tergantung pada kebutuhan. Penulis Perpustakaan pihak ketiga (berbeda dari menggunakan perpustakaan di organisasi Anda sendiri) mungkin sangat menyukai Pimpl.
Spacen Jasset
31

Pointer to implementationbiasanya tentang menyembunyikan detail implementasi struktural. Interfacesadalah tentang memberi contoh penerapan yang berbeda. Mereka benar-benar melayani dua tujuan berbeda.

D. Shawley
sumber
13
belum tentu, saya telah melihat kelas yang menyimpan banyak jerawat tergantung pada implementasi yang diinginkan. Seringkali ini dikatakan sebuah impl win32 vs sebuah linux impl dari sesuatu yang perlu diimplementasikan secara berbeda per platform.
Doug T.
14
Tetapi Anda dapat menggunakan Antarmuka untuk memisahkan detail implementasi dan menyembunyikannya
Arkaitz Jimenez
6
Meskipun Anda dapat mengimplementasikan pimpl menggunakan antarmuka, sering kali tidak ada alasan untuk memisahkan detail implementasi. Jadi tidak ada alasan untuk menjadi polimorfik. The Alasan untuk pimpl adalah untuk menjaga rincian pelaksanaan jauh dari klien (dalam C ++ untuk menjaga mereka dari header). Anda mungkin melakukan ini menggunakan basis / antarmuka abstrak, tetapi umumnya itu berlebihan.
Michael Burr
10
Mengapa ini berlebihan? Maksud saya, apakah ini lebih lambat metode antarmuka daripada yang pimpl? Mungkin ada alasan logis, tetapi dari sudut pandang praktis saya akan mengatakan lebih mudah melakukannya dengan antarmuka abstrak
Arkaitz Jimenez
1
Menurut saya, kelas / antarmuka dasar abstrak adalah cara "normal" untuk melakukan sesuatu dan memungkinkan pengujian yang lebih mudah melalui
mocking
28

Idiom pimpl membantu Anda mengurangi dependensi dan waktu build terutama dalam aplikasi besar, dan meminimalkan eksposur header dari detail implementasi kelas Anda ke satu unit kompilasi. Pengguna kelas Anda bahkan tidak perlu menyadari keberadaan jerawat (kecuali sebagai penunjuk samar yang tidak mereka ketahui!).

Kelas abstrak (virtual murni) adalah sesuatu yang harus diperhatikan klien Anda: jika Anda mencoba menggunakannya untuk mengurangi referensi penggandengan dan melingkar, Anda perlu menambahkan beberapa cara untuk memungkinkan mereka membuat objek (misalnya melalui metode pabrik atau kelas, injeksi ketergantungan atau mekanisme lainnya).

Pontus Gagge
sumber
17

Saya sedang mencari jawaban untuk pertanyaan yang sama. Setelah membaca beberapa artikel dan beberapa latihan, saya lebih suka menggunakan "antarmuka kelas virtual murni" .

  1. Mereka lebih lurus ke depan (ini adalah pendapat subjektif). Idiom Pimpl membuat saya merasa sedang menulis kode "untuk kompiler", bukan untuk "pengembang berikutnya" yang akan membaca kode saya.
  2. Beberapa framework pengujian memiliki dukungan langsung untuk Mocking kelas virtual murni
  3. Memang benar Anda membutuhkan pabrik agar dapat diakses dari luar. Tetapi jika Anda ingin memanfaatkan polimorfisme: itu juga "pro", bukan "kontra". ... dan metode pabrik yang sederhana tidak terlalu menyakitkan

Satu-satunya kelemahan ( saya mencoba untuk menyelidiki ini ) adalah idiom pimpl bisa lebih cepat

  1. ketika proxy-call disisipkan, sementara mewarisi perlu akses ekstra ke objek VTABLE saat runtime
  2. footprint memori pimpl public-proxy-class lebih kecil (Anda dapat melakukan pengoptimalan dengan mudah untuk swap yang lebih cepat dan pengoptimalan serupa lainnya)
Ilias Bartolini
sumber
21
Ingat juga bahwa dengan menggunakan pewarisan Anda memperkenalkan ketergantungan pada tata letak vtable. Untuk mempertahankan ABI, Anda tidak dapat lagi mengubah fungsi virtual (menambahkan di bagian akhir cukup aman, jika tidak ada kelas anak yang menambahkan metode virtualnya sendiri).
Paul de Vrieze
1
^ Komentar di sini harus lengket.
CodeAngry
10

Saya benci jerawat! Mereka mengerjakan kelas dengan jelek dan tidak bisa dibaca. Semua metode diarahkan ke jerawat. Anda tidak pernah melihat di header, fungsionalitas apa yang memiliki kelas tersebut, sehingga Anda tidak dapat memfaktorkan ulangnya (misalnya, cukup ubah visibilitas metode). Kelas terasa seperti "hamil". Saya pikir menggunakan iterfaces lebih baik dan benar-benar cukup untuk menyembunyikan implementasi dari klien. Anda dapat membiarkan satu kelas menerapkan beberapa antarmuka untuk menahannya. Seseorang harus lebih memilih antarmuka! Catatan: Anda tidak perlu kelas pabrik. Relevan adalah bahwa klien kelas berkomunikasi dengan instansinya melalui antarmuka yang sesuai. Menyembunyikan metode pribadi yang saya temukan sebagai paranoia yang aneh dan tidak melihat alasan untuk ini karena kami memiliki antarmuka.

Masha Ananieva
sumber
1
Ada beberapa kasus ketika Anda tidak dapat menggunakan antarmuka virtual murni. Misalnya ketika Anda memiliki beberapa kode lama dan Anda memiliki dua modul yang perlu Anda pisahkan tanpa menyentuhnya.
AlexTheo
Seperti yang ditunjukkan @Paul de Vrieze di bawah ini, Anda kehilangan kompatibilitas ABI saat mengubah metode kelas dasar, karena Anda memiliki ketergantungan implisit pada vtabel kelas. Itu tergantung pada kasus penggunaan, apakah ini menjadi masalah.
H. Rittich
"Penyembunyian metode privat yang saya temukan sebagai paranoia aneh" Bukankah itu memungkinkan Anda untuk menyembunyikan dependensi dan karenanya meminimalkan waktu kompilasi jika dependensi berubah?
pooya13
Saya juga tidak mengerti bagaimana Pabrik lebih mudah difaktorkan ulang daripada pImpl. Tidakkah Anda meninggalkan "antarmuka" dalam kedua kasus dan mengubah penerapan? Di Pabrik Anda harus memodifikasi satu file .h dan satu .cpp dan di pImpl Anda harus memodifikasi satu .h dan dua file .cpp tetapi itu saja dan Anda biasanya tidak perlu memodifikasi file cpp dari antarmuka pImpl.
pooya13
8

Ada masalah yang sangat nyata dengan pustaka bersama yang dielakkan oleh idiom pimpl dengan rapi yang tidak dapat dilakukan oleh virtual murni: Anda tidak dapat dengan aman mengubah / menghapus anggota data kelas tanpa memaksa pengguna kelas untuk mengkompilasi ulang kode mereka. Itu mungkin dapat diterima dalam beberapa keadaan, tetapi tidak misalnya untuk perpustakaan sistem.

Untuk menjelaskan masalah secara mendetail, pertimbangkan kode berikut di pustaka / header bersama Anda:

// header
struct A
{
public:
  A();
  // more public interface, some of which uses the int below
private:
  int a;
};

// library 
A::A()
  : a(0)
{}

Kompilator memancarkan kode di pustaka bersama yang menghitung alamat integer yang akan diinisialisasi menjadi offset tertentu (mungkin nol dalam kasus ini, karena itu satu-satunya anggota) dari penunjuk ke objek A yang diketahuinya this.

Di sisi pengguna kode, new Apertama-tama akan mengalokasikan sizeof(A)byte memori, kemudian menyerahkan pointer ke memori itu ke A::A()konstruktor sebagaithis .

Jika dalam revisi perpustakaan Anda nanti Anda memutuskan untuk melepaskan bilangan bulat, membuatnya lebih besar, lebih kecil, atau menambahkan anggota, akan ada ketidakcocokan antara jumlah alokasi kode memori pengguna, dan offset yang diharapkan kode konstruktor. Kemungkinan akibatnya adalah crash, jika Anda beruntung - jika Anda kurang beruntung, perangkat lunak Anda berperilaku aneh.

Dengan pimpl'ing, Anda dapat dengan aman menambah dan menghapus anggota data ke kelas dalam, karena alokasi memori dan panggilan konstruktor terjadi di pustaka bersama:

// header
struct A
{
public:
  A();
  // more public interface, all of which delegates to the impl
private:
  void * impl;
};

// library 
A::A()
  : impl(new A_impl())
{}

Yang perlu Anda lakukan sekarang adalah menjaga antarmuka publik Anda bebas dari anggota data selain penunjuk ke objek implementasi, dan Anda aman dari kelas kesalahan ini.

Sunting: Saya mungkin harus menambahkan bahwa satu-satunya alasan saya berbicara tentang konstruktor di sini adalah karena saya tidak ingin memberikan lebih banyak kode - argumen yang sama berlaku untuk semua fungsi yang mengakses anggota data.


sumber
4
Alih-alih void *, saya pikir lebih tradisional untuk meneruskan deklarasi kelas pelaksana:class A_impl *impl_;
Frank Krueger
9
Saya tidak mengerti, Anda tidak boleh mendeklarasikan anggota pribadi di kelas murni virtual yang ingin Anda gunakan sebagai antarmuka, idenya adalah untuk menjaga agar kelas tetap abstrak, tidak ada ukuran, hanya metode virtual murni, saya tidak melihat apa pun Anda tidak dapat melakukannya melalui perpustakaan bersama
Arkaitz Jimenez
@ Frank Krueger: Anda benar, saya sedang malas. @Arkaitz Jimenez: Sedikit kesalahpahaman; jika Anda memiliki kelas yang hanya berisi fungsi virtual murni, maka tidak ada gunanya membicarakan perpustakaan bersama. Di sisi lain, jika Anda berurusan dengan perpustakaan bersama, melakukan pimpl'ing kelas umum Anda bisa menjadi bijaksana untuk alasan yang diuraikan di atas.
10
Ini tidak benar. Kedua metode memungkinkan Anda menyembunyikan status implementasi kelas Anda, jika Anda membuat kelas lain menjadi kelas "basis abstrak murni".
Paul Hollingsworth
10
Kalimat pertama di anser Anda menyiratkan bahwa virtual murni dengan metode pabrik terkait entah bagaimana tidak membiarkan Anda menyembunyikan status internal kelas. Itu tidak benar. Kedua teknik memungkinkan Anda menyembunyikan status internal kelas. Perbedaannya adalah tampilannya bagi pengguna. pImpl memungkinkan Anda untuk tetap mewakili kelas dengan semantik nilai namun juga menyembunyikan status internal. Metode pabrik Kelas Dasar Abstrak Murni + memungkinkan Anda untuk merepresentasikan tipe entitas, dan juga memungkinkan Anda menyembunyikan status internal. Yang terakhir adalah bagaimana COM bekerja. Bab 1 dari "Essential COM" membahas hal ini dengan sangat jelas.
Paul Hollingsworth
6

Kita tidak boleh lupa bahwa warisan lebih kuat, lebih erat daripada pendelegasian. Saya juga akan mempertimbangkan semua masalah yang diangkat dalam jawaban yang diberikan saat memutuskan idiom desain apa yang akan digunakan dalam memecahkan masalah tertentu.

Sam
sumber
3

Meskipun secara luas tercakup dalam jawaban lain mungkin saya bisa sedikit lebih eksplisit tentang satu manfaat pimpl daripada kelas basis virtual:

Pendekatan pimpl transparan dari sudut pandang pengguna, artinya Anda dapat, misalnya, membuat objek kelas pada tumpukan dan menggunakannya secara langsung dalam wadah. Jika Anda mencoba menyembunyikan implementasi menggunakan kelas dasar virtual abstrak, Anda perlu mengembalikan penunjuk bersama ke kelas dasar dari pabrik, yang memperumit penggunaannya. Pertimbangkan kode klien yang setara berikut ini:

// Pimpl
Object pi_obj(10);
std::cout << pi_obj.SomeFun1();

std::vector<Object> objs;
objs.emplace_back(3);
objs.emplace_back(4);
objs.emplace_back(5);
for (auto& o : objs)
    std::cout << o.SomeFun1();

// Abstract Base Class
auto abc_obj = ObjectABC::CreateObject(20);
std::cout << abc_obj->SomeFun1();

std::vector<std::shared_ptr<ObjectABC>> objs2;
objs2.push_back(ObjectABC::CreateObject(13));
objs2.push_back(ObjectABC::CreateObject(14));
objs2.push_back(ObjectABC::CreateObject(15));
for (auto& o : objs2)
    std::cout << o->SomeFun1();
Superfly Jon
sumber
2

Dalam pemahaman saya, kedua hal ini memiliki tujuan yang sangat berbeda. Tujuan dari idiom jerawat pada dasarnya adalah memberi Anda pegangan untuk penerapan Anda sehingga Anda dapat melakukan hal-hal seperti pertukaran cepat untuk semacamnya.

Tujuan dari kelas virtual lebih pada garis memungkinkan polimorfisme, yaitu Anda memiliki pointer yang tidak diketahui ke objek dari tipe turunan dan ketika Anda memanggil fungsi x Anda selalu mendapatkan fungsi yang tepat untuk kelas apa pun yang sebenarnya ditunjuk oleh pointer dasar.

Apel dan jeruk banget.

Robert S. Barnes
sumber
Saya setuju dengan apel / jeruk. Tetapi tampaknya Anda menggunakan pImpl untuk sebuah fungsional. Tujuan saya sebagian besar adalah membangun-teknis dan menyembunyikan informasi.
xtofl
2

Masalah paling menjengkelkan tentang idiom pimpl adalah membuatnya sangat sulit untuk memelihara dan menganalisis kode yang ada. Jadi menggunakan pimpl Anda membayar dengan waktu pengembang dan frustrasi hanya untuk "mengurangi ketergantungan dan waktu build dan meminimalkan eksposur header dari detail implementasi". Putuskan sendiri, apakah itu sangat berharga.

Terutama "waktu pembuatan" adalah masalah yang dapat Anda selesaikan dengan perangkat keras yang lebih baik atau menggunakan alat seperti Incredibuild (www.incredibuild.com, juga sudah termasuk dalam Visual Studio 2017), sehingga tidak memengaruhi desain perangkat lunak Anda. Desain perangkat lunak umumnya harus independen dari cara perangkat lunak dibuat.

Trantor
sumber
Anda juga membayar dengan waktu pengembang ketika waktu pembuatan adalah 20 menit, bukan 2, jadi ini sedikit seimbang, sistem modul nyata akan banyak membantu di sini.
Arkaitz Jimenez
IMHO, cara pembuatan perangkat lunak seharusnya tidak mempengaruhi desain internal sama sekali. Ini adalah masalah yang sama sekali berbeda.
Trantor
2
Apa yang membuatnya sulit untuk dianalisis? Sekelompok panggilan dalam file implementasi yang diteruskan ke kelas Impl tidak terdengar sulit.
mabraham
2
Bayangkan implementasi debugging di mana pimpl dan antarmuka digunakan. Mulai dari panggilan dalam kode pengguna A, Anda melacak ke antarmuka B, melompat ke kelas C pimpled untuk akhirnya mulai debugging kelas implementasi D ... Empat langkah sampai Anda dapat menganalisis apa yang sebenarnya terjadi. Dan jika semuanya diimplementasikan dalam DLL Anda mungkin akan menemukan antarmuka C di suatu tempat di antara ....
Trantor
Mengapa Anda menggunakan antarmuka dengan pImpl ketika pImpl juga dapat melakukan tugas antarmuka? (yaitu dapat membantu Anda mencapai inversi ketergantungan)
pooya13