Template szintaxis

Czirkos Zoltán · 2019.02.27.

A sablon osztályok és sablon függvények szintaxisa

1. Sablon osztályok és függvények definíciója

A sablon kódban bizonyos típusokat (class, typename) vagy egész számokat, pointereket (int, ...) fordítási időben cserélhetünk ki. A függvényeket vagy az osztályokat a szokásos módon írhatjuk meg, azonban előttük, a template kulcsszó után, kacsacsőrök között meg kell adnunk a sablonparamétereket. A sablonparaméterek megadása hasonlóképp működik, mint az egyszerű függvényparamétereknél; név, típus sorrendben:

template <typename T> // név és típus, vö. int x
class SomeClass {
    /* ... */
};

Ez a sablon elsődleges definíciója (base template). Egyszerre két szerepe is van. Először is, deklarálja a sablon nevét, és megadja a sablonparaméterek számát és típusát. Jelen esetben egy SomeClass nevű osztálysablonról van szó, amelynek egy sablonparamétere van, egy típus. Másodszor pedig, ez definiálja a sablont, azaz megadja azt a lefordítandó kódot, amelyben példányosításkor a sablonparaméterek konkrét értékekre cserélődnek.

A sablonoknak létezhet ún. explicit, vagy teljes specializációja (full specialization, hivatalosan: explicit specialization). Ezzel meg tudunk adni bizonyos sablonparaméter-értékek esetére egy olyan kódrészletet, amely eltérő működésű, mint az elsődleges definícióban megadott. A teljes specializációt arról ismerjük meg, hogy a template kulcsszó után üres kacsacsőrpár áll <>, az osztály (vagy függvény) neve után pedig kacsacsőrök között meg van adva a sablonparaméter konkrét értéke:

template <>
class SomeClass<int> {
    /* ... */
};

Az osztály neve után megadott sablonparaméter értékek pontosan olyan számúak és típusúak kell legyenek, mint az elsődleges definíciónál megadottak. Jelen esetben egyetlen int típus van megadva, ami stimmel is számban és típusban is. A template <typename T> szerint egyetlen paraméter kell legyen, egy típus neve, az most int.

Végül pedig, az osztálysablonoknak létezhet részleges specializációja is. Ilyet olyan esetekben használhatunk, amikor a sablonparaméterek bizonyos részhalmazára szeretnénk megadni specializációt. Például ha az elsődleges definíciónál azt mondjuk, az egyik sablonparaméter egy típus, a részleges specializációnál tekinthetjük a típusokon belül a pointerek részhalmazát:

template <typename T>
class SomeClass<T*> {
    /* ... */
};

Ez a szintaktika ötvözi az elsődleges definíció és az explicit specializáció elemeit. A template kulcsszó után meg van adva egy sablonparaméter, <typename T>, mivel ez még változhat. Az osztály neve után pedig a <T*> azt adja meg, hogy hogyan kell a sablonparamétereket az eredeti, elsődleges definíció paramétereire illeszteni. Mivel az elsődleges definíció azt mondta, hogy egy darab típusnévből fog állni a sablonparaméterek listája, itt is egy típusnévnek kell lennie; jelen esetben ez egy pointer.

A részleges specializációnál általában már kevesebb sablonparaméter van, mint az elsődleges definíciónál. Például ha a sablonunk két típus sablonparaméterrel rendelkezik, megadhatunk egy részleges specializációt arra az esetre, amikot két egyező típust ad meg a használó:

template <typename T1, typename T2> // Sablonparaméterek: két típus
class SomeClass {
};


template <typename T>
class SomeClass<T, T> {             // Két paraméter, de már csak egy típus
};

Ez nincs szükségszerűen mindig így. Extrém, de egyáltalán nem szokatlan esetben előfordulhat az is, hogy a részleges specializációnak több sablonparamétere van, mint az elsődleges definícióban megadott paraméterek:

template <typename T>
class SomeClass {               // Sablonparaméter: egy típus
    /* ... */
};

template <typename RET, typename ARG>
class SomeClass<RET (*)(ARG)> { // Egy típus: valamilyen függvénypointer
    /* ... */
};

A fenti fogalmazás azonban elég pongyola volt. A specializációnak ugyanúgy egyetlen egy sablonparamétere van, mint ahogyan az elsődleges definícióban megadott sablon osztálynak: typename TRET (*)(ARG). Ez nem is lehet másképp! Csak most a RET (*)(ARG) függvénypointer típusban több típusnév is cserélgethető, a paraméter és a visszatérési érték típusa is. Az elsődleges definíció sablonparaméteréhez képest ez még így is részhalmaz, mert az összes típus halmazának részhalmaza az egyparaméterű függvényekre mutató függvénypointerek halmaza.

Épp a függvények kapcsán fordul elő gyakran, hogy olyan sablont adunk meg, amelynek elsődleges definíciója egyáltalán nincs, csak részleges specializációja. Ez így néz ki:

template <typename FUNCPTR>
class SomeClass;            // csak deklaráció

template <typename RET, typename ARG>
class SomeClass<RET (*)(ARG)> {
    /* ... */
};

Ezt a sablont csak egyparaméterű függvényre mutató pointerrel lehet példányosítani, semmilyen más típussal. Ha példányosításkor egy függvényre mutató pointer típust adunk meg, a fordító a részleges specializációt fogja használni, és annak a kódjában külön sablonparaméterekként látjuk a visszatérési érték és a paraméter típusát is. Ha bármi mással példányosítunk, akkor a fordító az elsődleges sablont választja ki, amihez viszont nincsen definíció, így a fordítás leáll.

Látszik, hogy az elsődleges definíciók és a specializációk szintaxisa nem túl logikus. Az előbbieknél a sablonparaméterek számát a template kulcsszó utáni kacsacsőrökben kell megadni, az utóbbiaknál pedig már az osztály vagy függvény neve után. Logikusnak tűnhet, hogy ezt egységesítsük:

template <typename T> class MyClass;
template <typename T> class MyClass<T> { /* ... */ };   // HIBÁS!

Ezt azonban nem lehet, nem engedi a szabvány. Ahogyan függvények részleges specializációját sem engedi – azt azért nem, mert összeakadna egy másik nyelvi elemmel, a függvénynevek túlterhelésével.

Az elsődleges definíció hiánya a C++11-es változó argumentumszámú sablonoknál is teljesen szokásos dolog:

template <int... NUMBERS>        // tetszőlegesen sok, 0...∞ darab integer
class SomeClass;

template <int HEAD, int... TAIL>
class SomeClass<HEAD, TAIL...> { // specializáció az 1...∞ esetekre
    /* ... */
};

template <>
class SomeClass<> {              // specializáció a 0 paraméter esetére
    /* ... */
}

A nulla elem, és az egytől végtelenig bármennyi elem, közösen kiadják a nullától végtelenig bármennyi elem halmazt. Az első deklarációnak itt is csak annyi a szerepe, hogy megadja, hogyan hívják az osztályt, és milyen sablonparaméterei vannak.

2. A template kulcsszó egyéb jelentései

A fenti példákban a template kulcsszót mindig kacsacsőr követte. Ezt a zárójelpárt akkor is ki kell írni, ha az teljesen üres (a teljes specializációnál). Ha nem tesszük, akkor a kódrészlet teljesen mást jelent: explicit példányosítást. A sablonok általában automatikusan példányosodnak, igény szerint, minden fordítási egységben, ahol használják őket. Ezért kell a definíciókat is a fejlécfájlokba tenni. Ha explicit példányosítást használunk, akkor egy adott sablonfüggvény, vagy sablonosztály összes tagfüggvénye, példányosodik a fordítási egységben, akkor is, ha nem használjuk:

template class SomeClass<int>;

Így ha csak néhány konkrét típussal használjuk a sablonunkat, a definíciója áthelyezhető lehet egy forrásfájlba (.cpp) a fejlécfájlból (.h). Ezáltal egy nagy projekt fordítása gyorsabb lehet.

Ennek éppen az ellenkezőjét jelenti az, ha egy ilyen sor elé még az extern kulcsszót is tesszük C++11-ben: az kifejezetten tiltja a sablon példányosítását az adott fordítási egységben. Ahhoz, hogy egy ilyen program linkelhető legyen, egy másik fordítási egységben példányosítva kell lennie a hivatkozott sablonnak.

3. Összefoglaló táblázat

template <typename T> T less(T, T); template után van kacsacsőr, deklarált név után nincs: sablon osztály vagy függvény általános, elsődleges deklarációja: ; vagy definíciója: { ... }.
template <> int less<int>(int, int); template után és név után is van kacsacsőr: sablon osztály vagy függvény specializációja. Lehet deklaráció ; és definíció { ... } is.
template int less<int>(int, int); template után nincs, de a név után van kacsacsőr: explicit példányosítás, azaz példányosítás kérése akkor is, ha használattal az adott fordítási egységben nem találkozik a fordító.
extern template int less<int>(int, int); C++11, példányosítás tiltása az adott fordítási egységben, mivel tudjuk, hogy máshol biztosan példányosodik a sablon, és linkelhető lesz a program.

4. Irodalom