Apa gunanya pola PImpl sementara kita bisa menggunakan antarmuka untuk tujuan yang sama di C ++?

49

Saya melihat banyak kode sumber yang menggunakan idiom PImpl di C ++. Saya berasumsi bahwa tujuannya adalah untuk menyembunyikan data / tipe / implementasi pribadi, sehingga dapat menghapus ketergantungan, dan kemudian mengurangi waktu kompilasi dan header menyertakan masalah.

Tetapi antarmuka / kelas abstrak murni di C ++ juga memiliki kemampuan ini, mereka juga dapat digunakan untuk menyembunyikan data / tipe / implementasi. Dan untuk membiarkan pemanggil melihat antarmuka saat membuat objek, kita dapat mendeklarasikan metode pabrik di header antarmuka.

Perbandingannya adalah:

  1. Biaya :

    Cara antarmuka biaya lebih rendah, karena Anda bahkan tidak perlu mengulangi implementasi fungsi public wrapper void Bar::doWork() { return m_impl->doWork(); }, Anda hanya perlu mendefinisikan tanda tangan di antarmuka.

  2. Dipahami dengan baik :

    Teknologi antarmuka lebih baik dipahami oleh setiap pengembang C ++.

  3. Kinerja :

    Cara kinerja antarmuka tidak lebih buruk daripada idiom PImpl, keduanya merupakan akses memori tambahan. Saya menganggap kinerjanya sama.

Berikut ini adalah kodesemu untuk mengilustrasikan pertanyaan saya:

// Forward declaration can help you avoid include BarImpl header, and those included in BarImpl header.
class BarImpl;
class Bar
{
public:
    // public functions
    void doWork();
private:
    // You don't need to compile Bar.cpp after changing the implementation in BarImpl.cpp
    BarImpl* m_impl;
};

Tujuan yang sama dapat diimplementasikan menggunakan antarmuka:

// Bar.h
class IBar
{
public:
    virtual ~IBar(){}
    // public functions
    virtual void doWork() = 0;
};

// to only expose the interface instead of class name to caller
IBar* createObject();

Jadi apa gunanya PImpl?

ZijingWu
sumber

Jawaban:

50

Pertama, PImpl biasanya digunakan untuk kelas non-polimorfik. Dan ketika kelas polimorfik memiliki PImpl, biasanya tetap polimorfik, yang masih mengimplementasikan antarmuka dan mengesampingkan metode virtual dari kelas dasar dan sebagainya. Jadi implementasi yang lebih sederhana dari PImpl bukan antarmuka, ini adalah kelas sederhana yang secara langsung berisi anggota!

Ada tiga alasan untuk menggunakan PImpl:

  1. Membuat antarmuka biner (ABI) independen dari anggota pribadi. Dimungkinkan untuk memperbarui perpustakaan bersama tanpa mengkompilasi ulang kode dependen, tetapi hanya selama antarmuka biner tetap sama. Sekarang hampir semua perubahan dalam header, kecuali untuk menambahkan fungsi non-anggota dan menambahkan fungsi non-anggota virtual, mengubah ABI. Ungkapan PImpl memindahkan definisi dari anggota pribadi ke sumber dan dengan demikian memisahkan ABI dari definisi mereka. Lihat Masalah Antarmuka Biner yang Rapuh

  2. Saat tajuk berubah, semua sumber termasuk yang harus dikompilasi ulang. Dan kompilasi C ++ agak lambat. Jadi dengan memindahkan definisi anggota pribadi ke sumber, idiom PImpl mengurangi waktu kompilasi, karena lebih sedikit dependensi perlu ditarik di header, dan mengurangi waktu kompilasi setelah modifikasi bahkan lebih karena tanggungan tidak perlu dikompilasi ulang (ok, ini berlaku untuk antarmuka + fungsi pabrik dengan kelas beton tersembunyi juga).

  3. Untuk banyak kelas dalam C ++ pengecualian keamanan adalah properti yang penting. Seringkali Anda perlu membuat beberapa kelas dalam satu sehingga jika selama operasi pada lebih dari satu anggota melempar, tidak ada anggota yang dimodifikasi atau Anda memiliki operasi yang akan membuat anggota dalam keadaan tidak konsisten jika melempar dan Anda memerlukan objek yang berisi untuk tetap konsisten. Dalam kasus seperti itu, Anda menerapkan operasi dengan membuat instance PImpl baru dan menukar mereka ketika operasi berhasil.

Sebenarnya antarmuka juga dapat digunakan untuk implementasi bersembunyi saja, tetapi memiliki kelemahan sebagai berikut:

  1. Menambahkan metode non-virtual tidak merusak ABI, tetapi menambahkan metode virtual tidak. Antarmuka karena itu tidak memungkinkan menambahkan metode sama sekali, PImpl tidak.

  2. Inteface hanya dapat digunakan melalui pointer / referensi, sehingga pengguna harus mengurus manajemen sumber daya yang tepat. Di sisi lain, kelas yang menggunakan PImpl masih merupakan tipe nilai dan menangani sumber daya secara internal.

  3. Implementasi tersembunyi tidak dapat diwarisi, kelas dengan PImpl dapat.

Dan tentu saja antarmuka tidak akan membantu dengan keamanan pengecualian. Anda perlu tipuan di dalam kelas untuk itu.

Jan Hudec
sumber
Poin bagus. Menambahkan fungsi publik di Implkelas tidak akan merusak ABI, tetapi menambahkan fungsi publik di antarmuka akan merusak ABI.
ZijingWu
1
Menambahkan fungsi / metode publik tidak harus merusak ABI; perubahan aditif ketat tidak melanggar perubahan. Namun, Anda harus sangat berhati-hati; kode yang memediasi antara front-end dan back-end harus berurusan dengan segala macam kesenangan dengan versi. Semuanya menjadi rumit. (Hal semacam ini adalah mengapa sejumlah proyek perangkat lunak lebih suka C ke C ++; ini memungkinkan mereka mendapatkan pegangan yang lebih ketat tentang apa sebenarnya ABI itu.)
Donal Fellows
3
@DonalFellows: Menambahkan fungsi / metode publik tidak merusak ABI. Menambahkan metode virtual dapat dan selalu demikian kecuali jika pengguna dicegah mewarisi objek (yang dicapai oleh PImpl).
Jan Hudec
4
Untuk memperjelas mengapa menambahkan metode virtual mematahkan ABI. Itu ada hubungannya dengan tautan. Menautkan ke metode non-virtual adalah dengan nama, terlepas dari apakah perpustakaan itu statis atau dinamis. Menambahkan metode non-virtual lainnya hanya dengan menambahkan nama lain untuk ditautkan. Namun metode virtual adalah indeks dalam vtable, dan kode eksternal secara efektif menghubungkan dengan indeks. Menambahkan metode virtual dapat memindahkan metode lain di dalam vtable tanpa mengetahui kode eksternal.
SnakE
1
PImpl juga mengurangi waktu kompilasi awal. Sebagai contoh, jika implementasi saya memiliki bidang beberapa jenis template (mis. unordered_set), Tanpa PImpl, saya harus #include <unordered_set>masuk MyClass.h. Dengan PImpl, saya hanya perlu memasukkannya ke dalam .cppfile, bukan .hfile, dan karena itu segala sesuatu yang memasukkannya ...
Martin C. Martin
7

Saya hanya ingin membahas poin kinerja Anda. Jika Anda menggunakan antarmuka, Anda harus telah membuat fungsi virtual yang TIDAK AKAN diuraikan oleh pengoptimal kompiler. Fungsi PIMPL dapat (dan mungkin akan, karena mereka sangat pendek) digarisbawahi (mungkin lebih dari sekali jika fungsi IMPL juga kecil). Panggilan fungsi virtual tidak dapat dioptimalkan kecuali Anda menggunakan optimasi analisis program lengkap yang membutuhkan waktu sangat lama.

Jika kelas PIMPL Anda tidak digunakan dengan cara yang kritis terhadap kinerja, maka poin ini tidak terlalu menjadi masalah, tetapi asumsi Anda bahwa kinerjanya sama hanya berlaku di beberapa situasi, tidak semua.

Chumby Gumball
sumber
1
Dengan menggunakan optimasi tautan-waktu, sebagian besar overhead menggunakan idiom jerawat dihapus. Baik fungsi outter wrapper maupun fungsi implementasi dapat digarisbawahi, membuat fungsi panggilan secara efektif seperti kelas normal yang diimplementasikan di dalam header.
goji
Ini hanya benar jika Anda menggunakan optimasi waktu tautan. Maksud dari PImpl adalah bahwa kompiler tidak memiliki implementasi, bahkan dari fungsi penerusan. Tautan tidak.
Martin C. Martin
5

Halaman ini menjawab pertanyaan Anda. Banyak orang setuju dengan pernyataan Anda. Menggunakan Pimpl memiliki keunggulan berikut di atas bidang / anggota pribadi.

  1. Mengubah variabel anggota pribadi suatu kelas tidak memerlukan mengkompilasi ulang kelas yang bergantung padanya, sehingga membuat waktu lebih cepat, dan FragileBinaryInterfaceProblem berkurang.
  2. File header tidak perlu # mencantumkan kelas yang digunakan 'berdasarkan nilai' dalam variabel anggota pribadi, sehingga waktu kompilasi lebih cepat.

Ungkapan Pimpl adalah optimasi waktu kompilasi, teknik pemecahan ketergantungan. Ini memotong file header besar. Ini mengurangi header Anda menjadi hanya antarmuka publik. Panjang header penting dalam C ++ ketika Anda berpikir tentang cara kerjanya. #include secara efektif menggabungkan file header ke file sumber - jadi ingatlah setelah unit C ++ yang pra-diproses bisa sangat besar. Menggunakan Pimpl dapat meningkatkan waktu kompilasi.

Ada teknik pemecahan ketergantungan lain yang lebih baik - yang melibatkan peningkatan desain Anda. Apa yang diwakili metode pribadi? Apa tanggung jawab tunggal? Haruskah mereka menjadi kelas lain?

Dave Hillier
sumber
PImpl sama sekali bukan optimasi dalam hal run-time. Alokasi tidak sepele dan jerawat berarti alokasi tambahan. Ini adalah teknik pemecahan ketergantungan dan optimisasi waktu kompilasi saja.
Jan Hudec
@JanHudec diklarifikasi.
Dave Hillier
@DaveHillier, +1 untuk menyebutkan FragileBinaryInterfaceProblem
ZijingWu