wirtualne dziedziczenie

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:

Wygenerowano przy pomocy projektu gcc-uml

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

Wygenerowano przy pomocy projektu gcc-uml

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 oraz WalkingAnimal rozpoczyna się od obszaru klasy Animal,
  • 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:

  1. kompilator nie wie, pola z której instancji klasy Animal użyć,
  2. metody klasy FlyingAnimal oraz WalkingAnimal operują na osobnych instancjach pól klasy Animal.

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:

wirtualne dziedziczenie
Wygenerowano przy pomocy projektu gcc-uml

W porównaniu do poprzedniego diagramu występują następujące różnice:

  1. klasy wykorzystujące wirtualne dziedziczenie zawierają tablice metod wirtualnych (nawet, gdy żadna z klas nie zawiera metod wirtualnych),
  2. klasa bazowa dziedziczona w sposób wirtualny zostaje umieszczona na końcu bloku pamięci klas potomnych na wszystkich kolejnych poziomach dziedziczenia,
  3. klasa Duck zawiera uciętą wersję klasy FlyingAnimal oraz WalkingAnimal.

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.

  1. za Wikipedią ↩︎
  2. 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

Your email address will not be published. Required fields are marked *