Ders 11 / 14
Çoklu Kalıtım ve Elmas Sorunu
Çoklu kalıtımda ikinci taban alt nesnenin türetilmiş nesneyle aynı adresi paylaşmadığının ve elmas biçimli kalıtımda ortak atanın sanal kalıtım olmadan ikiye katlandığının ölçümü.
Yayın Güncelleme
Bu derste
- Çoklu kalıtımda yalnız ilk taban alt nesnenin türetilmiş nesneyle aynı adresi paylaştığını ölçer
- Elmas biçimli kalıtımda ortak bir atanın sanal kalıtım olmadan iki ayrı kopyaya bölündüğünü gösterir
- Sanal kalıtımın ortak atayı tek bir paylaşılan taban alt nesneye indirdiğini doğrular
İçindekiler
Önceki iki ders tekli kalıtımı ölçtü: bir türetilmiş nesnenin tek bir taban alt nesnesi vardı ve o alt nesnenin adresi hep türetilmiş nesnenin adresiyle çakıştı. Bu ders bir sınıfın birden çok taban sınıftan aynı anda türediği durumu ele alıyor. İlk taban alt nesne yine türetilmiş nesnenin başında duruyor mu? İkincisi de aynı adresi mi paylaşıyor? Ve iki taban sınıfın ortak bir atası varsa, o ata türetilmiş nesnenin içinde kaç kez yer kaplıyor?
Çoklu Kalıtımda İkinci Taban Alt Nesnenin Adresi Farklı
Triatletci, birbiriyle ilişkisiz iki sınıftan türüyor: Yuzucu ve Kosucu. İlk taban
alt nesne türetilmiş nesnenin başında mı duruyor, ikincisi de aynı adresi mi
paylaşıyor?
// coklu_kalitim_ofset.cpp — coklu kalitimda ilk taban ayni adreste, ikincisi degil
#include <iostream>
class Yuzucu {
public:
int stil_ = 1;
};
class Kosucu {
public:
int mesafe_ = 5;
};
class Triatletci : public Yuzucu, public Kosucu {
public:
int toplam_puan_ = 0;
};
int main() {
Triatletci t;
std::cout << std::boolalpha;
std::cout << "t ile ilk taban Yuzucu adresinin ayni olup olmadigi "
<< (static_cast<void*>(&t) == static_cast<void*>(static_cast<Yuzucu*>(&t))) << "\n";
std::cout << "t ile ikinci taban Kosucu adresinin ayni olup olmadigi "
<< (static_cast<void*>(&t) == static_cast<void*>(static_cast<Kosucu*>(&t))) << "\n";
return 0;
}
t ile ilk taban Yuzucu adresinin ayni olup olmadigi true t ile ikinci taban Kosucu adresinin ayni olup olmadigi false
CB10. Triatletci, kalıtım listesinde önce Yuzucu’yu, sonra Kosucu’yu bildiriyor
— derleyici taban alt nesneleri bu sırayla yerleştiriyor. İlk taban alt nesne
(Yuzucu) t’nin başında duruyor, bu yüzden &t ile static_cast<Yuzucu*>(&t) aynı
adresi taşıyor, true. İkinci taban alt nesne (Kosucu) ise Yuzucu alt nesnesinden
sonraki bir ofsette duruyor; &t ile static_cast<Kosucu*>(&t) farklı adresler,
false. Tekli kalıtımda (Kalıtım dersi) taban alt nesnenin adresi hep türetilmiş
nesneyle çakışıyordu çünkü taşınacak tek bir taban vardı; çoklu kalıtımda bu yalnız
ilk taban için geçerli. Sınıf parça: her iki taban alt nesne de t’nin bir
parçası, ama aynı adresi paylaşan yalnız ilki.
Elmas Sorunu: Ortak Atadan İki Ayrı Kopya
Yuzucu ve Kosucu’nun ortak bir atası Canli olsun. Triatletci ikisinden birden
türediğinde, Canli alt nesnesine Yuzucu üzerinden ve Kosucu üzerinden ulaşmak aynı
nesneye mi çıkıyor, yoksa iki ayrı kopyaya mı?
// elmas_ayrik.cpp — sanal kalitim olmadan ortak ata iki ayri kopyaya bolunuyor
#include <iostream>
class Canli {
public:
int can_ = 100;
};
class Yuzucu : public Canli {};
class Kosucu : public Canli {};
class Triatletci : public Yuzucu, public Kosucu {};
int main() {
Triatletci t;
Canli* yuzucu_uzerinden = static_cast<Canli*>(static_cast<Yuzucu*>(&t));
Canli* kosucu_uzerinden = static_cast<Canli*>(static_cast<Kosucu*>(&t));
std::cout << std::boolalpha;
std::cout << "Yuzucu uzerinden ve Kosucu uzerinden ulasilan Canli'nin ayni olup olmadigi "
<< (static_cast<void*>(yuzucu_uzerinden) == static_cast<void*>(kosucu_uzerinden)) << "\n";
std::cout << "Triatletci boyutunun iki ayri Canli'yi barindiracak kadar buyuk olup olmadigi "
<< (sizeof(Triatletci) >= 2 * sizeof(Canli)) << "\n";
return 0;
}
Yuzucu uzerinden ve Kosucu uzerinden ulasilan Canli'nin ayni olup olmadigi false Triatletci boyutunun iki ayri Canli'yi barindiracak kadar buyuk olup olmadigi true
CB11. Yuzucu ve Kosucu, Canli’den sıradan (sanal olmayan) kalıtımla
türüyor; her biri kendi Canli alt nesnesini taşıyor. Triatletci her ikisinden
türediğinde, bu iki Canli kopyası da Triatletci’nin içine giriyor — birleşmiyor.
yuzucu_uzerinden ile kosucu_uzerinden farklı adresler taşıyor, false: bunlar iki
ayrı Canli nesnesi. sizeof(Triatletci)’nin en az iki Canli büyüklüğünü kapsaması
bunu bellek düzeninde doğruluyor, true. Bu, elmas sorunu: t.can_ yazmak hangi
kopyanın kastedildiği belirsiz olduğu için derleyiciyi durdurur, yalnız t.Yuzucu::can_
ya da t.Kosucu::can_ gibi açık bir yol belirtilirse derlenir — bu ders o belirsiz
satırı hiç çalıştırmıyor, yalnız iki kopyanın varlığını adres ve boyut ölçümüyle
gösteriyor. Sınıf parça: Triatletci, Canli’nin bir değil, iki ayrı parçasını
taşıyor.
Sanal Kalıtım: Tek Paylaşılan Taban Alt Nesnesi
Yuzucu ve Kosucu, Canli’den sanal kalıtımla türerse, Triatletci her ikisinden
türediğinde Canli alt nesnesi yine ikiye mi bölünüyor, yoksa tek bir paylaşılan
nesneye mi indiriliyor?
// elmas_sanal.cpp — sanal kalitim ortak atayi tek paylasilan alt nesneye indiriyor
#include <iostream>
class Canli {
public:
int can_ = 100;
};
class Yuzucu : virtual public Canli {};
class Kosucu : virtual public Canli {};
class Triatletci : public Yuzucu, public Kosucu {};
int main() {
Triatletci t;
Canli* yuzucu_uzerinden = static_cast<Canli*>(static_cast<Yuzucu*>(&t));
Canli* kosucu_uzerinden = static_cast<Canli*>(static_cast<Kosucu*>(&t));
std::cout << std::boolalpha;
std::cout << "Yuzucu uzerinden ve Kosucu uzerinden ulasilan Canli'nin ayni olup olmadigi "
<< (static_cast<void*>(yuzucu_uzerinden) == static_cast<void*>(kosucu_uzerinden)) << "\n";
std::cout << "t.can_ ile yuzucu_uzerinden->can_ 'in ayni adresi tasiyip tasimadigi "
<< (static_cast<void*>(&t.can_) == static_cast<void*>(&yuzucu_uzerinden->can_)) << "\n";
return 0;
}
Yuzucu uzerinden ve Kosucu uzerinden ulasilan Canli'nin ayni olup olmadigi true t.can_ ile yuzucu_uzerinden->can_ 'in ayni adresi tasiyip tasimadigi true
CB12. virtual public Canli bildirimi, Yuzucu ve Kosucu’ya “kendi Canli
kopyanı kurma, en tepedeki türetilmiş sınıfın paylaştığı tek kopyayı kullan” diyor.
Triatletci her ikisinden türediğinde artık tek bir Canli alt nesnesi var —
yuzucu_uzerinden ile kosucu_uzerinden aynı adresi taşıyor, true. Bu paylaşım
sayesinde t.can_ de artık belirsiz değil, doğrudan derleniyor ve aynı paylaşılan
nesneye işaret ediyor, true. Önceki ölçümdeki elmas sorunu — iki ayrı Canli
kopyası — burada ortadan kalktı, çünkü Canli’ye giden iki yol da aynı tek nesnede
birleşiyor. Sınıf parça: Triatletci, Canli’nin artık iki değil, tek bir
parçasını taşıyor ve bu parçaya hem Yuzucu hem Kosucu üzerinden ulaşılıyor.
Nitelenmiş Adla İki Ayrı Kopyaya Erişip Yalnız Birini Değiştirmek
CB11, t.can_ yazmanın hangi kopya kastedildiği belirsiz olduğu için derlenmediğini,
yalnız t.Yuzucu::can_ ya da t.Kosucu::can_ gibi nitelenmiş bir adla derlendiğini
söylemişti ama bu satırı hiç çalıştırmamıştı. Nitelenmiş adla yalnız bir kopya
değiştirildiğinde, diğer kopya gerçekten etkilenmeden mi kalıyor?
// coklu_kalitim_nitelenmis_erisim.cpp — sanal olmayan kalitimda iki ayri Canli kopyasina nitelenmis adla erisilip biri degistiriliyor
#include <iostream>
class Canli {
public:
int can_ = 100;
};
class Yuzucu : public Canli {};
class Kosucu : public Canli {};
class Triatletci : public Yuzucu, public Kosucu {};
int main() {
Triatletci t;
t.Yuzucu::can_ = 70;
std::cout << "t.Yuzucu::can_ degistirildikten sonra degeri " << t.Yuzucu::can_ << "\n";
std::cout << "t.Kosucu::can_ degismeden kalan degeri " << t.Kosucu::can_ << "\n";
std::cout << std::boolalpha
<< "t.Yuzucu::can_ ile t.Kosucu::can_ 'in ayni adresi tasiyip tasimadigi "
<< (static_cast<void*>(&t.Yuzucu::can_) == static_cast<void*>(&t.Kosucu::can_)) << "\n";
return 0;
}
t.Yuzucu::can_ degistirildikten sonra degeri 70 t.Kosucu::can_ degismeden kalan degeri 100 t.Yuzucu::can_ ile t.Kosucu::can_ 'in ayni adresi tasiyip tasimadigi false
CB32. t.Yuzucu::can_ = 70; nitelenmiş adı kullanarak derleyiciye hangi kopyanın
kastedildiğini açıkça söylüyor ve yalnız o kopyayı değiştiriyor. t.Kosucu::can_
sorgulandığında hâlâ ilk değeri, 100, dönüyor — Yuzucu üzerinden yapılan değişiklik
Kosucu‘nun kendi Canli kopyasına hiç sızmıyor. Adres karşılaştırması bunu
doğruluyor: false, iki nitelenmiş erişim gerçekten iki ayrı nesneye ulaşıyor. Bu,
CB11’in bellek boyutuyla dolaylı gösterdiği şeyi doğrudan kanıtlıyor: Triatletci’nin
içinde Canli bir kez değil, iki kez var ve nitelenmiş erişim programcıya bu iki
kopyadan hangisiyle çalışacağını seçme imkânı veriyor — derleyicinin kendiliğinden
seçemediği bir kararı elle vermek. Sınıf parça: t.Yuzucu::can_ ve t.Kosucu::can_,
t’nin iki farklı parçası, bağımsız olarak değişebiliyorlar.
Sanal Tabanın Yapıcısını En Türemiş Sınıf Çağırıyor
Canli’nin artık parametre alan, varsayılan olmayan bir kurucusu var. Yuzucu ve
Kosucu kendi kurucularında Canli’yi kendi değerleriyle kurmaya çalışıyor, ama
Triatletci de kendi kurucusunda Canli’yi ayrı bir değerle kuruyor. Sanal kalıtımda
paylaşılan can_ hangi kurucunun verdiği değeri taşıyor?
// elmas_sanal_yapici_zinciri.cpp — sanal tabanin yapicisini en turemis sinif dogrudan cagiriyor, aradakiler gorunmezden geliniyor
#include <iostream>
class Canli {
public:
explicit Canli(int can) : can_(can) {
std::cout << "Canli yapicisi can=" << can_ << "\n";
}
int can_;
};
class Yuzucu : virtual public Canli {
public:
Yuzucu() : Canli(1) {
std::cout << "Yuzucu yapicisi (kendi Canli(1) cagrisi gorunmezden gelinecek)\n";
}
};
class Kosucu : virtual public Canli {
public:
Kosucu() : Canli(2) {
std::cout << "Kosucu yapicisi (kendi Canli(2) cagrisi gorunmezden gelinecek)\n";
}
};
class Triatletci : public Yuzucu, public Kosucu {
public:
Triatletci() : Canli(100), Yuzucu(), Kosucu() {
std::cout << "Triatletci yapicisi, paylasilan can_ = " << can_ << "\n";
}
};
int main() {
Triatletci t;
return 0;
}
Canli yapicisi can=100 Yuzucu yapicisi (kendi Canli(1) cagrisi gorunmezden gelinecek) Kosucu yapicisi (kendi Canli(2) cagrisi gorunmezden gelinecek) Triatletci yapicisi, paylasilan can_ = 100
CB33. Canli yapicisi can=100 yalnız bir kez basılıyor, can=1 ya da can=2
hiç görünmüyor — sanal tabanın kurucusunu çağırma sorumluluğu, ara sınıflarda değil,
en türemiş sınıfta. Yuzucu’nun ve Kosucu’nun kendi mem-initializer listelerindeki
Canli(1) ve Canli(2) çağrıları, Triatletci üzerinden kurulurken gözardı ediliyor;
bu satırlar yalnız Yuzucu ya da Kosucu tek başına (Triatletci’siz) kurulduğunda
devreye girerdi. Triatletci‘nin kendi mem-initializer listesindeki Canli(100) ise
gerçekten çalışan tek çağrı — bu yüzden paylaşılan can_ 100. Sanal tabanlar,
kalıtım zincirindeki konumlarından bağımsız olarak, her zaman en tepedeki türetilmiş
sınıfın kurucusu tarafından ilklendiriliyor; bu da CB12’nin ölçtüğü “tek paylaşılan
nesne” ilkesinin kurucu tarafındaki karşılığı — paylaşılan nesne tek olduğu için, onu
kuracak çağrı da tek olmak zorunda. Sınıf parça: can_, t’nin tek bir paylaşılan
parçası ve bu parçanın değerini yalnız en türemiş sınıfın kurucusu belirliyor.
Sanal Kalıtımın Yapısal Maliyeti: Kopya Azalıyor, İşaretçi Ekleniyor
CB11 sanal olmayan elmasta Canli’nin iki kez yer kapladığını, CB12 sanal elmasta tek
kopyaya indiğini göstermişti. Kopya sayısı azalırken toplam nesne boyutu da aynı oranda
mı küçülüyor, yoksa sanal kalıtımın kendi ek yükü bu kazancın bir kısmını mı yiyor?
// elmas_sanal_yapisal_maliyet.cpp — sanal kalitimin nesne boyutuna etkisi ayni sinif kumesinin sanalsiz halidir karsilastiriliyor
#include <iostream>
class Canli {
public:
int can_ = 1;
};
class YuzucuDuz : public Canli {};
class KosucuDuz : public Canli {};
class TriatletciDuz : public YuzucuDuz, public KosucuDuz {};
class YuzucuSanal : virtual public Canli {};
class KosucuSanal : virtual public Canli {};
class TriatletciSanal : public YuzucuSanal, public KosucuSanal {};
int main() {
std::cout << std::boolalpha
<< "sanal elmasin boyutu sanalsiz elmastan kucuk mu: "
<< (sizeof(TriatletciSanal) < sizeof(TriatletciDuz)) << "\n";
std::cout << "sanal elmasin boyutu tek bir Canli'den buyuk mu (ek yuk var mi): "
<< (sizeof(TriatletciSanal) > sizeof(Canli)) << "\n";
std::cout << "sanalsiz elmasin boyutu en az iki Canli kadar mi: "
<< (sizeof(TriatletciDuz) >= 2 * sizeof(Canli)) << "\n";
return 0;
}
sanal elmasin boyutu sanalsiz elmastan kucuk mu: false sanal elmasin boyutu tek bir Canli'den buyuk mu (ek yuk var mi): true sanalsiz elmasin boyutu en az iki Canli kadar mi: true
CB34. Beklenti kopya sayısı azaldığı için sanal elmasın daha küçük çıkması yönündeydi,
ama ölçüm bunu doğrulamıyor: false. Canli’nin tek bir int üyesi olduğu için
kopyanın bedeli zaten küçük (birkaç bayt); buna karşılık Yuzucu ve Kosucu’nun her
biri, kendi Canli’sine nasıl ulaşacağını çözmek için ayrı bir kayıt (sanal taban
işaretçisi ya da benzeri bir mekanizma, derleyiciye göre değişir) taşıyor — iki taban,
iki ayrı kayıt. Bu derleyicide ek kayıtların toplam bedeli, tasarruf edilen tek Canli
kopyasından daha ağır bastı. Sanal elmasın tek bir Canli‘den büyük olması (true)
zaten bekleniyordu; asıl öğretici olan, “daha az kopya” ile “daha küçük nesne”nin aynı
şey olmadığıdır. Bu, CB26’nın sanal işlevler için ölçtüğü “vptr her nesneye ekleniyor”
bulgusunun çoklu kalıtım bağlamındaki karşılığı: paylaşılan bir şeyi (yöntem tablosu /
taban alt nesnesi) tek elde tutmak, nesneye küçük ama sıfır olmayan bir muhasebe
maliyeti yüklüyor — ve bu maliyet, ortadan kaldırılan kopyanın kendi boyutundan büyük
de olabiliyor. Sınıf parça: ölçülen büyüklük farkı, t’nin sanal taban alt
nesnesinin kendisinden değil, o alt nesneye giden yolu tutan ek parçalardan
kaynaklanıyor.
Sınırlayıcı Ölçüm
Bu derste ölçülen sanal kalıtım, verinin (can_) paylaşımını ve sanal taban sınıfın
kurucusunu yalnız en türemiş sınıfın çağırdığı kuralı gösterdi; ama ölçüm sanal
kalıtımın taşıdığı ek dolaylılık maliyetini (her erişimin bir sanal taban işaretçisi
üzerinden çözülmesi) ele almadı — yalnız adres eşitliğini ölçtü, çözüm yolunun kaç adım
sürdüğünü değil. Triatletci’nin kendi sanal işlevleri olduğu, Yuzucu ve Kosucu’nun
da birbirinden farklı aynı imzalı işlevler tanımladığı durum — adlandırma çakışması ve
belirsiz yeniden tanımlama — bu dersin kapsamı dışında. Nitelenmiş erişim (t.Yuzucu::can_)
yalnız sanal olmayan kalıtımda gösterildi; sanal kalıtımda paylaşılan tek kopyaya
nitelenmiş adla erişmenin nasıl davrandığı — ki normal erişimden farksız olurdu, çünkü
kopya zaten tek — bu ders ayrıca sınamadı.
Özet
- Çoklu kalıtımda yalnız ilk taban sınıfın alt nesnesi türetilmiş nesneyle aynı adresi paylaşıyor; sonraki taban alt nesneleri farklı bir ofsette duruyor.
- İki taban sınıfın ortak bir atası varsa ve kalıtım sanal değilse, o ata türetilmiş nesnenin içinde iki ayrı kopya olarak yer kaplıyor — elmas sorunu.
sizeof, iki ayrı kopyanın varlığını bellek boyutunda doğruluyor; iki farklı yoldan ulaşılan ortak ata nesnesi farklı adresler taşıyor.virtual publickalıtım, ortak atayı en tepedeki türetilmiş sınıfın paylaştığı tek bir alt nesneye indiriyor; iki yoldan ulaşılan ata artık aynı adresi taşıyor.- Bu dersin ölçtüğü ifade parça sınıfında: taban alt nesne türetilmiş nesnenin bir parçası, ama çoklu ve elmas biçimli kalıtımda o parçanın adresi ve sayısı değişiyor.
Sonraki Adım
Bu derste ölçülen hep normal akışta tamamlanan kurucu ve yıkıcı çağrılarıydı; hiçbir
kurucu bir hata fırlatmadı. Bir sonraki ders, İstisnalar, bir kurucunun yarıda kesildiği
— bir hata fırlattığı — durumu ölçüyor: yapım tamamlanmadan fırlatılan bir istisna,
o nesnenin zaten kurulmuş parçalarının yıkımını nasıl etkiliyor ve bir istisna nesnesi
throw ile fırlatılıp catch ile yakalanırken bir kopya mı çıkarıyor. Ölçülen yine
bir ifadenin hangi nesneyle çalıştığı, ama bu kez soru kurma sürecinin ortasında kesilen
bir nesnenin durumu.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.