Hoşgeldin Misafir

Deadly Diamond of Death ve Virtual Inheritance - C++

xS0N3R

23 Ağu 2020
5,427 Mesaj

Aktiflik

Seviye

Deneyim

TIM / GÖREV:
Merhaba, daha önce yazılım ile azıcık bile ilgilenen birisi varsa illa ki OOP (Nesne Yönelimli Programlama) kavramını duymuştur. Basit düzeyde sınıflar ve onlara ait objeler arasındaki ilişkiler üzerine kurulu bir yazım tekniğidir OOP. Ama -yine çoğunuzun duymuş olacağı üzere- çoğunlukla bir sınıfın başka bir sınıftan türetildiği durumlar bazı belirsizlik problemlerine ve memory üzerindeki şişkinliklere yol açabilir. Kısa bir örnekle
C++:
class A
{
public:
int m_x[100];
}

class B: public A
{
public:
int m_y;
};

int main()
{
    B obj_B;
}

Bu örnekte basitçe bir A sınıfından türetilen B sınıfı, ve bu B sınıfına ait objenin oluşmasını görüyoruz. "obj_B" objesinin memory üzerindeki görüntüsü aşağıdaki gibidir
l049j9r.jpg


obj_B objesi içerisinde A sınıfını tutar. Eğer constructor'larında bir mesaj yazdırsaydık B sınıfından bir obje yaratılırken önce A sınıfının constructor mesajının ekranda belirdiğini görebilirdik. Şimdi kalıtım mimarimizi yataya doğru biraz genişletelim, bir C sınıfı daha türetelim. Bu C sınıfı da -tıpkı B gibi- A sınıfından kalıtım alsın

C++:
class C: public A
{
public:
int m_z;
};
}

Bu durumda kalıtım mimarimiz görseldeki gibi olur
gbpowfp.jpg


Aynı şekilde oluşturacağımız bir "obj_C" objesinde ise, A sınıfından kalıtım görecektik. Bu noktada problemimiz derinleşmedi. Ama şimdi problemimizi bir katman daha derinleştirelim, ve hem B hem de C sınıflarından türetilen bir D sınıfı oluşturalım.

C++:
class D: public B, public C
{
public:
int m_t;
};
}

Bu şemada D sınıfı hem kendisine ait bir m_t attribute'una -niteliğine- hem de kalıtım aldığı m_x[100], m_y ve m_z attribute'larına sahip.
8n916jt.jpg


Burada dikkatli gözlerin fark edebileceği bir problem var. B sınıfı yaratılırken, içerisinde A sınıfını barındırıyordu. C sınıfı yaratılırken de içerisinde A sınıfını barındırıyordu. D sınıfı yaratılırken ise içerisinde hem B ve hem C sınıfını barındırıyor. Yani D sınıfının içerisinde iki tane A sınıfı var. Hafıza üzerinde kapladığı yerlere bakarak bu durumu doğrulayalım:

C++:
#include <iostream>
.
.

int main()
{
    D d;
    std::cout << "Size A: " << sizeof(A) << "\n";
    std::cout << "Size B: " << sizeof(B) << "\n";
    std::cout << "Size C: " << sizeof(C) << "\n";
    std::cout << "Size D: " << sizeof(D) << "\n";

>>
PS C:\Users\Hacknology\tht> g++ virtual_inheritance.cpp -o inheritance.exe   
PS C:\Users\Hacknology\tht> .\inheritance.exe
Size A: 400
Size B: 404
Size C: 404
Size D: 812
}

Göreceğiniz üzere A sınıfı sadece 400 byte'lık bir yer tutuyor, bunun sebebi sahip olduğu 100 integer tutacak m_x array'i -her bir integer benim sistemimde 4 byte veri tutmakta-. B sınıfında bu sayı 404, int m_y'den 4 byte geliyor ve kalıtım aldığı A::m_x'ten de 400 byte geliyor. C sınıfı da benzer şekilde 404 byte'lık yer tutuyor. D sınıfında bu sayı 812. Bunun sebebi:
D::m_t (4)+ C::m_z (4)+ B::m_y (4) + A::m_x (400)(B'den kalıtıldı) + A::m_x (400)(C'den kalıtıldı)
(Eğer Constructor'lara mesaj yazdırsaydınız da A sınıfının 2 kere constructor çalıştırdığını görebilirdiniz.)


Bu durum aynı zamanda bir belirsizliğe yol açıyor. Eğer obj_D ismiyle yaratılan bir D sınıfı objesinin m_x özelliğini değiştirmek isteseydiniz bu B sınıfının yolundan gelen A::m_x mi olacaktı, yoksa C sınıfının yolundan gelen A::m_x mi olacaktı? Bu belirsizlikten dolayı derleyici size hata verecek ve kodunuzu derlemeyecektir.


C++ ile belli bir seviyede ilgilenenler varsa dilin problemleri çözmek için halihazırda var olan keyword'leri sık sık farklı amaçlar için overload ettiğini, yani aynı kelimeye dilin yerine göre farklı anlamlar yüklediğini biliyordur. Bu problemin çözümü için de en çok overload edilmiş kelimelerden birisi olan "virtual" kelimesinin gücüne sığınacağız. Lakin buradaki "virtual" kelimesinin daha önce muhtemelen karşılaşmış olduğunuz fonksiyon belirtecindeki virtual ile birebir aynı anlamda örtüşmediğini belirtmek isterim.

B ve C sınıfının tanımlarına ekleyeceğimiz bir virtual kelimesi, örnekte göreceğiniz üzere, bu konuyu çözecektir.

C++:
class A
{
public:
int m_x[100];
};

class B: virtual public A
{
public:
    int m_y;   
};

class C: virtual public  A
{
public:
    int m_z;
};

Artık derleyicimiz hem B, hem C yolları için yalnızca bir adet A sınıfı oluşturacak, ve kopya A sınıfı yerine aynı sınıf bilgisini kullanacaktır. Eğer bu durumda sınıfların boyutunu yazdırmak istersek:
C++:
PS C:\Users\Hacknoloy\tht> g++ virtual_inheritance.cpp -o inheritance.exe
PS C:\Users\Hacknology\tht> .\inheritance.exe
Size A: 400
Size B: 408
Size C: 408
Size D: 420

Gördüğünüz üzere neredeyse yarı yarıya bir verim kazancımız oldu hafıza üzerinde. Şimdi sınıfların tuttuğu yerleri incelersek:
C++:
A sınıfı için
400  byte

|_ >400 -> int m_x[100]



B sınıfı için

408 byte

|_> 400 -> Sınıf A (Aslında bu B sınıfı içerisinde tutulmuyor, bazı derleyiciler bu 400 byte'ı göstermeyebiliyorken benim sistemimde gösteriyor)

|_> 4 -> int m_y

|_> 4 -> ortak A sınıfına ait bilgiyi tutan pointer (vptr)



Benzer şekilde, C sınıfı da 408 byte olacaktır.



D sınıfı için


420 byte


|_> 400 -> Sınıf A


|_> 4 -> int m_y


|_> 4 -> int m_z


|_> 4 -> int m_t


|_> 4 -> ortak A sınıfınta ait bilgiyi tutan pointer (vptr)

Gördüğünüz gibi bu implementasyonda D sınıfı yaratılırken ortak bir taban A (Base) sınıfı kullanıldı, bu yüzden 400 byte'a yakın tasarruf sağlandı. Ayrıca artık D sınıfının m_x array'i herhangi bir belirsizlik yaratılmadan erişilebilir oldu.

Okuduğunuz için teşekkür ederim, uzun bir aradan sonra ilk yazımın sonuna geldim.