Sablon metaprogramozás

A metaprogramozásról

A meta- előtagot gyakran használjuk arra, hogy valamilyen magasabb szintű abszrakciót jelezzünk;metaadatnak hívjuk azokat az információkat melyek magukról az adatoktól szólnak. Hasonlóan a metaprogram olyan programot jelent, mely programokról szól (például a kimenete egy program).

A programok adatokat dolgoznak fel; a metaprogramok is, csak azokat programoknak is tekinthetjük. (kód = adat pl. LISP) Ami tehát egy programnak bemenő adat, az egy másik programnak maga a kódja.

A metaprogramozás egyidős magával a programozással. Gondoljunk például a fordítóprogramokra, melyeknek mind a bemenete, mind a kimenete egy-egy program, tehát bizonyos szempontból metaprogramok.

Sablonok, típussal való paraméterezés

Az olyan (al)programokat, melyek típusparaméterrel rendelkeznek sablonoknak hívjuk. Egyszerű példa a vermet kezelő osztály, amely adott típusú elemeket kezel oly módon, hogy fordítási időben megállapítható, hogy a veremnek adott elemek típusa megegyezik-e a veremhez tartozó típussal.

Megjegyzés: a sablon fogalmát általánosíthatjuk abban az értelemben, hogy nem csak típust, hanem tetszőleges adatot megengedünk paraméternek például egy szövegrészletet (makró) vagy programkódot.

A sablon metaprogramozás

A sablon metaprogramozás egy olyan programkészítési technika, mellyel elvileg tetszőleges program fordítási idejű kiértékelése elérhető a sablonok által nyújtott lehetőségeken keresztül.

A sablon metaprogramozás elsőként C++-ban jelent meg. Érdekesség, hogy a C++ sablonrendszerét nem metaprogramozásra fejlesztették ki, később a szabványosítási folyamat során derült csak ki, hogy a sablonok kifejező ereje megegyezik a Turing gépekével (azaz tetszőleges programstruktúra leírható velük).

A feladat specifikációja és a program

Programokat formális nyelveken szokás készíteni. Egy formális nyelven leírt specifikációt tekinthetünk tehát programnak. (Most ne foglalkozzunk azzal, hogy egy feladat és a hozzá tartozó program milyen viszonyban áll egymással. Tételezzük fel, hogy a program megoldja az általunk kitűzött feladatot.)

Minél több információt helyezünk el a programszövegben, annál konkrétabb (specifikusabb) lesz a leírásunk, annál kevesebb szabadságfoka (paramétere) van a programnak. Erre egyszerű példa, ha egy paramétert egy rögztített értékkel helyettesítünk. A konkretizálás egyben azt is jelenti, hogy a futtatás szempontjából hatékonyabb kódot generálhatunk a programszövegből.

Vegyünk egy egyszerű példát: rendezzünk egy tetszőleges elemű tömböt illetve egy 3 eleműt. Könnyű látni, hogy míg az első feladatot csak egy általános rendező algoritmus tudja megoldani, a másodikkal már egy néhány elágazásból és cseréből álló egyszerűbb program képes megbírkózni. Ha tudjuk, hogy n<=5, akkor megtehetjük, hogy 1-től 5-ig előállítjuk a rendező programokat és a program indulásakor n alapján elágazunk. Ez bizonyos értelemben a részfeladatok megoldását jelenti.

Fordítási idejú kiértékelés

(Gyakran nevezik ezt parciális kiértékelésnek is.)

A fordítási idejű kiértékelés már nagyon rég óta ismert fogalom, már az első fordítóprogramok is támogatták őket. A programszövegbe írható egyszerűbb kifejezéseket, utasításokat a fordítóprogram a kódba a végrehajtás szempontjából egyszerűbb formában helyezi el. Például

A sablon metaprogramozás abban hoz újdonságot, hogy bonyolultabb feladatok elvégzésére is alkalmassá teszi a fordítóprogramot.

Megjegyzés: Vegyük észre, hogy a lefordított program is egy formális leírás; az eredeti specifikáció egy kibővített és ugyanakkor egyben leegyszerűsített változata. Kibővített annyiban, hogy a fordítóprogram hozzáadja saját tudását (pl. az ismeretet a processzor utasításkészletéről), leegyszerűsített pedig annyiban, hogy a futtatás szempontjából lényegtelen információk kimaradnak a kódból (pl. a változónevek).

Hasonló eszközök

Makrók

Néhány magasszintű nyelv támogatja a makrók használatát. A sablon metaprogramozással közös tulajdonságuk, hogy általában egyszerű feladatokat oldunk meg velük illetve olyanokat, amelyeket bonyolult lenne a megszokott nyelvi eszközökkel megvalósítani. További közös tulajdonság, hogy fokozott figyelmet igényel elkészítésük.

Az alábbi C kódrészlet a MIN makróhívásokat ? : operátorra cseréli a kódban. A zárójelek azért szükségesek, mert nem tudhatjuk, hogy  az aktuális paraméterek milyen precedenciájú operátorokat tartalmaznak. További probléma, hogy az egyik paraméter kétszer is kiértékelődik, egyszer a feltételben, egyszer pedig az eredményben, igy ha a kiértékelésnek mellékhatása van, akkor a második eredmény már különböyű lehet. Látható, hogy mind a definíciónál, mind felhasználásnál látszólag lényegtelen dolgokra is figyelni kell.

#define MIN(a,b) ((a)<(b) ? (a) : (b))

Programszöveg generátorok

Egyes algoritmus osztályokhoz léteznek programszöveg generátorok. Ezek előnye, hogy nem kell 'kézzel' megírni egy hosszú, de viszonylag egyszerű belső szerkezetű programot, hanem elegendő a generátor programot egy tömör paraméterezéssel ellátni. A lexikális és szintaktikus elemző generátorok klasszikus példái az ilyen programoknak; ezek egy nyelvtanból közvetlenül egy elemző programot képesek generálni, melyek az adott nyelvet fogadják el. (pl: lex, yacc, JavaCC, Antlr)

Másik példa a generátorokra a számtalan kisebb nagyobb script (pl. Perl) program, melyeket a fordítás előtt közvetlenül kell futtatni.

A programszöveg generátorok használatát több dolog is motiválhatja:

A programszöveg generálásnak természetesen hártányai is vannak: nem lehet a generált programot közvetlenül módosítani, mert az elveszti a szinkront az eredeti leírással. A programszöveg generátorok ennek ellenére a gyakorlatban rendkívül népszerűek.

Sablon metaprogramozás C++-ban

A C++ metaprogramozási lehetőségeit legkönnyebben példákon keresztül lehet bemutatni. A példák megértéséhez elegendő a C++ nyelv alapos ismerete; a sablon metaprogramozás lényegében egy szellemes felhasználása a nyelvi eszközöknek.

Hasznos példa: faktoriális

Az alábbi példa a gyakorlatban alkalmazható a sablon metaprogramok halmazát illusztrálja. Az ilyen programokkal parciális kiértékelést végezhetünk.

Az alábbi program egy konstans faktoriálisát számítja ki és adja értékül a fact7 változónak. A fordítás a következőképp zajlik: a fordítő elöször a 7 paraméterrel rendelkező Factorial struktúrát próbálja megtalálni, ehhez azonban szükséges a 6-os, ahhoz a 5-ös és így tovább egészen az 1-ig ahol megáll a rekurzió. Itt megtalája a value konkrét értékét a Factorial<1> struktúrának. Ezt felfelé, egészen a kezdetekig visszahelyettesíti a szorzásba és mivel ha a szorzás mindkét oldalán konstans áll, rögtön a szorzatot helyettesíti be a value értékének.

#include<iostream>

template <int N>
struct Factorial
{
    enum { value = N * Factorial<N-1>::value };
};

template <>
struct Factorial<1>
{
    enum { value = 1 };
};

int main()
{
    const int fact7 = Factorial<7>::value;
    std::cout << fact7 << std::endl;
    return 0;
}

Most lássuk, hogy valóban megtörtént-e a fordítási idejű kiértékelés. A lefordított programkódban látható, hogy a fact7 kezdeti értéke pontosan 7! azaz 5040.

	[...]
	0041B197 mov eax,0CCCCCCCCh 
	0041B19C rep stos dword ptr [edi] 
const int fact7 = Factorial<7>::value;
	0041B19E mov dword ptr [fact5],13B0h // 0x13b0 = 5040 = 7!
std::cout << fact7 << std::endl;
	0041B1A5 push offset std::endl (4194B5h) 
	0041B1AA push 13B0h 
	0041B1AF mov ecx,offset std::cout (457668h) 
	0041B1B4 call std::basic_ostream<char,std::char_traits<char> >::operator<<
	[...]

Érdekes példa: Prímszámok

Ez a példa az első C++ metaprogramok közül való. Lefordítani nem lehet, viszont érdekessége hogy a fordító a hibaüzeneteiben rendre a prímszámokat írja ki.

Mivel nem lehet lefordítani, ez a példa egy másik a gyakorlatban kevésbé használható csoportját mutatja be a sablon metaprogramoknak. Ebbe a csoportba tartoznak még azok a metaprogramok is, melyek a C++ önelemzést (self reflection, introspection) valósítják meg, szintén ügyes hibaüzenetekkel.

A példát szemlélve vegyük észre, hogy a D<i,j> osztályoknak j=0 esetén lesz konstruktora, míg a j>0 esetben nem. Az is_prime sablon prim értéke 0 lesz nem prímekre, így a D struktúra létrehozható lesz velük, különben nem, mivel nincs konstruktora D-nek.

// Fordítás: gcc 3.3.1: g++ -ftemplate-depth-50 -c prime.cpp

// Ezek az osztályok okozzák a hibaüzeneteket.
template <int i, int prim> struct D {};
template <int i> struct D<i,0> { D(int);};	

// Ezekkel határozzák meg, hogy egy szám prím vagy sem.
template <int p, int i> struct is_prime {
	enum { prim = ((p%i) && is_prime< (i>2 ? p : 0), i-1>::prim) };
};
template<> struct is_prime<0,1> { enum { prim = 1}; };
template<> struct is_prime<0,0> { enum { prim = 1}; };

// Ez az osztály iterál 2..n között.
template <int i> struct Prime_print {
	Prime_print<i-1> a;
	enum { prim = is_prime<i,i-1>::prim };
	void f()
	{ 	a.f(); 
		D<i,prim> d = prim; 
	}
};
template<> struct Prime_print<2> { 
	enum { prim = 1};
	void f() 
	{
		D<2,prim> d = prim; 
	}
};

void foo() {
	Prime_print<40> a;
	a.f();
}

Az eredmény egy részlete:

prime.cpp: In member function `void Prime_print<2>::f()':
prime.cpp:22: error: conversion from `Prime_print<2>::<anonymous enum>' to 
non-scalar type `D<2, 1>' requested
prime.cpp: In member function `void Prime_print<i>::f() [with int i = 3]':
prime.cpp:18: instantiated from `void Prime_print<i>::f() [with int i = 4]'
[...]
prime.cpp:18: error: conversion from `Prime_print<3>::<anonymous enum>' to 
non-scalar type `D<3, 1>' requested
prime.cpp: In member function `void Prime_print<i>::f() [with int i = 5]':
[...]
prime.cpp:18: error: conversion from `Prime_print<7>::<anonymous enum>' to 
non-scalar type `D<7, 1>' requested
prime.cpp: In member function `void Prime_print<i>::f() [with int i = 11]':
[...]
prime.cpp:18: error: conversion from `Prime_print<11>::<anonymous enum>' to 
non-scalar type `D<11, 1>' requested
[...]

Mixin

Korlátozottan lehetőség van mixin-ek létrehozására is. (A mixin egy adott funkcionalitást megvalósító osztályrészlet, mely később más osztályokhoz kapcsolható úgy mintha az szerves része lenne az osztálynak.)

Az alábbi klasszikus példa egy gráf osztályhoz kapcsolható és az élek illetve csúcsok eléréseit számlálja.

template <class Graph>
class Counting: public Graph {
	int nodes_visited, edges_visited;
public:
	Counting() : nodes_visited(0), edges_visited(0), Graph() { }
	node succ_node (node v) {
		nodes_visited++;
		return Graph::succ_node(v);
	}
	edge succ_edge (edge e) {
		edges_visited++;
		return Graph::succ_edge(e);
	}
};
Counting< Ugraph >  counted_ugraph;

Trait

Ha egy sablonnak a T típusparaméterétől függ egy másik típus paramétere, akkor az ilyen függő típust traitnek nevezzük. A trait-ek tehát olyan osztályok, melyek egy-egy típusról hordoznak (egy kis) információt. Klasszikus példa a szabványos C++ könyvtárban található basic_string implementáció. A fejlécen látható, hogy a Tr típusa függ a Ch típustól.

template<class Ch, class Tr = char_traits<Ch>, class A = allocator<Ch> >
class std::basic_string
{
	[...]

A Tr típus tehát Ch-tól függően char_traits<char> vagy char_traits<wchar_t>. A basic_string tehát attól függően, hogy milyen típussal lett paraméterezve használja az egyik illetve a másik osztályt az implementációhoz. Megjegyzés: a traitek bizonyos szempontból hasonlítanak az interfészekhez (Java). A két specializáció egyfajta implementációja a char_traits "interfésznek", a basic_string pedig az interfészt kliensként használja.

template<class Ch> struct char_traits { };

template<> struct char_traits<char>
{
	// ide kerülnek a szükséges műveletek fejlécei, például
	static bool eq(const char_type& __c1, const char_type& __c2);
	[...]
}

template<> struct char_traits<char>
{	
	// ide kerülnek az egyes műveletek implementációi a char típushoz
	static size_t length(const char_type* __s)	{ return strlen(__s); }
}

template<> struct char_traits<wchar_t>
{	
	// ide kerülnek az egyes műveletek implementációi a wchar_t típushoz
	static size_t length(const char_type* __s)	{ return wcslen(__s); }
}

 

Sablon metaprogramozás a gyakorlatban - könyvtárak

A sablon metaprogramozás értékelése

Lehetőségek és előnyök:

És a hátrányok:

A fentiekhez még két fontos dolog kapcsolódik. Az egyik az, hogy a sablon metaprogramozás nem nyújt lehetőséget a metaprogramozásban rejlő erő teljes kihasználására. A másik pedig, hogy a fentiek csupán a sablon metaprogramozásra vetnek rossz fényt; általában véve a metaprogramozás hátránya csak az lehet, hogy feleslegesen vezetünk be általánosításokat és ezáltal feleslegesen oldunk meg egy általánosabb problémákat.

Hivatkozások

...