A virtuális öröklődés egy C++ technika, amely biztosítja, hogy az ősosztály tagváltozóinak csak egy példányát örököljék a származtatott osztályok. Virtuális öröklődés nélkül, ha két B és C osztály örököl egy A osztályból, és D osztály örököl mind a B, mind a C osztályból, akkor a D osztály tartalmaz két példányt az A tagváltozóiból, egyet a B osztályon keresztül, egyet pedig a C osztályon keresztül. Ezek függetlenül, hatókör-felbontással lesznek elérhetők.

A gyémántöröklődés képe, egy olyan probléma, amelyet a virtuális öröklődés megpróbál megoldani

Ehelyett ha a B és a C osztály az A osztályból virtuálisan örököl, akkor a D osztály objektumai csak az A osztály tagváltozóinak halmazát tartalmazzák.

Ez a szolgáltatás a leginkább a többszörös öröklődésnél hasznos, mivel a virtuális őst a mellékobjektum közös elemévé teszi a származtatandó osztály és az abból származó összes osztály számára. Ez felhasználható a „gyémántprobléma” elkerülésére, azáltal, hogy egyértelművé teszi, hogy melyik ősosztályt kell használni, mivel a származtatandó osztály szempontjából (a fenti példában a D osztály) a virtuális alap (A) úgy viselkedik, mintha a közvetlen ősosztály lenne. D nem egy szülőosztályon (B vagy C) keresztül származtatott osztály.[1][2]

Ezt akkor használják, amikor az öröklődés egy készlet korlátozását jelenti, nem pedig a komponensek összetételét. A C++ rendszerben egy olyan alaposztályt, amely a hierarchia egészében egységes, virtuálisnak kell jelölni a virtual kulcsszóval.

Adott a következő osztályhierarchia.

struct Állat
{
 	virtual ~Állat() = default;
 	virtual void Eszik() {}
};

struct Emlős : Állat
{
 	virtual void Lélegzik() {}
};

struct SzárnyasÁllat : Állat
{
 	virtual void SzárnyCsapás() {}
};

// A denevér egy szárnyas emlős.
struct Denevér: Emlős, SzárnyasÁllat {};

Denevér denevér;

Mint ahogy fent is deklaráltuk, egy Denevér.Eszik meghívás félreérthető, mert két Állat (közvetlen) ősosztály a denevérben, szóval bármelyik denevér objektumnak van két különböző Állat ősosztály mellékobjektuma. Szóval megpróbálni közvetlenül hozzá kötni egy Denevér objektumnak az Állat mellékobjektumát nem járna sikerrel, mivel a kötés öröklése nem egyértelmű.

Denevér d;
Állat& a = d;  // hiba

Az egyértelműség érdekében az egyiknek át kell alakulnia valamelyik ősosztályba.

Denevér d;
Állat& emlős = static_cast<Emlős&>(d);
Állat& szárnyas = static_cast<SzárnyasÁllat&>(d);

Az Eszik hívásához ugyanazon az egyértelműsítésre vagy explicit minősítésre van szükség: static_cast<Emlős&>(denevér).Eszik() vagy static_cast<SzárnyasÁllat&>(denevér).Eszik() vagy alternatív módon denevér.Emlős :: Eszik() és denevér.SzárnyasÁllat :: Eszik(). Az explicit kvalifikáció nem csupán könnyebb, egységesebb szintaxist alkalmaz mind a mutatók az objektumok számára, de lehetővé teszi a statikus továbbítást is, tehát vitathatatlanul ez lenne a preferált módszer.

Ebben az esetben az Állat kettős öröklődése valószínűleg nemkívánatos, mivel azt akarjuk modellezni, hogy a kapcsolat (a denevér egy állat) csak egyszer létezik, az, hogy a denevér Emlős és SzárnyasÁllat, nem jelenti azt, hogy kétszer is Állat, az Állat alaposztály egy olyan szerződésnek felel meg, amelyet a Denevér indít (a „is-a” kapcsolat valóban azt jelenti, hogy „végrehajtja a követelményeket”) és egy denevér csak egyszer Állat. A „csak egyszer” való világbéli jelentése az, hogy a Denevérnek csak egyik módja lehet az Eszik() megvalósításához, nem pedig két különféle módja, attól függően hogy a denevért Emlős része eszik-e, vagy a SzárnyasÁllat része. (Az első kódrészletben azt látjuk, hogy az Eszik nem íródik felül, sem az Emlős-ben, sem a SzárnyasÁllat-ban, tehát a két Állat mellékobjektuma ugyanúgy fog viselkedni, de ez csak egy degenerált eset de nem különbözik a C++ szemszögéből.

A megoldás szerkesztés

Az osztályokat a következők szerint deklarálhatjuk újra:

struct Állat
{
 	virtual ~Állat() = default;
 	virtual void Eszik() {}
};

// Két osztály virtuálisan örökli az Állat-ot:
struct Emlős: virtual Állat
{
 	virtual void Lélegzik() {}
};

struct SzárnyasÁllat: virtual Állat
{
 	virtual void SzárnyCsapás() {}
};

// A denevér még mindig egy szárnyas emlős
struct Denevér: Emlős, SzárnyasÁllat {};

A Denevér:SzárnyasÁllat Állati része most ugyanazon példányra mutat, mint a Denevér:Emlős által használt Állat, azaz egy denevérnek csak egy, megosztott, állati példánya van a reprezentációjában, tehát egy Denevér hívásánál az Eszik egyértelmű. Ezenkívül a Denevérről az Állatra történő kasztolás szintén egyértelmű, mivel már csak egy Állat példány létezik, amelybe a Denevér át konvertálható.

Annak a képessége, hogy megosztunk egyetlen egy példányt az Állat ősből az Emlős és a SzárnyasÁllatok leszármazottja között. Két virtuális táblát tartalmazó mutató van jelen, egy az öröklés hierarchiáját tartja számon. Ebben a példában egy az Emlősre és egy a SzárnyasÁllat-ra az Állat-ból származtatva. Az objektum mérete így növekedett 2 mutatóval ebben az esetben, azonban most már csak egyetlen egy Állat objektumot kell kezelni.

Jegyzetek szerkesztés

  1. Milea, Andrei: Solving the Diamond Problem with Virtual Inheritance. Cprogramming.com . (Hozzáférés: 2010. március 8.)
  2. McArdell, Ralph: C++/What is virtual inheritance?. All Experts , 2004. február 14. [2010. január 10-i dátummal az eredetiből archiválva]. (Hozzáférés: 2010. március 8.)