Jazyk C++
Šablonová magie
V minulých dílech našeho programátorského seriálku pro pokročilé jsme vás seznámili s metaprogramováním, šablonovými třídami rysů a šablonami výrazů. Nyní si na závěr ukážeme, jak tyto techniky kombinovat. Pomocí metafunkcí budeme definovat třídy rysů, které popisují obvyklé aritmetické konverze a celočíselná rozšíření při vyhodnocování výrazů.

Syntéza typu
Aritmetické konverze jsou běžnou součástí vyčíslování aritmetických výrazů. Například když sčítáme číslo typu double a číslo typu int, je výsledek typu double. Tak je psáno ve standardu jazyka a překladače to zvládnou. Pokud ale použijeme například vektor parametrizovaný typem dat
template <class T> class Vektor { /*...*/ };
a chceme sečtením vektorů typů Vektor<double> a Vektor<int> dostat výsledek typu Vektor<double>, nemůžeme čekat pomoc od překladače a musíme si to naprogramovat sami.
Úkol je tedy jasný – naučit překladač používat aritmetické konverze i pro argumenty šablon. Jak na to? Jednu z možností jsme už naznačili v článku o třídách rysů – pro každou dvojici typů definovat pomocí explicitní specializace typ výsledné hodnoty. Zopakujme si, jak by to vypadalo:
template <class T1, class T2>
struct Vysledek
{
};

template <>
struct Vysledek<int, double>
{
   typedef double TYP;
};

template <>
struct Vysledek<double, int>
{
   typedef double TYP;
};

// ... a tak dál
Je sice pravda, že počet těchto kombinací je konečný, neboť konečný je i počet vestavěných typů, ale stále to připomíná metodu hrubé síly. Teď si ukážeme, že to lze zvládnout elegantněji, a to pomocí metafunkcí. Metafunkce bude mít dva vstupní parametry – typy – a bude vracet výsledný typ. Mimo jiné půjde o pěkný příklad třídy rysů definované pomocí metafunkce.

Jak to vidí standard
Pro připomenutí si zde uveďme pravidla, podle kterých se řídí aritmetické konverze při zpracování výrazů. Připomeňme, že nás zajímají jenom takové situace, kdy parametry operátorů jsou číselné nebo výčtové typy. Začneme u binárních operátorů (standard, část 5.0.9). Následující pravidla se nazývají obvyklé aritmetické konverze:
Jestliže jeden z operandů je typu long double, pak druhý by měl být převeden na typ long double.
Jinak, jestliže jeden z operandů je typu double, pak druhý by měl být převeden na typ double.
Jinak, jestliže jeden z operandů je typu float, pak druhý by měl být převeden na typ float.
Jinak by se měly oba operandy podrobit celočíselnému rozšíření.
Potom, jestliže jeden z operandů je typu unsigned long int, pak druhý by měl být převeden na typ unsigned long int.
Jinak, jestliže jeden operand je typu long int a druhý typu unsigned int, pak jestliže long int může reprezentovat všechny hodnoty typu unsigned int, operand typu unsigned int by měl být převeden na long int; jinak by oba operandy měly být převedeny na unsigned long int.
Jinak, jestliže jeden z operandů je typu long int, pak druhý by měl být převeden na typ long int.
Jinak, jestliže jeden z operandů je typu unsigned int, pak druhý by měl být převeden na typ unsigned int.
(Dále už zbývá jen možnost, že oba operandy jsou typu int.)
Celočíselné rozšíření je popsáno ve standardu v části 4.5. Zjednodušeně řečeno: Malé celočíselné typy (char, signed char, unsigned char, short, unsigned short, wchar_t, bool) a výčtové typy mohou být převedeny na nejmenší větší celočíselný typ (nejméně však int nebo unsigned int), který pokrývá celý jejich rozsah.
V případě unárních aritmetických operátorů (tj. +, -) se pro celočíselné a výčtové typy vždy provede celočíselné rozšíření. Tedy například jestliže promenna je typu char, pak +promenna nebo ‑promenna je typu int.
Překladač všechna tato pravidla zná, ale pokud je chceme použít i pro šablonové argumenty tříd či funkcí, musíme jim překladač znovu naučit – a to právě pomocí metakódu.

Celočíselná rozšíření
Začneme s celočíselným rozšířením. Vytvoříme metafunkci (třídu rysů), která bude mít za parametr typ. Pro většinu typů vrátí ten samý typ, ale pro malé celočíselné typy vrátí jejich celočíselné rozšíření.
// primární šablona
template <class T> 
struct CelociselneRozsireni
{
   typedef T RESULT;
};

// explicitní specializace pro 
// malé celočíselné typy
template <> 
struct CelociselneRozsireni<char>
{
   typedef int RESULT;
};
// ...
A tak podobně pro ostatní malé celočíselné typy – vše naleznete na Chip CD 5/01 ve zdrojovém souboru aritmeticke_konverze.h. Pokud se v programu objeví
CelociselneRozsireni<double>::RESULT
nepůjde o nic jiného než o typ double; zatímco
CelociselneRozsireni<char>::RESULT
znamená typ int, neboť se provede rozšíření.

Uspořádání typů
Zde uvažujeme typy int, unsigned int a všechny další vestavěné typy s větším rozsahem, neboť spoléháme na předchozí metafunkci pro celočíselné rozšíření. Viděli jsme, že aritmetické konverze jsou uspořádány podle rozsahu daných typů. Proto definujeme pomocnou metafunkci (třídu rysů), která uspořádá typy podle rozsahu. Nejnižší pořadí bude mít typ int, nejvyšší pak long double. Primární šablona bude tentokrát prázdná – tím sice značně omezíme počet použitelných typů jen na těch pár vestavěných, ale pro naše účely to zatím stačí.
// primární šablona
template <class T>
struct PoradiTypu
{
};

// pro typ int
template <> PoradiTypu<int>
{
   static const int Poradi = 1;
};

// pro typ unsigned
template <> PoradiTypu<unsigned>
{
   static const int Poradi = 2;
};

// ...
Pro ostatní typy se postupuje analogicky. Zbytek naleznete opět ve zdrojovém souboru aritmeticke_konverze.h. 
To byl první krok. Teď musíme popsat, co se stane, když se potkají dva typy v binárním operátoru. Pravidlo známe: vybere se ten s větším rozsahem a druhý typ se na něj převede (pokud nejsou stejné). Potřebujeme tedy implementovat rozhodovací schéma.

Rozhodovací schéma
Pomocí metafunkce chceme rozhodnout, který z typů T1 a T2 má vetší rozsah. Rozhodování se řídí pravidlem, že větší rozsah má typ s vyšším pořadím. Rozhodovací metafunkce nám odpoví na otázku, zda má typ T1 větší rozsah (je lepší).
template <class T1, class T2> 
struct JeTypT1Lepsi
{
   static const bool RESULT = 
      (PoradiTypu<T1>::Poradi > 
      PoradiTypu<T2>::Poradi);
};
Toto je velmi jednoduché rozhodovací schéma, v němž jsme se omezili pouze na vestavěné aritmetické typy. Dalo by se ovšem rozšířit i o zpracování tříd, např. std::complex<T>. Zájemce odkazujeme na knihovnu Blitz++ (viz infotipy).
Už máme rozhodovací metafunkci, ale ještě se neumíme postarat o výběr správného typu. Napíšeme proto další pomocnou metafunkci:
template <class T1, class T2, bool VYBER_T1 = true>
struct VyberLepsiTyp
{
   typedef T1 RESULT;
};

// částečná specializace
template <class T1, class T2>
struct VyberLepsiTyp<T1, T2, false>
{
   typedef T2 RESULT;
};
Je jistě zřejmé, že třetím argumentem šablony bude výsledek rozhodovací metafunkce. Jde v podstatě o implementaci úplného meta-if. V případě splnění podmínky (T1 je lepší) se vybere první typ (T1), v opačném případě druhý typ (T2).

A je to tady!
Nyní už můžeme přistoupit k definici výsledné metafunkce implementující obvyklé aritmetické konverze:
template <class T1, class T2>
struct AritmetickaKonverze
{
private:
   // nejprve celočíselné rozšíření
   typedef typename 
      CelociselneRozsireni<T1>::RESULT T1X;
   typedef typename 
      CelociselneRozsireni<T2>::RESULT T2X;
public:
   // konečný výsledek
   typedef typename 
      VyberLepsiTyp<T1X, T2X, 
         JeTypT1Lepsi<T1X, T2X>::RESULT
         >::RESULT RESULT;
};
A to už je vše. Takže jakmile se v programu objeví
AritmetickaKonverze<int, double>::RESULT
AritmetickaKonverze<char, double>::RESULT
půjde v obou případech o typ double. 
Pokud si vzpomínáte na funkci soucet z článku o třídách rysů, pak její vylepšená verze by vypadala takto:
template <class LEVY, class PRAVY>
typename AritmetickaKonverze<LEVY, PRAVY>::RESULT
soucet(LEVY levy, PRAVY pravy)
{
   typedef typename
      AritmetickaKonverze<LEVY, PRAVY>::RESULT result_t;
   return result_t(levy + pravy);
}

Příklad praktického použití
Minule jsme vytvořili implementaci šablon výrazů, ale bez syntézy typu. Nyní to, jak jsme slíbili, vylepšíme. Původní implementaci šablon výrazů naleznete na Chip CD v souboru et.cpp a nová, vylepšená implementace je v souboru et2.cpp. Zde se zaměříme pouze na změny (v dalším textu zvýrazněny). Předně je třeba upravit třídy rysů pro elementární operace.

template <class TYP>
struct UnarniMinus
{
   typedef typename
      CelociselneRozsireni<TYP>::RESULT T;
   static T apply(TYP x)
   {
      return -x;
   }
};

template <class T1, class T2>
struct BinarniMinus
{
   typedef typename
      AritmetickaKonverze<T1, T2>::RESULT T;
   static T apply(T1 x, T2 y)
   {
      return x - y;
   }
};
Dále musíme upravit operátory. Zde uvedeme pouze deklarace, definice naleznete ve zdrojovém souboru et2.cpp.
template <class V1, class V2>
Vyraz<BinaryOp<Vyraz<V1>, 
    Vyraz<V2>, BinarniPlus<
       typename Vyraz<V1>::T>,
       typename Vyraz<V2>::T> >
operator +(const Vyraz<V1>& v1,
    const Vyraz<V2> & v2);

template <class T1, class T2>
Vyraz<BinaryOp<
    Promenna<Vektor<T1> >,
    Promenna<Vektor<T2> >,
    BinarniPlus<T1, T2> > >
operator +(const Vektor<T1>& v1,
    const Vektor<T2> & v2);

template <class T, class V>
Vyraz<BinaryOp<
    Promenna<Vektor<T> >,
    Vyraz<V>, 
    BinarniPlus<T,
       typename Vyraz<V>::T> > >
operator +(const Vektor<T>& v1,
    const Vyraz<V> & v2);
Pomocné adaptéry (Vyraz, UnaryOp, BinaryOp) není třeba měnit. Jsou implementovány natolik obecně, že si bez problémů poradí i se syntézou typu. 
A nyní jednoduchý příklad. Mějme dva vektory
Vektor<int> a;
Vektor<double> b;
Jakého typu budou složky vektoru a + b? 
Přetížený operátor + vrátí třídu typu
Vyraz<BinaryOp<Promenna<Vektor<int> >, 
    Promenna<Vektor<double> >, 
    BinarniPlus<int, double> > >
Odtud můžeme hledaný typ “vypočítat” – zjednodušeně zapsáno:
Vyraz<...>::T = BinaryOp<...>::T = 
   = BinarniPlus<int, double>::T =
   = AritmetickaKonverze<int, double>::RESULT =
   = double
Takže výsledek je typu Vektor<double>. Pro složitější výrazy je situace obdobná. Když se na to podíváme z pohledu šablonového metaprogramování, můžeme typ Vyraz<...> považovat za metastrom – analogii známé datové struktury. Syntéza typu je pak rekurzivní zpracování metadat (typů) v metastromu, to vše v době překladu.

Závěr?
Tímto článkem prozatím končí náš miniseriál věnovaný pokročilým programovacím technikám pomocí šablon. Končí ale definitivně? Přiblížili jsme si koncepty metaprogramování, tříd rysů, šablon výrazů, ale na hodně dalších zajímavých záležitostí se nedostalo. Nemluvili jsme například o typových metaseznamech, třídách politik, multimetodách a o řadě dalších pozoruhodných programovacích technik. Možná se k uvedeným tématům ještě někdy vrátíme.

Jaroslav Franěk

infotipy
Blitz++: 
http://oonumerics.org/blitz
PETE: 
http://www.acl.lang.gov/pete
Standard C++: 
International standard ISO/IEC 14882, 1998-09-01
Související články:
Šablony po šesti letech, Chip 12/00, str. 192 – 196
Dřinu nechte překladači!, Chip 1/01, str. 142 – 145
Specializace trochu jinak, Chip 2/01, str. 142 – 144
Šablony výrazů v C++, Chip 4/01, str. 172 – 175
Ukázky programů: 
Chip CD 5/01, rubrika Chip Plus
-------

Errata
V článku “Šablony výrazů v C++” v minulém čísle se bohužel vyskytlo několik nedopatření, za které se vám autor touto cestou omlouvá:
– V definici operátoru = (strana 173 uprostřed) chybí příkaz return *this; – ve zdrojovém souboru však je vše v pořádku. 
– Při počešťování jmen proměnných (kvůli větší přehlednosti) došlo k několika opomenutím. Takže UnaryOp a UnarniOp je stejná třída, stejně jako BinaryOp a BinarniOp. Zdrojový kód ale používá UnaryOp a BinaryOp. 
– Největší nepříjemností je popis “Jak to funguje”, kde se v závěru pomíchala písmenka a, b, c.