A typename kulcsszó

Czirkos Zoltán · 2022.06.21.

Templatelt osztályból öröklődés rejtelmei

Mit jelent az alábbi kód?

void f() {
    A::B * C;
}

Valójában a kérdésnek két megoldása lehet:

  • A osztálybeli B belső típusú objektumra mutató pointer, amelynek a neve C. Tehát a *C kifejezéssel egy A::B típusú objektumot érnénk el. Konkrét példa: string::iterator * p.
  • A osztálybeli B nevű statikus változó, amit összeszorzunk C-vel.

A félreérthetőséget az okozza, hogy a belső típust és a statikus változót ugyanazzal a szintaxissal kell elérni; mindkettőt a :: scope operator segítségével tudjuk hivatkozni.

1. Belső osztályok és sablonok

A fentiekkel alapesetben nincsen gond. Bár a fordítóprogramok tervezésekor nehézséget jelent, hogy a sor jelentése kontextusfüggő – bele kell néznie az A osztályba, hogy kiderüjön a kódsor jelentése –, az A osztály vizsgálata után egyértelmű a kód.

Kivétel persze akkor, ha sablon kódban vagyunk:

template <typename A>
void f() {
    A::B * C;
}

Jelen pillanatban ugyanis a kódrészlet mindkét verzióvá „átváltozhatna”. Ha a sablonparaméter egy olyan osztály, amelyikben B belső típus, akkor C-t egy pointerként lehetne értelmezni:

class Egyik {
  public:
    class B {};
};


f<Egyik>();

/* A keletkező specializáció: */
template <>
void f<Egyik>() {
    Egyik::B * C;
}

Viszont ha egy másik osztállyal példányosítjuk a sablont, amelyben a B egy statikus változó, akkor ez ismét szorzásnak tűnik:

class Masik {
  public:
    static int B;
};


f<Masik>();

/* A keletkező specializáció: */
template <>
void f<Masik>() {
    Masik::B * C;
}

Hogyan oldható fel ez a kétértelműség? Úgy, hogy a kódban jeleznünk kell a fordító számára, hogy melyikről van szó; vagyis a :: scope operator használata esetén megjelöljük, hogy statikus változóra vagy belső típusra kell gondolnia. A szabály egyszerű:

  • Ha nem mondunk semmit, statikus változónak tekinti.
  • Viszont ha belső típusra szeretnénk hivatkozni, akkor elé kell írnunk, hogy typename.
template <typename A>
void f1() {
    A::B * C;          // statikus változózó
}

template <typename A>
void f2() {
    typename A::B * C; // belső típus
}

A::B-t ilyenkor függő névnek (dependent name) nevezzük; a jelentése ugyanis függ az A sablonparamétertől. A függés lehet közvetlen, vagyis a sablonparaméter maga lehet az az osztály, amelyből egy belső típust felhasználunk:

template <typename CONTAINER>
void f() {
    typename CONTAINER::iterator * ptr;
}

De lehet közvetett is, például:

template <typename T>
void f() {
    typename std::vector<T>::iterator * ptr;
}

A probléma itt is fennáll, hiszen amíg a T nem ismert, addig nem lehet tudni, a vektornak melyik specializációjával dolgozunk. Ne feledjük, csak számunkra egyértelmű ennek a kódnak a jelentése: a vektor, iterátor, ptr nevek elárulják, mire gondolunk. A fordító számára viszont ezek nem jelentenek semmit, az csak ennyit lát:

template <typename IZEBIGYO>
void f() {
    typename VALAMI::AKARMI<T>::BARMI * BLABLA;
}

2. Ősosztályokban

Ez egész problémakör egyébként teljesen más kontextusokban is előkerülhet:

template <typename T>
class Alap {
  public:
   int tagvaltozo;
   class BelsoTipus {
       ...
   };
   ...
};

template <typename U>
class Leszarmazott: public Alap<U> {
    void fv1() {
        std::cout << tagvaltozo; // 1
    }
    void fv2() {
        BelsoTipus x; // 2
    }
};

Az 1-essel jelölt helyen nem lehet tudni, hogy tagváltozóról, vagy globális változóról van szó. Ha Alap<U> rendelkezik ilyen nevű attribútummal, akkor ez lehetne az is. De ha nem – márpedig létezhet olyan specializációja, amelyiknek nincs ilyenje –, akkor ez egy globális változó. (Ha ez a helyzet, és tényleg van ilyen nevű globális változó, akkor a fordító értelmezni tudja majd a kódot!) Egyértelművé tehetjük ezt az ősosztály megjelölésével, vagy a this kiírásával:

void fv1() {
    std::cout << Alap<U>::tagvaltozo;
}

void fv1() {
    std::cout << this->tagvaltozo;
}

A 2-essel jelölt helyen ugyanez a helyzet: nem lehet tudni, hogy az ősosztály belső típusáról van szó, vagy egy globálisan létező típusról. Alapértelmezetten globális típusról, tehát a belső osztály hivatkozását kell egyértelműsíteni:

void fv2() {
    typename Alap<U>::BelsoTipus x;
}

Mivel ilyenkor függő nevet hivatkozunk (Alap<U> függ az U sablonparamétertől), ezért a typename kulcsszó is előkerül. Amelyik fordító enélkül is lefordítja a kódot, az nem követi a szabványt.