void_t

Czirkos Zoltán · 2022.06.21.

void_t: a template metaprogramozás egyik csodafegyvere

Walter E. Brown kitalált egy miniatűr sablon kódot, ami ilyen egyszerűen hangzik:

template <typename...>
using void_t = void;

Olyan, mintha ez a kódrészlet nem csinálna semmit, de kiderül, hogy mégis: szinte csodafegyvernek nevezhető.

1. A void_t működése: has_iterator

Mit csinál a fenti típusdefiníció? Lényegében semmit. Létrehoz egy sablon típusnevet (alias), amit bármilyen típussal vagy típusokkal példányosítunk, void-ot kapunk eredményül. Tehát void_t<void> = void, void_t<bool, double> = void és így tovább.

Mire jó ez? Arra, hogy SFINAE-vel és specializációkkal együtt használjuk.

Tegyük fel, hogy a feladatunk a következő. Egy olyan sablon osztályra van szükségünk, amelyik megmondja egy tárolóról, hogy az definiál-e iterátor típust. Például A nem ilyen, B viszont igen. Ezért a has_iterator<A>::value értéke hamis, B-vel ugyanennek a metafüggvénynek az értéke igaz kell legyen:

class A {};
class B {
  public:
    class iterator {};
};

int main() {
    std::cout << has_iterator<A>::value << std::endl;   /* false */
    std::cout << has_iterator<B>::value << std::endl;   /* true */
}

A void_t használatával ez ilyen egyszerű:

template <typename...>
using void_t = void;

template <typename, typename = void>
class has_iterator : public std::false_type {};

template <typename C>
class has_iterator<C, void_t<typename C::iterator>> : public std::true_type {};

Nézzük meg jobban a fenti kódot! Adott a has_iterator osztály, amelynek sablonparamétere a vizsgálandó osztály lesz. Van egy második sablonparamétere is, de annak az alapértelmezett értéke void, és láthatóan a használatnál nem is kell kiírni. Adott ezen kívül ennek az osztálynak egy részleges specializációja, amelyben az első sablonparaméter még mindig bármi lehet, a második pedig a void_t alias. Látszik az is, hogy az alap sablon a false_type-ból örököl, a specializáció pedig a true_type-ból, így lesz a value tag értéke hamis vagy igaz, attól függően, hogy az alap sablont vagy a specializált sablont használta a fordító egy adott típusra.

Próbáljuk meg példányosítani ezt az osztályt az A-val! Ebben az esetben a specializált változat elszáll, mivel A::iterator nem létezik. Ezért ezt a szokásos módon, a SFINAE szabály alapján eldobja a fordító. Marad az alap sablon, amelyik a false_type-ból örököl, ezért has_iterator<A>::value értéke false lesz.

Mi lesz a helyzet a B-vel példányosítás esetén? Ilyenkor a B::iterator is valid; void_t<B::iterator> értéke pedig void. Tehát a specializáció példányosítható. Igaz, az alap sablon is, de ilyenkor a fordító a specializációt fogja választani – mert az a specializált változat. Tehát has_iterator<B>::value értéke true lesz.

És ennyi, meg van oldva a feladat.

Itt már látszik egyébként, hogy a void-nak mi a jelentősége: igazából semmi, a void helyett lehetne bármilyen másik típust is használni. Lényeg, hogy ugyanazt a típust adjuk meg az alap sablonnál default paraméterként: typename = void, és a típusdefiníciónál: using void_t = void. Legegyszerűbb a void, mert nem jelent semmit.

2. További példák

Ha meg szeretnénk nézni, van-e egy osztálynak statikus, paraméter nélkül hívható create() függvénye:

template <typename, typename = void>
class has_create : public std::false_type {};

template <typename C>
class has_create<C, void_t<decltype(C::create())>> : public std::true_type {};

Ha arra vagyunk kíváncsiak, van-e az osztálynak x nevű, int típusú adattagja:

template <typename, typename = void>
class has_x : public std::false_type {};

template <typename C>
class has_x<C, void_t<decltype(&C::x)>> : public std::is_same<int C::*, decltype(&C::x)> {};

Az is_same vizsgálat az adattag típusát ellenőrzi.

Ha meg szeretnénk vizsgálni, az iterátorunk biztosít-e véletlen elérést (random access iterator), pl. rendelkezik-e az iterator + integer, iterator - iterator műveletekkel, amikre az algoritmusunknak szüksége van:

template <typename, typename = void>
class is_random_access : public std::false_type {};

template <typename I>
class is_random_access<I, void_t<decltype(I() + 0), decltype(I() - I())>> : public std::true_type {};

Látszik, hogy a typename... miatt bármennyi vizsgálandó kifejezést megadhatunk. Ha bármelyik részlet SFINAE miatt elszáll, a specializáció nem példányosítható, így tulajdonképpen a végső eredményt lényegében az egyes vizsgálatok eredményeinek logikai ÉS kapcsolata adja.

3. A void_t története

A void_t definíciója duplán C++11-es: a typename... miatt és a template<> using miatt is. De minden, ami a következő írásban szerepelni fog, működött volna C++98-ban is, csak akkoriban a void_t-t még nem találták fel. A C++98-as forma:

template <typename>
struct void_t {
    using type = void;
};

A működése ennek ugyanaz: adott egy típus, ami bármit kap, void-ot állít elő. Igaz, itt sablonparaméter is csak egy lehet, de SFINAE használatához ez általában elég.

Kis történeti érdekesség, hogy eredendően ez a void_t trükk nem minden fordítóval működött. GCC-vel például igen, clang-gel pedig nem. Kiderült, hogy ez nem a fordítók hibája, ugyanis a szabvány nem adta meg egyértelműen, hogy egy ilyen kódban mi a teendő, és az egyes gyártók eltérőképpen értelmezték a szöveget. A szabványnál is, nem egy hibáról, hanem egy hiányosságról van szó. A fenti has_iterator metafüggvénynek ugyanis, ha a B-vel példányosítjuk, mindkét változata egyforma. Csak a deklarációkat tekintve:

template <typename, typename = void>
class has_iterator;                                     /* alap */

template <typename C>
class has_iterator<C, void_t<typename C::iterator>>;    /* specializáció */

B-t behelyettesítve, és a void_t-t kifejtve:

template <>
class has_iterator<B, void>;    /* alapból lett */

template <>
class has_iterator<B, void>;    /* specializációból lett */

Tehát az alap sablonnál és a részleges specializációnál is végeredményben egy ugyanolyan formájú teljes specializáció áll elő automatikusan – és az nem volt definiálva a C++98-ban, hogy ilyenkor mi a teendő. Azóta ezt már rögzítették: ilyenkor az eredeti sablonok közül a specializáltat kell választania a fordítónak.