Expanze výrazů za pomoci přetěžování operátorů

Cíl

Cílem je používat vektory obdobně jako v následujícím příkladu, tedy s využitím přetěžování operátorů, ale bez vlivu na rychlost.

  1. #include "Fast_Vector.h"
  2.  
  3. int main()
  4. {
  5. int size_ = 5000000 ;
  6. /**
  7.   definice unikatnich vektoru
  8.   */
  9. Fast_Vector<1> a(size_,1.);
  10. Fast_Vector<2> b(size_,2);
  11. Fast_Vector<3> c(size_);
  12.  
  13. c = 5. * a + 7. * b ;
  14. std::cout << c[0] >> std::endl;
  15. }

Tedy vektorový výraz, ve kterém by při standardním přetěžování operátorů došlo k vytváření pomocných vektorů v paměti a tím k její alokaci a dealokaci, nahradíme smyčkou. Tato smyčka pro každou složku vektoru vypočte výraz, aniž by se alokavala jakákoliv další pamět v průběhu výpočtu. Zároveň je náš příklad jednoduchý a nevynucuje od uživatele znalost šablonového metaprogramování. Velikost vektorů při tom může být známa až za běhu programu. Zatím v příkladu vidíme jen násobení vektoru číslem typu double a sčítání vektorů. Postup dále popsaný ale není těžké rozšířit i na jiné typy a operace.

Šablony (Templates)

Šablony byly původně vyvinuty jako snaha využít ten samý kód pro víc typů. Protože už při překladu musí být zřejmé jaké typy se s danou šablonou váží, nahradí se šablona vždy kódem pro příslušný typ. Tedy není třeba psát funkci minimum a maximum pro různé typy, ale stačí využít stejného kódu. Stejně tak není třeba psát zvláštní kontejnery pro různé typy. Šablony jsou také výhodné v tom, že zůstává zachována typová kontrola.
S rozvojem standardu a rozšířením implementace šablon se přišlo na to, že je lze využít i k poměrně komplikovaným výpočtům a transformacím v době překladu. Šablony lze různě skládat a definovat jejich chování pro různé parametry, popř. definovat běžné chování a případy pro různé speciální typy vstupních parametrů (specializace).

Výrazy

Obalová třída Expr zapouzdřuje všechny možné výrazy do stejného rozhraní. Protože třídy operátorů se dědí z této obalové třídy, můžeme každý objekt operátoru předávat jako obalový objekt. Zároveň v této třídě zavádíme operátor přetypování, který nám naopak umožňuje přistupovat k obalenému objektu.

Třídy operátorů si uchovávají podvýrazy jako reference, dokud není rozvinutí výrazu kompletní. Operátor přístupu [] slouží k výpočtu i-té složky výrazu rekurzivně volající tento operátor u podvýrazů. Protože používáme inline funkce a všechny proměnné a výrazy lze určit v době kompilace může kompilér analyzovat všechny typy. Přetypované operátory berou jeden až dva parametry a provádí se pomocí tříd operátorů.

Pozn.: Překlad zdrojového kódu s těmito operátory může trvat poměrně delší čas, protože klade značné nároky na kompilátor. Následující postup lze také volně rozšiřovat na další objekty jako matice, polynomy, atd.

  1. template <class A>
  2. struct Expr
  3. {
  4. operator const A&() const
  5. {
  6. return *static_cast<const A*>(this);
  7. }
  8. };
  9. //----------------------------------------------------------------------------
  10. template <class A, class B>
  11. class Add : public Expr<Add<A,B> >
  12. {
  13. const A& a_;
  14. const B& b_;
  15. public:
  16. Add(const A& a, const B& b) : a_(a), b_(b) {}
  17. double operator[](int i) const
  18. {
  19. return a_<span style="font-style:italic"> + b_[i];
  20. }
  21. };
  22. //----------------------------------------------------------------------------
  23. template <class A, class B>
  24. inline Add<A,B>
  25. operator+ (const Expr<A>& a, const Expr<B>& b)
  26. {
  27. return Add<A,B>(a,b);
  28. }

Obdobně postupujeme pro ostatní operátory
[i]Pozn.: Mezera mezi znaky > > při použití vnořených šablon je nutná. Tokenizér C++ postupuje hladnovým způsobem a znaky >> by považoval za operátor posunutí a ne za dvě po sobě jdoucí ukončovací závorky.

Vektor

Vector je třída, která je odvozena od obalové třídy Expr, takže každý vektor se chová jako součást výrazu. Konstruktor vektoru alokuje paměť podle velikosti vektoru. Přetížený operátor [] nám umožňuje pracovat se složkami vektoru. Složky vektoru jsou typu double, ale šablonu lze rozšířit i na jiné typy.

Přetížený operátor = vektoru slouží k vyhodnocení výrazu na pravé straně pro všechny složky vektoru. Tento operátor ve smyčce počítá jednotlivé složky výsledku a využívá k tomu operátory [] tříd odvozeních od Expr.
Pozn.: Tato třída obsahuje pole proměnné délky, kterou není nutné zadat v době překladu.

  1. #include "EasyET.h"
  2.  
  3. class Vector : public Expr<Vector>
  4. {
  5. private:
  6. double * data;
  7. int n;
  8. public:
  9. Vector(int n_, double w = 0) : n(n_)
  10. {
  11. data = new double[n];
  12. for (int i = 0; i < n; ++i)
  13. {
  14. data[i] = w;
  15. }
  16. }
  17. ~Vector()
  18. {
  19. delete [] data;
  20. }
  21. double operator[] (int i) const
  22. {
  23. return data[i];
  24. }
  25.  
  26. template <class A>
  27. void operator = (const Expr<A>& a_)
  28. {
  29. const A& a(a_);
  30. for (int i = 0; i < n; ++i)
  31. {
  32. data[i] = a[i];
  33. }
  34. }
  35. };

Rychlé vektory

Protože ukazatel na data objektu Vector není statický, tak se vícenásobný výskyt stejného vektoru ve složitějším výrazu na pravé straně přiřazení považuje v době kompilace za různé proměnné a nedojde k potřebným optimalizacím. Proto pro složité pravé strany zavádíme speciální třídu vektoru, který má data deklarována přes statický ukazatel. Takový rychlý vektor ovšem musí být identifikován unikátním číslem v celé aplikaci.
Příklad:

  1. 3 * double Fast_Vector<1>::data[i] + 2 * double Fast_Vector<1>::data[i]
  2. //lze převést na
  3. 5 * double Fast_Vector<1>::data[i]
  4. //ale to se nestane pro výraz
  5. 3 * (Vector& v1).data[i] + 2 * (Vector& v2).data[i]
  6. //protože v1, v2 nejsou staticke proměnné (i když nakonec třeba platí v1==v2)

Pozn: Přestože používáme statický ukazatel, tak velikost pole zůstává dynamická stejně jako v předchozí implementaci.

  1. template <int unique_id>
  2. class Fast_Vector : public Expr<Fast_Vector<unique_id> > {
  3. private:
  4. static double * data;
  5. int n;
  6. public:
  7. Fast_Vector(int n_, double w = 0) : n(n_){
  8. data = new double[n];
  9. for (int i= 0; i < n; ++i) {
  10. data[i] = w;
  11. }
  12. ...
  13. };
  14.  
  15. template <int unique_id>
  16. double* Fast_Vector<unique_id>::data;

Příklad na expanzi výrazu

Výraz z úvodního příkladu

  1. c = 5. * a + 7. * b

se pomocí přetížení operátorů převede na

  1. c = 5. * Expr< FastVector(a) > + 7. * Expr<FastVector(b)>
  2. c = Mul< 5., Expr< FastVector(a) > > + Mul< 7., Expr<FastVector(b)> >
  3. c = Add< Mul< 5., Expr< FastVector(a) > >, Mul< 7., Expr<FastVector(b)> > >
  4. c = Expr< Add< Mul< 5., Expr< FastVector(a) > >, Mul< 7., Expr<FastVector(b)> > > >

Tedy do tvaru typu:

  1. c = Expr<...>

Toto přiřazení pro FastVector je přetíženo a způsobí volání vyhodnocení šablony pro každou složku vektoru.

  1. template <class A>
  2. void operator = (const Expr<A>& a_){
  3. const A& a(a_);
  4. for (int i = 0; i < n; ++i){
  5. data[i] = a[i];
  6. }
  7. }

Výraz data[i] = a[i] se převede rekurzivním voláním vyhodnocení podvýrazu přes operátor [] na:

  1. template <class A>
  2. data[i] = (5. * a[i]) + (7. * b[i])
  3. }

Přičemž pro vektor operátor [] vyzvedne i-tou složku vektoru.