Cast-ok

Czirkos Zoltán · 2019.02.27.

Néhány szó a cast-okról

Ez az írás jelentős átfedéseket tartalmaz a jegyzet 7. fejezetének 8. pontjával. Ha úgy érzed, maradtak olyan pontok, amiket nem sikerült tökéletesen megértened, érdemes lehet ennek ellenére elolvasnod.

1. A C és a C++ castok

A C-ben a cast-oláshoz (típuskonverzióhoz) az értéket előállító kifejezés elé kellett írni zárójelben, hogy milyen típusúvá szeretnénk konvertálni azt. Ez tulajdonképpen egy egyoperandusú prefix operátor:

int a = 2, b = 3;

double c = a / b;       // 0
double d = (double) a / (double) b;     // 0.6667

Ez egy univerzális konverzió, lényegében bármilyen típusú értékből bármilyet tud csinálni. Történjen a cast során bármi:

  • int *void *, generikus programozáshoz.
  • Point *char *, memória mágiához.
  • intdouble az egész osztás elkerüléséhez.
  • int const *int * interfész konstansok miatt.

Az egyik probléma a zárójeles cast szintaxissal tehát az, hogy nem látszik a kódon a cast célja. A másik probléma, hogy ezek a cast-ok a programban észrevétlenek, nem lehet rájuk keresni. A kerek zárójelet ezer más dologra használjuk.

Ezért a C++-ban új operátorokat vezettek be a típuskonverzióhoz. Az új operátorok nevein látszódik a típuskonverzió célja, továbbá egyedi kulcsszavak lévén könnyű megtalálni őket a programban.

Az új cast-ok template függvényekhez hasonló szintaxissal látszanak a kódban: valamilyen_cast<cél típus>(eredeti érték). A feladattól függően const_cast-ot, static_cast-ot, dynamic_cast-ot vagy reinterpret_cast-ot használunk; a módszertől függően pedig fordítási, esetleg futási idejű ellenőrzést kapunk.

2. const_cast

A const_cast célja, hogy a const minősítőt le tudjuk varázsolni egy pointer vagy egy referencia által mutatott objektumról. Tehát ezt pl. T const *T * konverzióra használhatjuk, egyéb fajta konverziók fordítási hibához vezetnek.

Ez az egyetlen egy olyan cast, amelyik a const-ot le tudja venni.

3. static_cast

A static_cast segítségével egymással „többé-kevésbé kompatibilis” típusok között konvertálunk. Ez olyan konverziókra való, amelyek jogosak, de amelyeket a fordító magától nem végezne el. Például:

  • Nem karaktert, hanem karakterkódot szeretnénk kiírni (overload kiválasztása):
    std::cout << static_cast<unsigned int>('A');
  • Egész osztás elkerülése:
    int a = 2, b = 3;
    std::cout << static_cast<double>(a) / static_cast<double>(b);
  • Upcast osztályhierarchiában, ha tudjuk, hogy a cast helyes (bár erre inkább a dynamic_cast való):
    Alakzat* a = alakzat_letrehoz(ALAKZAT_TEGLALAP);
    Teglalap* t = static_cast<Teglalap*>(a);

4. dynamic_cast

A dynamic_cast segítségével osztályhierarchiákban ugrálhatunk. Ez a cast, ahogy a neve is mutatja, futási idejű ellenőrzést is végez, és hibajelzést ad, ha a cast helytelen. Ehhez a futási idejű ellenőrzéshez virtuális függvénytáblára van szüksége, tehát ez csak polimorf osztályokon, azaz legalább egy virtuális függvénnyel rendelkező osztályokon működik. (De ha más nem, egy virtuális destruktor úgyis mindig van.) Ha pointert castolunk, helytelen típus esetén null értékű pointert ad:

Alakzat* a = alakzat_letrehoz();        // ad egy ismeretlen típusú alakzatot
Teglalap* t = dynamic_cast<Teglalap*>(a);
if (t == nullptr) {
    std::cout << "Nem téglalap";
} else {
    std::cout << "Téglalap, " <<
              << t->szelesseg() << "x" << t->magassag();
}

Referenciák esetén pedig std::bad_cast típusú kivételt dob:

void valami(Alakzat &a) {
    try {
        Teglalap& t = dynamic_cast<Teglalap&>(a);
        std::cout << "Téglalap, " <<
                  << t.szelesseg() << "x" << t.magassag();
    } catch(std::bad_cast) {
        std::cout << "Nem téglalap";
    }
}

Osztályhierarchiák esetén jobb elkerülni a cast-ok használatát. A túl sok (nullánál több? :D) a dynamic_cast egy code smell, ami helytelen tervezésre utal; valószínűleg az ősosztályokból hiányoznak virtuális függvények.

Virtuális öröklés esetén mindenképp dynamic_cast-ot kell használni. Ott ugyanis az objektumrészletek az egész objektumhoz képest nem nulla ofszeten kezdődnek, és az ofszetek a virtuális öröklés miatt eltérőek lehetnek a leszármazottakban. A pointerek közti eltolást a virtuális táblákból lehet kiolvasni.

5. reinterpret_cast

reinterpret_cast-ot használunk minden egyéb esetben, ami nem fér a fenti kategóriák valamelyikébe, vagy nem rakható össze azokból; olyan típusok között, amelyeknek semmi közük egymáshoz. Ezek tipikusan a „pointermágiát” használó kódrészletek, pl. tetszőleges típusú pointer unsigned char *-gá való konvertálása a memória bájtonkénti elérése céljából. Általában elmondható, hogy a reinterpret_cast nem hordozható kódhoz vezet.

Egy int bájtjainak kiírása:

int x = 0x11223344;
unsigned char* p = reinterpret_cast<unsigned char*>(&x);

for (size_t i = 0; i != sizeof(x); ++i)
    printf("%02x ", p[i]);

Ez nem hordozható kód, a futási eredménye architektúrától függ (int mérete, ábrázolási módja, endianness stb.).

6. Irodalom

  1. Bjarne Stroustrup: The Design and Evolution of C++. Addison-Wesley, 1994.
  2. Code Smell – Wikipedia.