Pimpl deyimi ile Saf sanal sınıf arayüzü


118

Bir programcıyı Pimpl deyimini ya da saf sanal sınıfı ve kalıtımı seçmeye ne yönelteceğini merak ediyordum.

Pimpl deyiminin her bir genel yöntem ve nesne oluşturma ek yükü için açık bir ek dolaylama ile geldiğini anlıyorum.

Öte yandan, Pure sanal sınıf, kalıtımsal uygulama için örtük indireksiyon (vtable) ile birlikte gelir ve hiçbir nesne oluşturma ek yükü olmadığını anlıyorum.
DÜZENLEME : Ancak nesneyi dışarıdan yaratırsanız bir fabrikaya ihtiyacınız olacak

Pure virtual class'ı pezevenk deyiminden daha az çekici yapan nedir?


3
Harika soru, sadece aynı şeyi sormak istedim. Ayrıca bkz. Boost.org/doc/libs/1_41_0/libs/smart_ptr/sp_techniques.html
Frank

Yanıtlar:


60

Bir C ++ sınıfı yazarken, bunun

  1. Bir Değer Türü

    Değere göre kopya, kimlik asla önemli değildir. Bir std :: map içinde anahtar olması uygundur. Örnek, bir "dize" sınıfı veya bir "tarih" sınıfı veya bir "karmaşık sayı" sınıfı. Böyle bir sınıfın örneklerini "kopyalamak" mantıklıdır.

  2. Bir Varlık türü

    Kimlik önemlidir. Her zaman referansla aktarılır, asla "değer" ile aktarılmaz. Genellikle, sınıfın örneklerini "kopyalamak" hiç mantıklı gelmez. Mantıklı olduğu zaman, polimorfik bir "Klon" yöntemi genellikle daha uygundur. Örnekler: Bir Soket sınıfı, bir Veritabanı sınıfı, bir "politika" sınıfı, işlevsel bir dilde "kapanış" olabilecek herhangi bir şey.

Hem pImpl hem de saf soyut temel sınıf, derleme zamanı bağımlılıklarını azaltmaya yönelik tekniklerdir.

Ancak, Değer türlerini (tür 1) uygulamak için yalnızca pImpl kullanıyorum ve yalnızca bazen birleştirme ve derleme zamanı bağımlılıklarını en aza indirmek istediğimde. Çoğu zaman, zahmete değmez. Sizin de doğru bir şekilde işaret ettiğiniz gibi, daha fazla sözdizimsel ek yük vardır çünkü tüm genel yöntemler için yönlendirme yöntemleri yazmanız gerekir. Tip 2 sınıfları için, her zaman ilişkili fabrika yöntem (ler) i ile saf bir soyut temel sınıf kullanırım.


6
Lütfen Paul de Vrieze'nin bu cevaba yaptığı yoruma bakınız . Bir kitaplıktaysanız ve istemciyi yeniden oluşturmadan .so / .dll dosyanızı değiştirmek istiyorsanız Pimpl ve Pure Virtual önemli ölçüde farklılık gösterir. İstemciler pimpl ön uçlarına isme göre bağlanırlar, bu nedenle eski yöntem imzalarını saklamak yeterlidir. OTOH saf soyut durumda, vtable indeksi ile etkin bir şekilde bağlanırlar, böylece yöntemleri yeniden düzenlemek veya ortasına eklemek uyumluluğu bozacaktır.
Yılan

1
İkili karşılaştırılabilirliği korumak için yalnızca bir Pimpl sınıfı ön ucuna yöntemler ekleyebilir (veya yeniden sıralayabilirsiniz). Mantıksal olarak konuşursak, arayüzü hala değiştirdiniz ve biraz tehlikeli görünüyor. Buradaki cevap, "bağımlılık ekleme" yoluyla birim testine de yardımcı olabilecek mantıklı bir dengedir; ancak cevap her zaman gereksinimlere bağlıdır. Üçüncü taraf Kütüphane yazarları (kendi kuruluşunuzdaki bir kütüphaneyi kullanmaktan farklı olarak), Pimpl'i büyük ölçüde tercih edebilir.
Spacen Jasset

31

Pointer to implementationgenellikle yapısal uygulama ayrıntılarını gizlemekle ilgilidir. Interfacesfarklı uygulamaları örneklemekle ilgilidir. Gerçekten iki farklı amaca hizmet ediyorlar.


13
değil, istenen uygulamaya bağlı olarak birden fazla sivilce depolayan sınıflar gördüm. Genellikle bu, her platform için farklı şekilde uygulanması gereken bir şeyin linux impl'ine karşı bir win32 impl olduğunu söyler.
Doug T.

14
Ancak uygulama ayrıntılarını ayırmak ve gizlemek için bir Arayüz kullanabilirsiniz
Arkaitz Jimenez

6
Bir arabirim kullanarak pimpl uygulayabilirsiniz, ancak genellikle uygulama ayrıntılarını ayırmak için hiçbir neden yoktur. Yani polimorfik olmaya gerek yok. Sebebi Pimpl için uzak istemciden uygulama ayrıntılarını tutmak için (C ++ başlığının onları dışarıda tutmak için) 'dir. Bunu soyut bir temel / arayüz kullanarak yapabilirsiniz, ancak bu genellikle gereksizdir.
Michael Burr

10
Neden abartılı? Demek istediğim, arayüz yöntemi pezevenk yönteminden daha mı yavaş? Mantıklı nedenler olabilir, ancak pratik açıdan soyut bir arayüzle yapmanın daha kolay olduğunu söyleyebilirim
Arkaitz Jimenez

1
Soyut temel sınıfın / arayüzün işleri yapmanın "normal" yolu olduğunu ve alay yoluyla daha kolay test yapılmasına izin verdiğini
söyleyebilirim

28

Pimpl deyimi, özellikle büyük uygulamalarda derleme bağımlılıklarını ve süreleri azaltmanıza yardımcı olur ve sınıfınızın uygulama ayrıntılarının tek bir derleme birimine maruz kalmasını en aza indirir. Sınıfınızın kullanıcılarının bir sivilcenin varlığından haberdar olmaları bile gerekmez (gizli olmadıkları şifreli bir işaretçi olması dışında!).

Soyut sınıflar (saf sanallar), müşterilerinizin farkında olması gereken bir şeydir: Birleştirme ve döngüsel referansları azaltmak için bunları kullanmaya çalışırsanız, nesnelerinizi oluşturmalarına izin vermenin bir yolunu eklemeniz gerekir (örneğin, fabrika yöntemleri veya sınıfları aracılığıyla, bağımlılık enjeksiyonu veya diğer mekanizmalar).


17

Aynı soru için bir cevap arıyordum. Bazı makaleleri okuduktan ve biraz alıştırma yaptıktan sonra "Saf sanal sınıf arayüzleri" kullanmayı tercih ediyorum .

  1. Daha yalındırlar (bu öznel bir görüştür). Pimpl deyimi, kodumu okuyacak olan "sonraki geliştirici" için değil, "derleyici için" kod yazdığımı hissettiriyor.
  2. Bazı test çerçeveleri, Mocking saf sanal sınıfları için doğrudan desteğe sahiptir
  3. Dışarıdan erişilebilmesi için bir fabrikaya ihtiyacınız olduğu doğru . Ama polimorfizmden yararlanmak istiyorsanız: bu aynı zamanda "pro", "aleyhte" değil. ... ve basit bir fabrika yöntemi gerçekten çok acıtmaz

Tek dezavantajı ( bunu araştırmaya çalışıyorum ) pimpl deyiminin daha hızlı olabilmesidir.

  1. proxy çağrıları satır içi olduğunda, miras alma, çalışma zamanında VTABLE nesnesine ek bir erişime ihtiyaç duyduğunda
  2. bellek ayak izi pimpl public-proxy-sınıfı daha küçüktür (daha hızlı takas ve diğer benzer optimizasyonlar için kolayca optimizasyonlar yapabilirsiniz)

21
Ayrıca, mirası kullanarak vtable düzenine bir bağımlılık getireceğinizi unutmayın. ABI'yı korumak için artık sanal işlevleri değiştiremezsiniz (kendi sanal yöntemlerini ekleyen alt sınıflar yoksa sonuna eklemek güvenlidir).
Paul de Vrieze

1
^ Buradaki bu yorum yapışkan olmalıdır.
CodeAngry

10

Sivilcelerden nefret ederim! Sınıfı çirkin yapıyorlar ve okunaklı değiller. Tüm yöntemler sivilceye yönlendirilir. Hiçbir zaman başlıklarda, hangi işlevlerin sınıfa sahip olduğunu görmezsiniz, bu nedenle onu yeniden düzenleyemezsiniz (örneğin, basitçe bir yöntemin görünürlüğünü değiştirebilirsiniz). Sınıf "hamile" gibi geliyor. Bence arabirim kullanmak, uygulamayı istemciden gizlemek için daha iyi ve gerçekten yeterli. Bir sınıfın onları zayıf tutmak için birkaç arabirim uygulamasına izin verebilirsiniz. Arayüzleri tercih etmeli! Not: Fabrika sınıfına ihtiyacınız yoktur. Önemli olan, sınıf istemcilerinin örnekleriyle uygun arabirim aracılığıyla iletişim kurmasıdır. Özel yöntemlerin saklanmasını garip bir paranoya olarak buluyorum ve arayüzümüz olduğu için bunun nedenini görmüyorum.


1
Saf sanal arabirimleri kullanamayacağınız bazı durumlar vardır. Örneğin, eski bir kodunuz olduğunda ve onlara dokunmadan ayırmanız gereken iki modülünüz olduğunda.
AlexTheo

@Paul de Vrieze aşağıda belirtildiği gibi, temel sınıfın yöntemlerini değiştirirken ABI uyumluluğunu kaybedersiniz, çünkü sınıfın vtable'ına örtük bir bağımlılığınız vardır. Bunun bir sorun olup olmadığı, kullanım durumuna bağlıdır.
H. Rittich

"Garip bir paranoya olarak bulduğum özel yöntemlerin gizlenmesi" Bu, bağımlılıkları gizlemenize ve dolayısıyla bir bağımlılık değişirse derleme süresini en aza indirmenize izin vermiyor mu?
pooya13

Ayrıca Fabrikaların yeniden faktörlendirmenin pImpl'den daha kolay olduğunu da anlamıyorum. Her iki durumda da "arayüzü" terk edip uygulamayı değiştirmiyor musunuz? Fabrikada bir .h ve bir .cpp dosyasını değiştirmeniz gerekir ve pImpl'de bir .h ve iki .cpp dosyasını değiştirmeniz gerekir, ancak bununla ilgili ve genellikle pImpl arayüzünün cpp dosyasını değiştirmenize gerek yoktur.
pooya13

8

Paylaşılan kitaplıklarda, pimpl deyiminin saf sanalların yapamayacağı kadar düzgün bir şekilde atlattığı çok gerçek bir sorun var: sınıfın kullanıcılarını kodlarını yeniden derlemeye zorlamadan bir sınıfın veri üyelerini güvenli bir şekilde değiştiremez / kaldıramazsınız. Bu, bazı durumlarda kabul edilebilir, ancak örneğin sistem kitaplıkları için geçerli değildir.

Sorunu ayrıntılı olarak açıklamak için, paylaşılan kitaplığınızda / başlığınızda aşağıdaki kodu göz önünde bulundurun:

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

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

Derleyici, işaretleyiciden olduğunu bildiği A nesnesine belirli bir uzaklık (bu durumda tek üye olduğu için muhtemelen sıfırdır) olarak başlatılacak tamsayının adresini hesaplayan paylaşılan kitaplıkta kod yayar this.

Kod kullanıcı tarafında, bir new Ailk tahsis edecek sizeof(A)daha sonra bellek için bir işaretçi el, bellek bayt A::A()olarak kurucu this.

Kitaplığınızın daha sonraki bir revizyonunda tamsayıyı kaldırmaya, büyütmeye, küçültmeye veya üye eklemeye karar verirseniz, kullanıcının kodunun ayırdığı bellek miktarı ile yapıcı kodunun beklediği uzaklıklar arasında bir uyumsuzluk olacaktır. Muhtemel sonuç bir çökmedir, eğer şanslıysanız - daha az şanslıysanız, yazılımınız tuhaf davranır.

Pimpl'ing ile, paylaşılan kitaplıkta bellek ayırma ve yapıcı çağrısı gerçekleştiğinden, iç sınıfa veri üyelerini güvenle ekleyebilir ve kaldırabilirsiniz:

// 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())
{}

Şimdi yapmanız gereken tek şey, genel arabiriminizi uygulama nesnesine yönelik işaretçi dışında veri üyelerinden uzak tutmaktır ve bu hata sınıfından kurtulursunuz.

Düzenleme: Burada kurucudan bahsetmemin tek sebebinin daha fazla kod sağlamak istememem olduğunu eklemeliyim - aynı argümantasyon veri üyelerine erişen tüm işlevler için geçerlidir.


4
Void * yerine, uygulayıcı sınıfı ilan etmenin daha geleneksel olduğunu düşünüyorum:class A_impl *impl_;
Frank Krueger

9
Anlamıyorum, arayüz olarak kullanmayı düşündüğünüz sanal saf sınıfta özel üyeler ilan etmemelisiniz, fikir sınıfı esasen soyut tutmaktır, boyutsuz, sadece saf sanal yöntemler, hiçbir şey görmüyorum paylaşımlı kütüphaneler üzerinden yapamazsınız
Arkaitz Jimenez

@Frank Krueger: Haklısın, tembel davrandım. @Arkaitz Jimenez: Küçük bir yanlış anlama; Yalnızca saf sanal işlevler içeren bir sınıfınız varsa, paylaşılan kitaplıklar hakkında konuşmanın pek bir anlamı yoktur. Öte yandan, eğer paylaşılan kütüphanelerle uğraşıyorsanız, kamuya açık sınıflarınızı pezevenkleştirmek, yukarıda özetlenen sebepten dolayı akıllıca olabilir.

10
Bu sadece yanlış. Diğer sınıfınızı "saf soyut temel" sınıf yaparsanız, her iki yöntem de sınıflarınızın uygulama durumunu gizlemenize izin verir.
Paul Hollingsworth

10
Yanıtlayıcınızdaki ilk cümle, ilişkili bir fabrika yöntemine sahip saf sanalların bir şekilde sınıfın iç durumunu gizlemenize izin vermediğini ima eder. Bu doğru değil. Her iki teknik de sınıfın iç durumunu gizlemenize izin verir. Aradaki fark, kullanıcıya nasıl göründüğüdür. pImpl, bir sınıfı değer semantiği ile temsil etmenize ve aynı zamanda dahili durumu gizlemenize izin verir. Saf Soyut Temel Sınıf + fabrika yöntemi, varlık türlerini temsil etmenize ve ayrıca dahili durumu gizlemenize olanak tanır. İkincisi, COM'un tam olarak nasıl çalıştığıdır. "Essential COM" un 1. Bölümünde bu konuda büyük bir tartışma var.
Paul Hollingsworth

6

Mirasın, yetkilendirmeden daha güçlü, daha yakın bir bağlantı olduğunu unutmamalıyız. Belirli bir problemin çözümünde hangi tasarım deyimlerinin kullanılacağına karar verirken verilen cevaplarda ortaya çıkan tüm sorunları da hesaba katardım.


3

Diğer cevaplarda geniş bir şekilde ele alınsa da, sanal temel sınıflara göre pimpl'in bir avantajı konusunda biraz daha açık olabilirim:

Bir pimpl yaklaşımı, kullanıcı bakış açısından şeffaftır, yani yığın üzerinde sınıfın nesnelerini oluşturabilir ve bunları doğrudan konteynerlerde kullanabilirsiniz. Uygulamayı soyut bir sanal temel sınıf kullanarak gizlemeye çalışırsanız, fabrikadan temel sınıfa paylaşılan bir işaretçi döndürmeniz gerekir, bu da kullanımını karmaşıklaştırır. Aşağıdaki eşdeğer müşteri kodunu göz önünde bulundurun:

// 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();

2

Benim anlayışıma göre bu iki şey tamamen farklı amaçlara hizmet ediyor. Sivilce deyiminin amacı temelde uygulamanıza bir bakış açısı sağlamaktır, böylece bir tür hızlı takas gibi şeyler yapabilirsiniz.

Sanal sınıfların amacı, polimorfizme izin verme çizgisi üzerinedir, yani türetilmiş tipteki bir nesneye bilinmeyen bir göstericiniz vardır ve x işlevini çağırdığınızda, temel göstericinin gerçekte işaret ettiği sınıf için her zaman doğru işlevi elde edersiniz.

Gerçekten elma ve portakal.


Elmalara / portakallara katılıyorum. Ama bir işlev için pImpl kullandığınız görülüyor. Amacım çoğunlukla inşa-teknik ve bilgi gizleme.
xtofl

2

Pimpl deyimiyle ilgili en can sıkıcı sorun, mevcut kodun bakımını ve analizini son derece zorlaştırmasıdır. Bu nedenle, pimpl kullanarak, yalnızca "derleme bağımlılıklarını ve sürelerini azaltmak ve uygulama ayrıntılarının üstbilgi açığa çıkmasını en aza indirmek" için geliştiricinin zamanı ve hayal kırıklığı ile ödeme yaparsınız. Gerçekten buna değip değmeyeceğine kendiniz karar verin.

Özellikle "derleme süreleri", daha iyi bir donanımla veya Incredibuild (www.incredibuild.com, ayrıca Visual Studio 2017'ye dahil edilmiştir) gibi araçlar kullanarak çözebileceğiniz ve dolayısıyla yazılım tasarımınızı etkilemeyen bir sorundur. Yazılım tasarımı, genellikle yazılımın oluşturulma biçiminden bağımsız olmalıdır.


Ayrıca geliştirme süreleri 2 yerine 20 dakika olduğunda geliştirici süresiyle ödeme yaparsınız, bu yüzden biraz dengesi vardır, gerçek bir modül sistemi burada çok yardımcı olur.
Arkaitz Jimenez

IMHO, yazılımın oluşturulma şekli iç tasarımı hiç etkilememelidir. Bu tamamen farklı bir sorundur.
Trantor

2
Analiz etmeyi zorlaştıran nedir? Impl sınıfına yönlendiren bir uygulama dosyasındaki bir grup çağrı kulağa zor gelmiyor.
mabraham

2
Hem pimpl hem de arabirimlerin kullanıldığı hata ayıklama uygulamalarını hayal edin. A kullanıcı kodundaki bir çağrıdan başlayarak, arayüz B'yi izlersiniz, sonunda D uygulama sınıfında hata ayıklamaya başlamak için sivilceli C sınıfına atlarsınız ... Gerçekte ne olduğunu analiz edene kadar dört adım. Ve eğer her şey bir DLL'de uygulanıyorsa, muhtemelen aralarında bir yerde bir C arayüzü bulacaksınız ....
Trantor'un

PImpl aynı zamanda bir arayüzün işini de yapabiliyorken neden pImpl ile bir arayüz kullanasınız? (yani, bağımlılığı tersine çevirmenize yardımcı olabilir)
pooya13
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.