Gerekmediği halde kullanmam durumunda sanal yıkıcılar kullanmanın maliyeti nedir?
Sokulması maliyeti bir (kalıtsal veya sınıf tanımının bir parçası) bir olasılıkla çok dik bir (ya da nesneye bağlı değildir), böylece gibi, nesne için saklanan bir sanal işaretçi başlangıç maliyeti bir sınıfa sanal işlevi:
struct Integer
{
virtual ~Integer() {}
int value;
};
Bu durumda, hafıza maliyeti nispeten büyüktür. Bir sınıf örneğinin gerçek bellek boyutu şimdi 64 bit mimarilerde şöyle görünür:
struct Integer
{
// 8 byte vptr overhead
int value; // 4 bytes
// typically 4 more bytes of padding for alignment of vptr
};
Bu Integer
sınıf için toplam 4 bayt yerine toplam 16 bayttır. Bunların bir milyonunu bir dizide saklarsak, 16 megabayt bellek kullanımı ile sonuçlanır: tipik 8 MB L3 CPU önbelleğinin iki katı büyüklüğündedir ve böyle bir diziyi tekrar tekrar yinelemek 4 megabayt eşdeğerinden daha yavaş olabilir ek önbellek özlüyor ve sayfa hataları sonucu sanal işaretçi olmadan.
Ancak bu sanal işaretçi nesne başına maliyeti, daha fazla sanal işlevle artmaz. Bir sınıfta 100 sanal üye işlevine sahip olabilirsiniz ve örnek başına ek yük hala tek bir sanal işaretçi olabilir.
Sanal işaretçi genellikle bir genel bakış açısından en acil endişe kaynağıdır. Ancak, örnek başına sanal bir işaretçiye ek olarak, sınıf başına maliyettir. Sanal işlevli her sınıf, sanal bir işlev vtable
çağrısı yapıldığında adresleri gerçekte çağırması gereken işlevlere (sanal / dinamik gönderme) depolayan bir bellek oluşturur . vptr
Bu sınıfa özel noktaların daha sonra örnek başına saklanan vtable
. Bu genel gider daha az endişe vericidir, ancak bu genel giderin karmaşık bir kod tabanındaki bin sınıfa gereksiz yere ödenmesi durumunda ikili boyutunuzu şişirebilir ve bir miktar çalışma zamanı maliyeti ekleyebilir, örneğin vtable
maliyetin bu tarafı gerçekten daha fazla ve Karışımda daha fazla sanal işlev.
Performans açısından kritik alanlarda çalışan Java geliştiricileri, bu tür bir ek yükü çok iyi anlarlar (genellikle boks bağlamında tanımlanırlar), çünkü Java kullanıcı tanımlı bir tür, merkezi bir object
temel sınıftan dolaylı olarak miras alır ve Java'daki tüm işlevler dolaylı olarak sanaldır (geçersiz kılınabilir) ) Aksi belirtilmedikçe doğada. Sonuç olarak, bir Java, Integer
aynı şekilde vptr
örneklenen bu stil meta verilerinin bir sonucu olarak 64 bit platformlarda 16 bayt bellek gerektirme eğilimindedir ve Java'da, int
bir çalışma zamanı ödemeden bir sınıfa tek bir şeyi sarmak genellikle imkansızdır. bunun için performans maliyeti.
Öyleyse soru şudur: c ++ neden tüm yıkıcıları varsayılan olarak sanal olarak ayarlamıyor?
C ++, "kullandıkça öde" türünde bir zihniyetin yanı sıra C'den devralınan pek çok çıplak metal donanım tabanlı tasarımla performansı gerçekten destekliyor. dahil olan her bir sınıf / örnek. Performans, C ++ gibi bir dili kullanmanızın temel nedenlerinden biri değilse, C ++ dilinin çoğu daha az güvenli olduğundan ve ideal performansta olduğundan daha zor olduğundan diğer programlama dillerinden daha fazla yararlanabilirsiniz. böyle bir tasarımı tercih etmenin kilit nedeni.
Ne zaman sanal yıkıcılar kullanmam gerekmiyor?
Oldukça sık. Eğer bir sınıf kalıtsal olarak tasarlanmamışsa, sanal bir yıkıcıya ihtiyaç duymaz ve ihtiyaç duymadığı bir şey için büyük olasılıkla büyük bir ek ödeme yapar. Aynı şekilde, bir sınıf kalıtılmak üzere tasarlanmış olsa bile, ancak bir alt işaretçi türünü asla bir temel işaretçi ile silmeseniz bile, sanal bir yıkıcı gerektirmez. Bu durumda, güvenli bir uygulama, korunmalı, sanal olmayan bir yıkıcıyı tanımlamaktır.
class BaseClass
{
protected:
// Disallow deleting/destroying subclass objects through `BaseClass*`.
~BaseClass() {}
};
Bu durumda sanal yıkıcılar kullanmamalı mıyım?
Sanal yıkıcılar kullanmanız gerektiğinde ele alınması daha kolaydır . Oldukça çoğu zaman kod tabanınızdaki daha fazla sınıf kalıtım için tasarlanmayacaktır.
std::vector
O zaman bu baz işaretçi silme sorunu (eğilimli olacak şekilde, örneğin, miras olarak tasarlanmamıştır ve tipik (çok titrek tasarımı) miras olmamalıdır std::vector
beceriksiz ek olarak kasten sanal yıkıcı önler) nesne dilimleme sorunları eğer senin türetilmiş sınıf, yeni bir durum ekler.
Genel olarak kalıtsal bir sınıfın halka açık bir sanal yıkıcı veya korumalı, sanal olmayan bir sınıfının olması gerekir. Gönderen C++ Coding Standards
bölüm 50:
50. Temel sınıf yıkıcılarını herkese açık ve sanal ya da korumalı ve sanal olmayan bir yere yerleştirin Silmek ya da silmemek; Soru şu: Eğer bir işaretçi ile bir üsse silme işlemine izin veriliyorsa, o zaman Base'in yıkıcısı açık ve sanal olmalıdır. Aksi takdirde, korunmalı ve sanal olmamalıdır.
C ++ 'nın dolaylı olarak vurgulama eğiliminde olduğu şeylerden biri (çünkü tasarımlar gerçekten kırılgan ve beceriksiz ve hatta başka türlü güvensiz olma eğilimindedir), kalıtımın bir düşünce olarak kullanılmak üzere tasarlanmış bir mekanizma olmadığı fikridir. Polimorfizm akılda tutulabilen bir genişletilebilirlik mekanizmasıdır, fakat genişletilebilirliğin gerektiği yere ilişkin öngörü gerektiren bir genişletme mekanizmasıdır. Sonuç olarak, temel sınıflarınız, önceden belirlenmiş bir miras hiyerarşisinin kökleri olarak tasarlanmalı ve önceden böyle bir öngörülemeden sonradan sonradan aldığınız bir şey değildir.
Mevcut kodu tekrar kullanmak için miras almak istediğiniz durumlarda, kompozisyon genellikle şiddetle tavsiye edilir (Kompozit Yeniden Kullanım Prensibi).