Erős típusok 2.

Czirkos Zoltán, Dobra Gábor · 2026.09.06.

1. tag struct

class Complex példa

class Image vs class ImageWindow

2. Any – type erasure

A C++-ban többféle generikusság is létezik. A sablonok lehetővé teszik azt, hogy egy osztály vagy függvény forráskódjában egy típust fordítási időben kicseréljünk: így létre tudjuk hozni egész számok, valós számok, valamilyen objektumok listáját. A heterogén kollekciókban valamilyen ősosztálybeli objektumokat (pontosabban azok pointereit) tároljuk, és ott a különböző típusú objektumok futási időben cserélhetőek.

Időnként előfordul az, hogy egy olyan változót szeretnénk létrehozni, amelyik „tényleg generikus”. Mit jelent ez? Egy ilyen változó tetszőleges típusú értéket fölvehet, legyen az akár beépített, akár felhasználó által definiált. Továbbá képes arra, hogy dinamikusan, futási időben típust váltson. Olyasmi kellene legyen ez a változó, mint a C-s void* pointer, ami bármire mutathat. Itt is a programozónak kell majd tudnia, hogy épp milyen típusú értéket tárol a változó, de szerencsére ez a C-s verziónál okosabban megcsinálható, futási idejű ellenőrzésekkel, és érték szerinti másolással.

Gondoljuk meg, mire lenne itt szükségünk!

int main() {
    Any a;

    a.set(5);
    std::cout << a.get<int>();

    a.set(std::string("hello"));
    std::cout << a.get<std::string>();
}

A változó itt egyszer int, máskor std::string típusú értéket tárolt el. A .set() tagfüggvény sablonfüggvény kell legyen, hogy tetszőleges típusú paramétert át tudjon venni. Igazából ugyanúgy sablon, mint a .get(), ahol a programozó adja meg a típust. Csak az előbbinek van paramétere, amiből a fordító a sablonparamétert le tudja vezetni. A .get()-nél erre nincsen lehetőség, mivel annak nincs paramétere.

A .set() függvénynek adott objektumok eltérő méretűek lehetnek, mégis ugyanannak az Any típusú, előre lefoglalt memóriaterülettel rendelkező változónak kell eltárolnia őket. Ez csak úgy oldható meg, ha a tényleges tárolás az Any objektumon kívül történik. Ezért dinamikus memóriakezelést kell használnunk: az így lefoglalt objektumnak az Any csak a memóriacímét fogja tárolni. Ahányféle objektumot kap a .set(), annyiféle objektumot kellhet tárolni; így ha a .set() sablonfüggvény, akkor a dinamikusan létrehozott objektumnak is sablonnak kell lennie.

A new int és a new std::string kifejezésektől kapott int* és std::string* típusú pointerek nem kompatibilisek egymással. Az Any tárolhatna void*-ot, de jobban járunk, ha az eltárolandó objektumot becsomagoljuk egy segédobjektumba, a segédobjetumok számára pedig létrehozunk egy közös ősosztályt. Tulajdonképpen egy egyelemű heterogén „kollekciót” kapunk (ahol a kollekció nem véletlenül van idézőjelben, már csak a tervezési mintára való utalásként szerepel):

class Any {
    class ContainerBase {                    // Üres ősosztály
      public:
        virtual ~ContainerBase() = default;
    };

    template <typename T>
    class Container final : public ContainerBase { // Container<T>::data tárol
      public:
        T data;
        Container(T const &what) : data(what) {}
    };

    ContainerBase* pdata = nullptr;          // Emiatt kell az ősosztály
};

A módszer neve: type erasure. Ebben a közös ősosztály „csak azért van, hogy legyen”, mert kell a közös ős.

A .set() és a .get() függvények ezután már könnyen megvalósíthatóak. A .set() megadott típusú csomagoló objektumot foglal, és beállítja a pointert. Ezen a ponton elveszik a típusinformáció, de azt a .get() újból megkapja a sablonparaméterében, és egy dynamic_cast visszaalakítja a segédobjektum referenciáját a megfelelő típusúra:

template <typename T>
void Any::set(T const& what) {
    ContainerBase* newpdata = new Container<T>(what);
    delete pdata;
    pdata = newpdata;
}

template <typename T>
T& Any::get() {
    return dynamic_cast<Container<T>&>(*pdata).data;
}

A dynamic_cast futási idejű hibaellenőrzését itt ki is lehet használni. Ha helytelen a konverzió, std::bad_cast típusú hibát fog dobni (referenciák esetén így viselkedik). A dynamic_cast-hoz szükség van virtuális függvényre, de ez a feltétel szerencsére automatikusan teljesül az ősosztálybeli virtuális destruktor miatt, amire amúgy is szükségünk lenne.

Már csak egy dolgot kell megoldanunk, az Any objektumok érték szerinti másolását. A ContainerBase* indirekció miatt a fordító által generált másoló konstruktor helytelenül működik, csak a pointert másolja, nem az objektumot. A pointer által mutatott objektum dinamikus típusa azonban ismeretlen, ezért a szokásos trükköt kell alkalmaznunk: a virtuális konstruktort. Ez egy újabb virtuális függvény a segédobjektum számára. Szerencsére a .clone()-t most csak egyszer kell implementálnunk, a sablon osztályban:

class Any {
  private:
    class ContainerBase {
        virtual ContainerBase* clone() const = 0;
    };

    template <typename T>
    class Container final : public ContainerBase {
        ContainerBase* clone() const override { return new Container<T>(*this); }
    };

    ContainerBase* pdata;
};

template <typename T>
Any::Any(Any const& to_copy) {
    pdata = to_copy.pdata->clone();
}

A teljes változat letölthető innen: any.cpp. A C++17 már tartalmaz egy ehhez hasonlóan működő osztályt, [std::any néven](/cpp14std).