Cechą charakterystyczną języka C++ jest wielodziedziczenie. Jest to mechanizm, który w pewnych okolicznościach może być użyteczny, jednak wiąże się z niebezpieczeństwem – z problemem Deadly Diamond of Death1. Propozycją rozwiązania problemu jest wirtualne dziedziczenie.
Problem typu diamond
Problem Deadly Diamond of Death został zaprezentowany w poniższym kodzie:
namespace property {
class Animal {
public:
double weight;
double width;
double height;
};
class FlyingAnimal: public Animal {
public:
double flydistance = 0.0;
void fly(const double distance) {
flydistance += distance;
}
};
class WalkingAnimal: public Animal {
public:
double walkdistance = 0.0;
void walk(const double distance) {
walkdistance += distance;
}
};
}
class Duck: public property::FlyingAnimal, public property::WalkingAnimal {
public:
int noquacks = 0;
void quack() {
++noquacks;
}
};
Kod może być przedstawiony w postaci następującego diagramu:

Struktura pamięci wygenerowana przez kompilator GCC2 dla klasy Duck
jest następująca:

Numery w kolumnach to odpowiednio wartość przesunięcie początku obszaru względem początku struktury oraz rozmiar obszaru pamięci.
Należy zwrócić uwagę, że:
- obszar każdej klasy rozpoczyna się od sekwencji klas dziedziczonych, po czym następują pola klas,
- obszar pamięci klasy
FlyingAnimal
orazWalkingAnimal
rozpoczyna się od obszaru klasyAnimal
, - rozmiar dołączonych w ramach dziedziczenia struktur odpowiada rozmiarom zdefiniowanych struktur.
Problem tej sytuacji polega na tym, że Duck
zawiera dwa razy pola weight
, width
oraz height
z klasy Animal
. W konsekwencji:
- kompilator nie wie, pola z której instancji klasy
Animal
użyć, - metody klasy
FlyingAnimal
orazWalkingAnimal
operują na osobnych instancjach pól klasyAnimal
.
W optymistycznym przypadku programista otrzyma komunikat o błędzie kompilacji, a pesymistycznym przypadku programista doświadczy trudnych do zdiagnozowania błędów wykonania programu związanych z utratą danych.
Wirtualne dziedziczenie
Powyższy problem można rozwiązać używając słowa kluczowego virtual
w następujący sposób:
namespace property {
class Animal {
public:
double weight;
double width;
double height;
};
class FlyingAnimal: virtual public Animal {
public:
double flydistance = 0.0;
void fly(const double distance) {
flydistance += distance;
}
};
class WalkingAnimal: virtual public Animal {
public:
double walkdistance = 0.0;
void walk(const double distance) {
walkdistance += distance;
}
};
}
class Duck: public property::FlyingAnimal, public property::WalkingAnimal {
public:
int noquacks = 0;
void quack() {
++noquacks;
}
};
Wówczas kompilator zostaje poinstruowany o zastosowaniu innej organizacji pamięci. Obraz pamięci przedstawiono na poniższym diagramie:

W porównaniu do poprzedniego diagramu występują następujące różnice:
- klasy wykorzystujące wirtualne dziedziczenie zawierają tablice metod wirtualnych (nawet, gdy żadna z klas nie zawiera metod wirtualnych),
- klasa bazowa dziedziczona w sposób wirtualny zostaje umieszczona na końcu bloku pamięci klas potomnych na wszystkich kolejnych poziomach dziedziczenia,
- klasa
Duck
zawiera uciętą wersję klasyFlyingAnimal
orazWalkingAnimal
.
Tak jak to jest ukazane na diagramie, kompilator zapewnia wystąpienie tylko jednej instancji klasy bazowej w klasach potomnych poprzez wycięcie z klas dziedziczonych klasy podstawowej. Świadczy o tym różnica w rozmiarze obszarów pamięci klas bazowych dołączonych do klasy Duck
w porównaniu do rozmiaru tych klas. Przykładowo klasa FlyingAnimal
zajmuje 320 bitów, natomiast w klasie Duck
obszar pamięci zarezerwowany dla tej klasy to 128 bitów.
Podsumowanie
GCC w ciekawy sposób zaimplementował funkcjonalność wirtualnego dziedziczenia. Warto o tym wiedzieć na wypadek potencjalnej rozmowy rekrutacyjnej.
- za Wikipedią ↩︎
- do wygenerowania diagramów został użyty kompilator w wersji gcc-15 na architekturze x86-64 ↩︎
Twój zespół potrzebuje wsparcia we wdrożeniu dobrych praktyk?
Leave a Reply