Test MJ: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Mirek (dyskusja | edycje)
Mirek (dyskusja | edycje)
 
(Nie pokazano 187 wersji utworzonych przez 3 użytkowników)
Linia 15: Linia 15:
==7.2 Potęgi==
==7.2 Potęgi==


Zaczniemy od bardzo prostego przykładu <span style="color:red">[[(zob. {szablony} rozdz. 17)]]</span>.
Zaczniemy od bardzo prostego przykładu (zob. D. Vandervoorde, N. Josuttis:''"C++ Szablony, Vademecum profesjonalisty"'', rozdz. 17).
Napiszemy szablon który ma za zadanie obliczać potęgi liczby 3.
Napiszemy szablon który ma za zadanie obliczać potęgi liczby 3.
Ponieważ w programowaniu za pomocą szablonów musimy posługiwać się
Ponieważ w programowaniu za pomocą szablonów musimy posługiwać się
Linia 26: Linia 26:
}}
}}


Za pomocą szablonów implementujemy to tak ([http://osilek.mimuw.edu.pl/images/9/91/Pow.cpp Źródło:Pow.cpp]):
Za pomocą szablonów implementujemy to tak ([http://osilek.mimuw.edu.pl/images/9/91/Pow.cpp Źródło: Pow.cpp]):
 
      
      
  template<int  N> struct Pow3 { <br>
  template<int  N> struct Pow3 {
  &nbsp; enum {val<nowiki>=</nowiki>3*Pow3<N-1>::val}; <br>
  &nbsp; enum {val<nowiki>=</nowiki>3*Pow3<N-1>::val};
  };<br>
  };
  template<> struct Pow3<0> {<br>
  template<> struct Pow3<0> {
  &nbsp; enum {val<nowiki>=</nowiki>1}; <br>
  &nbsp; enum {val<nowiki>=</nowiki>1};  
  };
  };


Linia 44: Linia 43:
jest taki sam jak podstawienia:
jest taki sam jak podstawienia:


i<nowiki>=</nowiki>81;
&nbsp;i<nowiki>=</nowiki>81;


Można też zastosować szablon funkcji
Można też zastosować szablon funkcji:


template<int N> int pow3() {
template<int N> int pow3() {
return 3*pow3<N-1>();
&nbsp; return 3*pow3<N-1>();
};
};
template<> int pow3<0>() {return 1;}
template<> int pow3<0>() {return 1;}
 
cout<<pow3<4>()<<endl;
cout<<pow3<4>()<<endl;
([http://osilek.mimuw.edu.pl/images/5/59/Powf.cpp Źródło: Powf.cpp])
/* {mod07/code/powf.cpp}{powf.cpp}*/


Nietrudno jest uogólnić powyższy kod tak aby wyliczał potęgi dowolnej
Nietrudno jest uogólnić powyższy kod tak aby wyliczał potęgi dowolnej
liczby  
liczby:


template<int K,int N> struct Pow { enum
template<int K,int N> struct Pow { enum  
{val<nowiki>=</nowiki>K*Pow<K,N-1>::val}; };
&nbsp;{val<nowiki>=</nowiki>K*Pow<K,N-1>::val}; };
 
template<int K> struct Pow<K,0> {
template<int K> struct Pow<K,0> {
&nbsp;enum {val<nowiki>=</nowiki>1};
enum {val<nowiki>=</nowiki>1};
};
};
([http://osilek.mimuw.edu.pl/images/9/91/Pow.cpp Źródło: Pow.cpp])
/* {mod07/code/pow.cpp}{pow.cpp}*/
 
Tutaj już nie można wykorzystać szablonu funkcji bo nie zezwala on na
specjalizację częściową.


Tutaj już nie można wykorzystać szablonu funkcji, bo nie zezwala on na
specjalizację częściową.<br>
Ograniczeniem dla takich obliczeń jest implementacja kompilatora,
Ograniczeniem dla takich obliczeń jest implementacja kompilatora,
przede wszystkim założony limit głebokości rekurencyjnego
przede wszystkim założony limit głebokości rekurencyjnego
konkretyzowania szablonów. Dla kompilatora g++ jest on ustawiany za
konkretyzowania szablonów. Dla kompilatora g++ jest on ustawiany za
pomocą opcji i defaultowo wynosi 500, dlatego już konkretyzacja
pomocą opcji i defaultowo wynosi 500, dlatego już konkretyzacja <tt>Pow<1,500>::val</tt> się nie powiedzie.   
!Pow<1,500>::val! się nie powiedzie.   
Konkretyzacja szablonów wymaga też pamięci i może się zdarzyć, że
Konkretyzacja szablonów wymaga też pamięci i może się zdarzyć że
kompilator wyczerpie limit pamięci lub czasu. <br> Kolejne ograniczenie to konieczność rachunków na liczbach całkowitych.
kompilator wyczerpie limit pamięci lub czasu.
Wiąże się to z faktem, że tylko stałe całkowitoliczbowe mogą być
 
Kolejne ograniczenie to konieczność rachunków na liczbach całkowitych,
wiąże się to z faktem że tylko stałe całkowito liczbowe mogą być
parametrami szablonów.
parametrami szablonów.


==Ciąg Fibonacciego==
==7.3 Ciąg Fibonacciego==


Po opanowaniu powyższych przykładów obliczanie wyrazów ciągu
Po opanowaniu powyższych przykładów obliczanie wyrazów ciągu
Fibonacciego jest prostym zadaniem. Przytoczymy je jednak tutaj aby
Fibonacciego jest prostym zadaniem. Przytoczymy je jednak tutaj, aby
zaprezentować pewną bardzo sympatyczną cechę metaprogramowania za
zaprezentować pewną bardzo sympatyczną cechę metaprogramowania za
pomocą szablonów. Ciąg Fibonnacciego jest definiowany rekurencyjnie:
pomocą szablonów. Ciąg Fibonacciego jest definiowany rekurencyjnie:


<center><math>f_n=f_{n-1}+f_{n-2},\quad f_1=f_2=1
{{przyklad|7.2||
</math></center>
<center><math>f_n=f_{n-1}+f_{n-2},\quad f_1=f_2=1</math>
</center>
}}
więc jego implementacja jest natychmiastowa:


więc jego implementacja jest natychmiastowa:
template<int N> struct Fibonacci {
&nbsp;enum {val <nowiki>=</nowiki> Fibonacci<N-1>::val+Fibonacci<N-2>::val};
};
template<> struct Fibonacci<1> {
&nbsp;enum {val <nowiki>=</nowiki> 1};
};
template<> struct Fibonacci<2> {
&nbsp;enum {val <nowiki>=</nowiki> 1};
};


template<int N> struct Fibonacci {
([http://osilek.mimuw.edu.pl/images/e/ea/Fibonacci_template.cpp Źródło: fibonacci_template.cpp])
enum {val <nowiki>=</nowiki> Fibonacci<N-1>::val+Fibonacci<N-2>::val};
};
template<> struct Fibonacci<1> {
enum {val <nowiki>=</nowiki> 1};
};
template<> struct Fibonacci<2> {
enum {val <nowiki>=</nowiki> 1};
};
/*{mod07/code/fibonacci_template.cpp}{fibonaccitemplate.cpp}*/


Przykład ten nie wart byłby może i wspomnienia gdyby nie fakt że
Przykład ten nie wart byłby może i wspomnienia gdyby nie fakt, że
rekurencyjna implementacja ciągu Fibonacciego jest bardzo nieefektywna.  
rekurencyjna implementacja ciągu Fibonacciego jest bardzo nieefektywna.  
Jeśli zaimplementujemy ją w zwykłym kodzie
Jeśli zaimplementujemy ją w zwykłym kodzie


int fibonacci(int n) {
int fibonacci(int n) {
if(n<nowiki>=</nowiki><nowiki>=</nowiki>1) return 1;
&nbsp;if(n<nowiki>=</nowiki><nowiki>=</nowiki>1) return 1;
if(n<nowiki>=</nowiki><nowiki>=</nowiki>2) return 1;
&nbsp;if(n<nowiki>=</nowiki><nowiki>=</nowiki>2) return 1;
return fibonacci(n-1)+fibonacci(n-2);
&nbsp;return fibonacci(n-1)+fibonacci(n-2);
}
}
/*{mod07/code/fibonacci.cpp}{fibonacci.cpp}*/
 
([http://osilek.mimuw.edu.pl/images/2/28/Fibonacci.cpp Żródło: fibonacci.cpp])


to obliczanie !fibonacci(45)! zajmie np.na moim komputerze ok. 8
to obliczanie <tt>fibonacci(45)</tt> zajmie np.na moim komputerze ok. 8
sekund.  Tymczasem szablon kompiluje sie poniżej jednej sekundy!  Skąd
sekund.  Tymczasem szablon kompiluje sie poniżej jednej sekundy!  Skąd
taka różnica? Czyżby kompilator był bardziej wydajny niż generowany
taka różnica? Czyżby kompilator był bardziej wydajny niż generowany
przez niego kod? W przypadku zwykłego kodu długi czas wykonania bierze
przez niego kod? W przypadku zwykłego kodu długi czas wykonania bierze
się z ogromnej liczby wywołań funkcji !fibonacci!. Liczba ta rośnie
się z ogromnej liczby wywołań funkcji <tt>fibonacci</tt>. Liczba ta rośnie
wykładniczo z !n! i większość czasu jest marnowana na wielokrotne
wykładniczo z <tt>n</tt> i większość czasu jest marnowana na wielokrotne
wywoływanie funkcji z tymi samymi argumentami.
wywoływanie funkcji z tymi samymi argumentami.


W przypadku użycia metaprogramu szablony konkretyzowane są tylko raz.
&nbsp;W przypadku użycia metaprogramu szablony konkretyzowane są tylko raz.
Więc jeśli już raz policzymy np. !Fibonacci<25>::val! to kolejne
Więc jeśli już raz policzymy np. <tt>Fibonacci<25>::val</tt> to kolejne
żądanie nie spowoduje już rozwinięcia rekurencyjnego a tylko
żądanie nie spowoduje już rozwinięcia rekurencyjnego, a tylko
podstawienie istniejącej wartości. Jak widzieliśmy zysk jest ogromny.
podstawienie istniejącej wartości. Jak widzieliśmy zysk jest ogromny.
Takie pamiętanie wyników raz wywołanych funkcji nazywane jest też
Takie pamiętanie wyników raz wywołanych funkcji nazywane jest też
programowaniem dynamicznym, jedynym znanym mi językiem który
programowaniem dynamicznym. Jedynym znanym mi językiem, który
bezpośrednio wspiera taki mechanizm jes !Mathematica!.
bezpośrednio wspiera taki mechanizm jest <tt>Mathematica</tt>.


==Pierwiastek kwadratowy==
==7.4 Pierwiastek kwadratowy==


Rozważymy teraz trudniejszy przykład szablonu obliczającego
Rozważymy teraz trudniejszy przykład szablonu obliczającego
pierwiastek kwadratowy ({szablony} rozdz. 17), choć tak naprawdę
pierwiastek kwadratowy ([1] rozdz. 17), choć tak naprawdę
ten kod jest bardziej uniwersalny i jak zobaczymy łatwo za jego pomocą
ten kod jest bardziej uniwersalny i, jak zobaczymy, łatwo za jego pomocą
zaimplementować inne funkcje. Ponieważ jesteśmu ograniczenie do
zaimplementować inne funkcje. Ponieważ jesteśmy ograniczeni do
arytemetyki licz całkowitych tak naprawdę nie liczymy pierwiasta z
arytmetyki liczb całkowitych, tak naprawdę nie liczymy pierwiastka z
!n! ale jego przybliżenie: największa liczbę całkowitą !k! taką że
<tt>n</tt> ale jego przybliżenie: największą liczbę całkowitą <tt>k</tt>, taką że <tt>k*k<<nowiki>=</nowiki>n</tt>.  
!k*k<<nowiki>=</nowiki>n!.  
W tym celu zastosujemy algorytm przeszukiwania binarnego:
W tym celu zastosujemy algorytm przeszukiwania binarnego:


int sqrt(int n,int low, int high) {
int sqrt(int n,int low, int high) {
 
&nbsp;if(low<nowiki>=</nowiki><nowiki>=</nowiki>high) return low;
if(low<nowiki>=</nowiki><nowiki>=</nowiki>high) return low;
&nbsp;int mid<nowiki>=</nowiki>(low+high+1)/2;
int mid<nowiki>=</nowiki>(low+high+1)/2;
&nbsp;if(mid*mid > n )  
 
&nbsp;&nbsp;&nbsp;return sqrt(n,low,mid-1);
if(mid*mid > n )  
&nbsp;else
return sqrt(n,low,mid-1);
&nbsp;&nbsp;&nbsp;return sqrt(n,mid,high);
else
}
return sqrt(n,mid,high);
}


co już łatwo przetłumaczyć na szablon:
co już łatwo przetłumaczyć na szablon:


template<int N,int L<nowiki>=</nowiki>1,int H<nowiki>=</nowiki>N> struct Sqrt{
template<int N,int L<nowiki>=</nowiki>1,int H<nowiki>=</nowiki>N> struct Sqrt{
enum  {mid<nowiki>=</nowiki>(L+H+1)/2};
&nbsp;enum  {mid<nowiki>=</nowiki>(L+H+1)/2};
enum  {res<nowiki>=</nowiki> (mid*mid> N)? (int)Sqrt<N,L,mid-1>::res :
&nbsp;enum  {res<nowiki>=</nowiki> (mid*mid> N)? (int)Sqrt<N,L,mid-1>::res :
(int)Sqrt<N,mid,H>::res};
&nbsp;&nbsp;(int)Sqrt<N,mid,H>::res};
};
};
template<int N,int L> struct Sqrt<N,L,L> {
&nbsp;enum {res<nowiki>=</nowiki>L};
};
([http://osilek.mimuw.edu.pl/images/5/54/Sqrt.cpp Źródło: sqrt.cpp])


template<int N,int L> struct Sqrt<N,L,L> {
Łatwo sprawdzić, że kod ten działa poprawnie. Niestety posiada on
enum {res<nowiki>=</nowiki>L};
};
/* {mod07/code/sqrt.cpp}{sqrt.cpp}*/
 
Łatwo sprawdzić że kod ten działa poprawnie. Niestety posiada on
istotną wadę.  W trakcie konkretyzacji szablonu konkretyzowane są oba
istotną wadę.  W trakcie konkretyzacji szablonu konkretyzowane są oba
szablony występujące w wyrażeniu warunkowym, nawet to które nie będzie
szablony występujące w wyrażeniu warunkowym, nawet ten, który nie będzie
potem używane.  Tak więc wykonywana jest duża liczna konkretyzacji z
potem używany.  Tak więc wykonywana jest duża liczba konkretyzacji, z
których tylko ułamek jest potrzebny (zob. rysunek). Jakie to obciążenie dla
których tylko ułamek jest potrzebny (zob. rysunek). Jakie to obciążenie dla
kompilatora to łatwo sprawdzić kompilując kod w którym wywołujemy
kompilatora to łatwo sprawdzić kompilując kod, w którym wywołujemy
!Sqrt<10000>!.  Ja nie doczekałem się na koniec kompilacji.
<tt>Sqrt<10000></tt>.  Ja nie doczekałem się na koniec kompilacji.


Na szczeście istnieje rozwiązanie, należy użyć szablonu !If_the_else!
Na szczęście istnieje rozwiązanie - należy użyć szablonu <tt>If_then_else</tt>
(zob. rozdzial [[##lbl:ifte|Uzupelnic lbl:ifte|]]):
(zob. [[Zaawansowane CPP/Wykład 5: Funkcje typów i inne sztuczki|rozdział 5.2.1]]):


template<int N,int L<nowiki>=</nowiki>1,int H<nowiki>=</nowiki>N> struct Sqrt{
template<int N,int L<nowiki>=</nowiki>1,int H<nowiki>=</nowiki>N> struct Sqrt{
enum  {mid<nowiki>=</nowiki>(L+H+1)/2};
&nbsp;enum  {mid<nowiki>=</nowiki>(L+H+1)/2};
 
&nbsp;typedef typename If_then_else<
typedef typename If_then_else<
&nbsp;&nbsp;(mid*mid> N),
(mid*mid> N),
&nbsp;&nbsp;Sqrt<N,L,mid-1>,
Sqrt<N,L,mid-1>,
&nbsp;&nbsp;Sqrt<N,mid,H> >::Result tmp;
Sqrt<N,mid,H> >::Result tmp;
&nbsp;enum  {res<nowiki>=</nowiki> tmp::res};
 
};
enum  {res<nowiki>=</nowiki> tmp::res};
template<int N,int L> struct Sqrt<N,L,L> {
 
&nbsp;enum {res<nowiki>=</nowiki>L};
};
};
 
([http://osilek.mimuw.edu.pl/images/e/ef/Sqrt_ifte.cpp Źródło: sqrt_ifte.cpp])
template<int N,int L> struct Sqrt<N,L,L> {
enum {res<nowiki>=</nowiki>L};
};
/* {mod07/code/sqrt_ifte.cpp}{sqrtifte.cpp}*/


Ten kod powoduje już tylko konkretyzację rzeczywiście wymaganych
Ten kod powoduje już tylko konkretyzację rzeczywiście wymaganych
szablonów a tych jest dużo mniej: rzędu <math>O(\log N)</math>.  Tym razem
szablonów, a tych jest dużo mniej: rzędu <math>O(\log N)</math>.  Tym razem
kompilacja wyrażenia !Sqrt<10000>! powiedzie się bez trudu.
kompilacja wyrażenia <tt>Sqrt<10000></tt> powiedzie się bez trudu.


==Pow(x)==
==7.5 Pow(x)==


Jak dotąd używaliśmy metaprogramowania do wyliczania wartości stałych.
Jak dotąd używaliśmy metaprogramowania do wyliczania wartości stałych.
Teraz postaramy się wygenerować funkcje. Zacznijmy od pytania po co?
Teraz postaramy się wygenerować funkcje. Zacznijmy od pytania: po co?
Pomijając syndrom Mount Everestu (wchodzę na niego bo jest), to
Pomijając syndrom Mount Everestu (wchodzę na niego bo jest), to
głownym powodem jest nadzieja uzyskania bardziej wydajnego kodu.
głównym powodem jest nadzieja uzyskania bardziej wydajnego kodu.
Weźmy dalej za przykład liczenie potęgi, tym razem dowolnej liczny
Weźmy dalej za przykład liczenie potęgi, tym razem dowolnej liczby
zmiennoprzecinkowej:
zmiennoprzecinkowej:


double pow_int(double x,int n) {
double pow_int(double x,int n) {
double res<nowiki>=</nowiki>1.0;
double res<nowiki>=</nowiki>1.0;
for(int i<nowiki>=</nowiki>0;i<n;++i)
for(int i<nowiki>=</nowiki>0;i<n;++i)
res*<nowiki>=</nowiki>x;
&nbsp;res*<nowiki>=</nowiki>x;
return res;
return res;
};
};
/*{mod07/code/powx.cpp}{powx.cpp}*/
([http://osilek.mimuw.edu.pl/images/d/d7/Powx.cpp Źródło: powx.cpp])


Patrząc na ten kod widzimy że w pętli wykonywana jest bardzo prosta
Patrząc na ten kod widzimy, że w pętli wykonywana jest bardzo prosta
instrukcja. Możemy więc się obawiać że instrukcje związane z obsługą
instrukcja. Możemy więc się obawiać, że instrukcje związane z obsługą
pętli mogą stanowić spory narzut.  
pętli mogą stanowić spory narzut.  
Co więcej ich obecność utrudnia kompilatorowi optymalizację kodu oraz   
Co więcej, ich obecność utrudnia kompilatorowi optymalizację kodu oraz   
może uniemożliwić rozwinięcie funkcji w miejscy wywołania.  
może uniemożliwić rozwinięcie funkcji w miejscu wywołania.  
Najlepiej by było zaimplementować funkcję w ten sposób
Najlepiej by było zaimplementować funkcję w ten sposób:


pow(x,n)<nowiki>=</nowiki> x*...*x; /*n razy*/
pow(x,n)<nowiki>=</nowiki> x*...*x; /*n razy*/


np.  
np.:


double pow2(double x) {return x*x;}
double pow2(double x) {return x*x;}
double pow3(double x) {return x*x*x;}
double pow3(double x) {return x*x*x;}
double pow4(double x) {return x*x*x*x;}
double pow4(double x) {return x*x*x*x;}
...
...


Wymaga to jednak kodowania ręcznego dla każdej potęgi której
Wymaga to jednak kodowania ręcznego dla każdej potęgi, której
potrzebujemy.  Ten sam efekt możemy osiągnąc za pomocą następujacego
potrzebujemy.  Ten sam efekt możemy osiągnąc za pomocą następującego
szablonu funkcji:
szablonu funkcji:


template<int N> inline double pow(x) {return x*pow<N-1>(x);}
template<int N> inline double pow(x) {return x*pow<N-1>(x);}
template<>  inline double pow<0>(x) {return 1.0;}
template<>  inline double pow<0>(x) {return 1.0;}
/*{mod07/code/powx.cpp}{powx.cpp}*/
([http://osilek.mimuw.edu.pl/images/d/d7/Powx.cpp Źródło: powx.cpp])


pod warunkiem że kompilator rozwinie wszystkie wywołania.
pod warunkiem, że kompilator rozwinie wszystkie wywołania.


Poniżej zamieszczam wyniki pomiarów wykonania 100 milionów wywołań
Poniżej zamieszczam wyniki pomiarów wykonania 100 milionów wywołań
każdej funkcji ({mod07/code/powx.cpp}{powx.cpp}). Czas jest
każdej funkcji ([http://osilek.mimuw.edu.pl/images/d/d7/Powx.cpp powx.cpp]). Czas jest
podany w sekundach. Zamieściłem wyniki dla różnych ustawień
podany w sekundach. Zamieściłem wyniki dla różnych ustawień
optymalizacji.
optymalizacji.
 
<div align=center>
{| border=1
{| border=1
|+ <span style="font-variant:small-caps">Uzupelnij tytul</span>
|-
|
|| powint(x,5)  ||  pow<5>(x)
|-
|
-O0  ||  7.22  ||  14.78
|-
|-
|  
|  
-O1 ||   0.37 ||  0.04
| pow_int(x,5)
|-
|  pow<5>(x)
|  
|---
-O2 ||   0.42 ||  0.05
|align="center"|-O0
|-
|align="center"|  7.22
|  
|align="center"|  14.78
-O3 ||   0.42 ||  0.05
|---
|-
|align="center"|-O1
|
|align="center"| 0.37
 
|align="center"|  0.04
|---
|align="center"|-O2
|align="center"| 0.42
|align="center"|  0.05
|---
|align="center"|-O3
|align="center"| 0.42
|align="center"|  0.05
|}
|}
 
</div>
Widać dramatyczną różnicę po włączeniu optymalizacji. Wiąże się to
Widać dramatyczną różnicę po włączeniu optymalizacji. Wiąże się to
prawdopodobnie z umożliwieniem rozwijana funkcji !inline!.  Potem
prawdopodobnie z umożliwieniem rozwijania funkcji <tt>inline</tt>.  Potem
wyniki już się nie zmianiają, ale widać że szablon !pow! wygenerował
wyniki już się nie zmieniają ale widać, że szablon <tt>pow</tt> wygenerował
funkcję będącą 10 razy szybszą od pozostałych. Pokazuje to dobitnie że
funkcję 10 razy szybszą od pozostałych. Pokazuje to dobitnie, że
optymalizacja ręczna ciągle ma sens.  
optymalizacja ręczna ciągle ma sens.


==Sortowanie bąbelkowe==
==7.6 Sortowanie bąbelkowe==


Zakończymy ten wykład bardziej skomplikowanym przykładem pokazujacym
Zakończymy ten wykład bardziej skomplikowanym przykładem pokazującym,
że metaprogramowanie można stosować nie tylko do obliczeń
że metaprogramowanie można stosować nie tylko do obliczeń
numerycznych. Popatrzmy na kod implementujący sortowanie bąbelkowe:
numerycznych. Popatrzmy na kod implementujący sortowanie bąbelkowe:


inline void swap (int &a,int &b) {int tmp<nowiki>=</nowiki>a;a<nowiki>=</nowiki>b;b<nowiki>=</nowiki>tmp;};
inline void swap (int &a,int &b) {int       tmp<nowiki>=</nowiki>a;a<nowiki>=</nowiki>b;b<nowiki>=</nowiki>tmp;};
 
void bubble_sort_function (int* data, int N) {
void bubble_sort_function (int* data, int N) {
&nbsp;for(int i <nowiki>=</nowiki> N-1; i>0; --i)
for(int i <nowiki>=</nowiki> N-1; i>0; --i)
&nbsp;&nbsp;for(int j<nowiki>=</nowiki>0;j<nowiki><</nowiki>i;++j)
for(int j<nowiki>=</nowiki>0;j<i;++j)
&nbsp;&nbsp;&nbsp;if(data[j]>data[j+1])  
if(data[j]>data[j+1])  
&nbsp;&nbsp;&nbsp;&nbsp;swap(data[j],data[j+1]);
swap(data[j],data[j+1]);
}
 
([http://osilek.mimuw.edu.pl/images/0/0f/Bubble_template.cpp Źródło: bubble_template.cpp])
}
/* {mod07/code/bubble_template.cpp}{bubbletemplate.cpp} */


Znów widzimy tu dwie pętle i wszystkie uwagi dotyczące funkcji
Znów widzimy tu dwie pętle i wszystkie uwagi dotyczące funkcji
!pow_int! tu też się stosują. Postaramy się więc zdefiniować szablon  
<tt>pow_int</tt> tu też się stosują. Postaramy się więc zdefiniować szablon,
który dokona rozwinięcia tych obu pętli. Np. dla !N<nowiki>=</nowiki>3! chcielibyśmy otrzymać następujący kod:
który dokona rozwinięcia tych obu pętli. Np. dla <tt>N<nowiki>=</nowiki>3</tt> chcielibyśmy otrzymać następujący kod:


//i<nowiki>=</nowiki>2 j<nowiki>=</nowiki>0
//i<nowiki>=</nowiki>2 j<nowiki>=</nowiki>0
if(data[0]>data[1]) swap(data[0],data[1]);
if(data[0]>data[1]) swap(data[0],data[1]);
//i<nowiki>=</nowiki>2 j<nowiki>=</nowiki>1
if(data[1]>data[2]) swap(data[1],data[2]);
//i<nowiki>=</nowiki>1 j<nowiki>=</nowiki>0
if(data[0]>data[1]) swap(data[0],data[1]);


//i<nowiki>=</nowiki>2 j<nowiki>=</nowiki>1
Jeśli Państwo śledzili wykład (przynajmniej ten), to już Państwo wiedzą, że
if(data[1]>data[2]) swap(data[1],data[2]);
pierwszym krokiem musi być przepisanie kodu sortowania na postać rekurencyjną:


//i<nowiki>=</nowiki>1 j<nowiki>=</nowiki>0  
void bubble_sort_function (int* data, int N) {
if(data[0]>data[1]) swap(data[0],data[1]);
&nbsp;for(int j<nowiki>=</nowiki>0;j<N-1;++j)
  &nbsp;&nbsp;if(data[j]>data[j+1])  
&nbsp;&nbsp;&nbsp;swap(data[j],data[j+1]);
&nbsp;if(N>2)
&nbsp;&nbsp;bubble_sort_function(data,N-1);
}


Jeśli państwo śledzili wykład (przynajmniej ten) to już państwo wiedzą że
To jeszcze nie to co trzeba, bo musimy zapisać pętle w postaci
pierwszym krokiem musi być przepisanie kodu  sortowania na postać rekurencyjną:
rekurencyjnej. Jeśli oznaczymy sobie:


void bubble_sort_function (int* data, int N) {
void loop(int * data,int N) {
&nbsp;for(int j<nowiki>=</nowiki>0;j<N-1;++j)
&nbsp;&nbsp;if(data[j]>data[j+1])
&nbsp;&nbsp;&nbsp;swap(data[j],data[j+1]);
}


for(int j<nowiki>=</nowiki>0;j<N-1;++j)
to łatwo zauważyć, że <tt>loop</tt> można zdefiniować następująco:
if(data[j]>data[j+1])
swap(data[j],data[j+1]);


if(N>2)
loop(int *data,int N) {
bubble_sort_function(data,N-1);
&nbsp;if(N>0) {
}
&nbsp;&nbsp;if(data[0]>data[1]) swap(data[0],data[1]);
 
&nbsp;&nbsp;loop(++data,N-1);
To jeszcze nie to co trzeba bo musimy zapisać pętle w postaci
&nbsp;}
rekurencyjnej. Jeśli oznaczymy sobie
}
 
void loop(int * data,int N) {
for(int j<nowiki>=</nowiki>0;j<N-1;++j)
if(data[j]>data[j+1])
swap(data[j],data[j+1]);
}
 
to  łatwo zauważyć że !loop! można zdefiniować następująco:
 
loop(int *data,int N) {
if(N>0) {
if(data[0]>data[1]) swap(data[0],data[1]);
loop(++data,N-1);
}
}


co natychmiast tłumaczy się na szablony:
co natychmiast tłumaczy się na szablony:


template<int N> inline void loop(int *data) {
template<int N> inline void loop(int *data) {
if(data[0]>data[1]) std::swap(data[0],data[1]);
&nbsp;if(data[0]>data[1]) std::swap(data[0],data[1]);
&nbsp;loop<N-1>(++data);
}
template<> inline void loop<0>(int *data) {};


loop<N-1>(++data);
Szablon funkcji <tt>bubble_sort_template</tt> ma więc postać:
}


template<> inline void loop<0>(int *data) {};
  template<int N>  inline void bubble_sort_template(int * data) {
 
&nbsp;loop<N-1>(data);
Szablon funkcji !bubble_sort_template! ma więc postać:
&nbsp;bubble_sort_template<N-1>(data);
 
}
template<int N>  inline void bubble_sort_template(int * data) {
template<>  inline void bubble_sort_template<2>(int * data) {
loop<N-1>(data);
&nbsp;loop<1>(data);
 
};
bubble_sort_template<N-1>(data);
([http://osilek.mimuw.edu.pl/images/0/0f/Bubble_template.cpp Źródło: bubble_template.cpp])
}
 
template<>  inline void bubble_sort_template<2>(int * data) {
loop<1>(data);
};
/* {mod07/code/bubble_template.cpp}{bubbletemplate.cpp} */


Poniżej znów podaję porównanie czasu wykonywania się 100 milionów  
Poniżej znów podaję porównanie czasu wykonywania się 100 milionów  
wywołań funckji !bubble_sort_function! i !bubble_sort_template! dla tablicy zawierającej 12 liczb całkowitych w kolejności malejącej.   
wywołań funkcji <tt>bubble_sort_function</tt> i <tt>bubble_sort_template</tt> dla tablicy zawierającej 12 liczb całkowitych w kolejności malejącej.   


<div align=center>
{| border=1
{| border=1
|+ <span style="font-variant:small-caps">Uzupelnij tytul</span>
|-  
|-  
|  
!|  
|| bubblesortfunction(a,12)  |bubblesorttemplate<12>(a)  
| bubblesortfunction(a,12)   
|-
| bubblesorttemplate<12>(a)  
|  
|---
-O0 || 43.3   || 42.2   
|align="center"|-O0  
|-
|align="center"|43.3  
|
|align="center"|42.2   
-O1 ||  21.0  ||  4.8
|-
|-
|  
|align="center"|-O1
-O2 ||   20.0   || 3.5
|align="center"|21.0
|align="center"|4.8
|-
|-
|  
|align="center"|-O2
-O3 ||   20.0 ||   3.6
|align="center"|20.0
|align="center"|3.5
|-
|-
|  
|align="center"|-O3
 
|align="center"|20.0
|align="center"|3.6
|}
|}
</div>


Widać że wersja na szablonach jest ok 4-5 razy szybsza. Zachęcam do
Widać, że wersja na szablonach jest ok. 4-5 razy szybsza. Zachęcam do
własnych eksperymentów.
własnych eksperymentów.<br>
 
Zaprojektowany szablon działa tylko dla tablic liczb całkowitych. Jest
Zaprojektowany szablon działa tylko dla tablic liczb całkowitych, jest
to ewidentne ograniczenie, które powinniśmy zlikwidować poprzez dodanie
to ewidentne ograniczenie które powinniśmy zlikwidować poprzez dodanie
doatkowego parametru szablonu. Niestety, prowadzi to do konieczności
doatkowego parametru szablonu. Niestety prowadzi to do konieczności
dokonania specjalizacji częściowej, która nie jest dozwolona dla
dokonania specjalizacji częściowej która nie jest dozwolona dla
szablonów funkcji. Na szczeście nie jest trudno przepisać naszą
szablonów funkcji. Na szczeście nie jest trudno przepisać naszą
implementacje używając szablonów klas. Pozostawiam to jako ćwiczenie
implementację używając szablonów klas. Pozostawiam to jako ćwiczenie
dla czytelników:)
dla czytelników :).


==Rozmiar kodu==
==7.7 Rozmiar kodu==


Jak pokazałem kod generowany przez szablon !bubble_sort_template! jest
Jak pokazałem, kod generowany przez szablon <tt>bubble_sort_template</tt> jest
bardziej efektywny, dzieje się to jednak kosztem jego rozmiaru. Są ku
bardziej efektywny. Dzieje się to jednak kosztem jego rozmiaru. Są ku
temu dwa powody: Po pierwsze podwójna pętla wewnątrz procedury
temu dwa powody. Po pierwsze podwójna pętla wewnątrz procedury
!bubble_sort_function! wykonuje <math>(N-1)*(N-2)/2</math> iteracji i tyle
<tt>bubble_sort_function</tt> wykonuje <math>(N-1)*(N-2)/2</math> iteracji i tyle
linijek powinien mieć w pełni rozwinięty kod w szablonie
linijek powinien mieć w pełni rozwinięty kod w szablonie
!bubble_sort_template!. Po drugie każda instancja szablonu jest
<tt>bubble_sort_template</tt>. Po drugie każda instancja szablonu jest
osobną funkcją i tak !bubble_sort_template<50>! i
osobną funkcją, stąd <tt>bubble_sort_template<50></tt> i
!bubble_sort_template<51>! generują osobny kod każda. W celu
<tt>bubble_sort_template<51></tt> generują osobny kod każda. W celu
sprawdzenia tych przewidywań przedstawiam poniżej rozmiar wynikowego
sprawdzenia tych przewidywań przedstawiam poniżej rozmiar wynikowego
kodu w bajtach dla programu który kompilował funkcję
kodu w bajtach dla programu, który kompilował funkcję
!bubble_sort_template<N>!
<tt>bubble_sort_template<N></tt>.


<div align=center>
{| border=1
{| border=1
|+ <span style="font-variant:small-caps">Uzupelnij tytul</span>
|-  
|-  
|  
|align="center"|N   
N  || size   
|align="center"|size   
|-
|
10  || 9333   
|-
|-
| 30  ||     17846 
|align="center"|10
|align="center"|9333   
|-
|-
| 50  ||     21847  
|align="center"|30
|align="center"|17846  
|-
|-
| 70  ||     40399  
|align="center"|50
|align="center"|21847  
|-
|-
| 90  ||     33296  
|align="center"|70
|align="center"|40399  
|-
|-
| 100 ||       53606
|align="center"|90
|align="center"|33296 
|-
|-
|    
|align="center"|100
 
|align="center"|53606
|}
|}
 
</div>
Widać że choć rozmiar w zasadzie rośnie z <math>N</math> to ta zależność nie jest
Widać, że choć rozmiar w zasadzie rośnie z <math>N</math>, to ta zależność nie jest
nawet monotoniczna. Wynika to pewnie z tego że dla większych <math>N</math>
nawet monotoniczna. Wynika to pewnie z tego, że dla większych <math>N</math>
kompilator nie dokonuje całkowitego rozwiniecią kodu.
kompilator nie dokonuje całkowitego rozwiniecią kodu.


==Podsumowanie==
==7.8 Podsumowanie==

Aktualna wersja na dzień 19:32, 21 sie 2006

7.1 Metaprogramowanie

Ogólnie rzecz biorąc metaprogramowanie oznacza pisanie programów, które piszą programy lub pisania programu, który pisze się sam. W naszym kontekście będzie to oznaczało wykonywanie obliczeń za pomocą szablonów, przy czym obliczenia te są wykonywane podczas kompilacji. Podstawą do tych obliczeń jest rekurencyjna konkretyzacja szablonów. Taką metodą można generować w czasie kompilacji całkiem skomplikowane fragmenty kodu, stąd określenie metaprogramowanie. Przykłady takich "metaszablonów" poznaliśmy już na wykładzie o funkcjach typów. W szczególności działanie na listach typów to właśnie przykłady metaprogramowania. W tym wykładzie przyjrzymy się dokładniej temu zagadnieniu i przeanalizujemy kolejne przykłady.

7.2 Potęgi

Zaczniemy od bardzo prostego przykładu (zob. D. Vandervoorde, N. Josuttis:"C++ Szablony, Vademecum profesjonalisty", rozdz. 17). Napiszemy szablon który ma za zadanie obliczać potęgi liczby 3. Ponieważ w programowaniu za pomocą szablonów musimy posługiwać się rekurencją to zaczynamy od sformułowania problemu w sposób rekurencyjny. To akurat jest bardzo proste:

Przykład 7.1

3N=3*3N1,30=1

Za pomocą szablonów implementujemy to tak (Źródło: Pow.cpp):

template<int  N> struct Pow3 {
  enum {val=3*Pow3<N-1>::val};
};
template<> struct Pow3<0> {
  enum {val=1}; 
};

teraz możemy już użyć w programie np. wyrażenie:

 i=Pow3<4>::val;

Podstawową zaletą metaprogramowania i głównym powodem jego używania jest fakt, że to wyrażenie jest obliczane w czasie kompilacji i efekt jest taki sam jak podstawienia:

 i=81;

Można też zastosować szablon funkcji:

template<int N> int pow3() {
  return 3*pow3<N-1>();
};
template<> int pow3<0>() {return 1;}
cout<<pow3<4>()<<endl;

(Źródło: Powf.cpp)

Nietrudno jest uogólnić powyższy kod tak aby wyliczał potęgi dowolnej liczby:

template<int K,int N> struct Pow { enum 
 {val=K*Pow<K,N-1>::val}; };
template<int K> struct Pow<K,0> {
 enum {val=1};
};

(Źródło: Pow.cpp)

Tutaj już nie można wykorzystać szablonu funkcji, bo nie zezwala on na specjalizację częściową.
Ograniczeniem dla takich obliczeń jest implementacja kompilatora, przede wszystkim założony limit głebokości rekurencyjnego konkretyzowania szablonów. Dla kompilatora g++ jest on ustawiany za pomocą opcji i defaultowo wynosi 500, dlatego już konkretyzacja Pow<1,500>::val się nie powiedzie. Konkretyzacja szablonów wymaga też pamięci i może się zdarzyć, że kompilator wyczerpie limit pamięci lub czasu.
Kolejne ograniczenie to konieczność rachunków na liczbach całkowitych. Wiąże się to z faktem, że tylko stałe całkowitoliczbowe mogą być parametrami szablonów.

7.3 Ciąg Fibonacciego

Po opanowaniu powyższych przykładów obliczanie wyrazów ciągu Fibonacciego jest prostym zadaniem. Przytoczymy je jednak tutaj, aby zaprezentować pewną bardzo sympatyczną cechę metaprogramowania za pomocą szablonów. Ciąg Fibonacciego jest definiowany rekurencyjnie:

Przykład 7.2

fn=fn1+fn2,f1=f2=1

więc jego implementacja jest natychmiastowa:

template<int N> struct Fibonacci {
 enum {val = Fibonacci<N-1>::val+Fibonacci<N-2>::val};
};
template<> struct Fibonacci<1> {
 enum {val = 1};
};
template<> struct Fibonacci<2> {
 enum {val = 1};
};

(Źródło: fibonacci_template.cpp)

Przykład ten nie wart byłby może i wspomnienia gdyby nie fakt, że rekurencyjna implementacja ciągu Fibonacciego jest bardzo nieefektywna. Jeśli zaimplementujemy ją w zwykłym kodzie

int fibonacci(int n) {
 if(n==1) return 1;
 if(n==2) return 1;
 return fibonacci(n-1)+fibonacci(n-2);
}

(Żródło: fibonacci.cpp)

to obliczanie fibonacci(45) zajmie np.na moim komputerze ok. 8 sekund. Tymczasem szablon kompiluje sie poniżej jednej sekundy! Skąd taka różnica? Czyżby kompilator był bardziej wydajny niż generowany przez niego kod? W przypadku zwykłego kodu długi czas wykonania bierze się z ogromnej liczby wywołań funkcji fibonacci. Liczba ta rośnie wykładniczo z n i większość czasu jest marnowana na wielokrotne wywoływanie funkcji z tymi samymi argumentami.

 W przypadku użycia metaprogramu szablony konkretyzowane są tylko raz. Więc jeśli już raz policzymy np. Fibonacci<25>::val to kolejne żądanie nie spowoduje już rozwinięcia rekurencyjnego, a tylko podstawienie istniejącej wartości. Jak widzieliśmy zysk jest ogromny. Takie pamiętanie wyników raz wywołanych funkcji nazywane jest też programowaniem dynamicznym. Jedynym znanym mi językiem, który bezpośrednio wspiera taki mechanizm jest Mathematica.

7.4 Pierwiastek kwadratowy

Rozważymy teraz trudniejszy przykład szablonu obliczającego pierwiastek kwadratowy ([1] rozdz. 17), choć tak naprawdę ten kod jest bardziej uniwersalny i, jak zobaczymy, łatwo za jego pomocą zaimplementować inne funkcje. Ponieważ jesteśmy ograniczeni do arytmetyki liczb całkowitych, tak naprawdę nie liczymy pierwiastka z n ale jego przybliżenie: największą liczbę całkowitą k, taką że k*k<=n. W tym celu zastosujemy algorytm przeszukiwania binarnego:

int sqrt(int n,int low, int high) {
 if(low==high) return low;
 int mid=(low+high+1)/2;
 if(mid*mid > n ) 
   return sqrt(n,low,mid-1);
 else
   return sqrt(n,mid,high);
}

co już łatwo przetłumaczyć na szablon:

template<int N,int L=1,int H=N> struct Sqrt{
 enum  {mid=(L+H+1)/2};
 enum  {res= (mid*mid> N)? (int)Sqrt<N,L,mid-1>::res :
  (int)Sqrt<N,mid,H>::res};
};
template<int N,int L> struct Sqrt<N,L,L> {
 enum {res=L};
};

(Źródło: sqrt.cpp)

Łatwo sprawdzić, że kod ten działa poprawnie. Niestety posiada on istotną wadę. W trakcie konkretyzacji szablonu konkretyzowane są oba szablony występujące w wyrażeniu warunkowym, nawet ten, który nie będzie potem używany. Tak więc wykonywana jest duża liczba konkretyzacji, z których tylko ułamek jest potrzebny (zob. rysunek). Jakie to obciążenie dla kompilatora to łatwo sprawdzić kompilując kod, w którym wywołujemy Sqrt<10000>. Ja nie doczekałem się na koniec kompilacji.

Na szczęście istnieje rozwiązanie - należy użyć szablonu If_then_else (zob. rozdział 5.2.1):

template<int N,int L=1,int H=N> struct Sqrt{
 enum  {mid=(L+H+1)/2};
 typedef typename If_then_else<
  (mid*mid> N),
  Sqrt<N,L,mid-1>,
  Sqrt<N,mid,H> >::Result tmp;
 enum  {res= tmp::res};
};
template<int N,int L> struct Sqrt<N,L,L> {
 enum {res=L};
};

(Źródło: sqrt_ifte.cpp)

Ten kod powoduje już tylko konkretyzację rzeczywiście wymaganych szablonów, a tych jest dużo mniej: rzędu O(logN). Tym razem kompilacja wyrażenia Sqrt<10000> powiedzie się bez trudu.

7.5 Pow(x)

Jak dotąd używaliśmy metaprogramowania do wyliczania wartości stałych. Teraz postaramy się wygenerować funkcje. Zacznijmy od pytania: po co? Pomijając syndrom Mount Everestu (wchodzę na niego bo jest), to głównym powodem jest nadzieja uzyskania bardziej wydajnego kodu. Weźmy dalej za przykład liczenie potęgi, tym razem dowolnej liczby zmiennoprzecinkowej:

double pow_int(double x,int n) {
double res=1.0;
for(int i=0;i<n;++i)
 res*=x;
return res;
};

(Źródło: powx.cpp)

Patrząc na ten kod widzimy, że w pętli wykonywana jest bardzo prosta instrukcja. Możemy więc się obawiać, że instrukcje związane z obsługą pętli mogą stanowić spory narzut. Co więcej, ich obecność utrudnia kompilatorowi optymalizację kodu oraz może uniemożliwić rozwinięcie funkcji w miejscu wywołania. Najlepiej by było zaimplementować tę funkcję w ten sposób:

pow(x,n)= x*...*x; /*n razy*/

np.:

double pow2(double x) {return x*x;}
double pow3(double x) {return x*x*x;}
double pow4(double x) {return x*x*x*x;}
...

Wymaga to jednak kodowania ręcznego dla każdej potęgi, której potrzebujemy. Ten sam efekt możemy osiągnąc za pomocą następującego szablonu funkcji:

template<int N> inline double pow(x) {return x*pow<N-1>(x);}
template<>  inline double pow<0>(x) {return 1.0;}

(Źródło: powx.cpp)

pod warunkiem, że kompilator rozwinie wszystkie wywołania.

Poniżej zamieszczam wyniki pomiarów wykonania 100 milionów wywołań każdej funkcji (powx.cpp). Czas jest podany w sekundach. Zamieściłem wyniki dla różnych ustawień optymalizacji.

pow_int(x,5) pow<5>(x)
-O0 7.22 14.78
-O1 0.37 0.04
-O2 0.42 0.05
-O3 0.42 0.05

Widać dramatyczną różnicę po włączeniu optymalizacji. Wiąże się to prawdopodobnie z umożliwieniem rozwijania funkcji inline. Potem wyniki już się nie zmieniają ale widać, że szablon pow wygenerował funkcję 10 razy szybszą od pozostałych. Pokazuje to dobitnie, że optymalizacja ręczna ciągle ma sens.

7.6 Sortowanie bąbelkowe

Zakończymy ten wykład bardziej skomplikowanym przykładem pokazującym, że metaprogramowanie można stosować nie tylko do obliczeń numerycznych. Popatrzmy na kod implementujący sortowanie bąbelkowe:

inline void swap (int &a,int &b) {int       tmp=a;a=b;b=tmp;};
void bubble_sort_function (int* data, int N) {
 for(int i = N-1; i>0; --i)
  for(int j=0;j<i;++j)
   if(data[j]>data[j+1]) 
    swap(data[j],data[j+1]);
}

(Źródło: bubble_template.cpp)

Znów widzimy tu dwie pętle i wszystkie uwagi dotyczące funkcji pow_int tu też się stosują. Postaramy się więc zdefiniować szablon, który dokona rozwinięcia tych obu pętli. Np. dla N=3 chcielibyśmy otrzymać następujący kod:

//i=2 j=0
if(data[0]>data[1]) swap(data[0],data[1]);
//i=2 j=1
if(data[1]>data[2]) swap(data[1],data[2]);
//i=1 j=0
if(data[0]>data[1]) swap(data[0],data[1]);

Jeśli Państwo śledzili wykład (przynajmniej ten), to już Państwo wiedzą, że pierwszym krokiem musi być przepisanie kodu sortowania na postać rekurencyjną:

void bubble_sort_function (int* data, int N) {
 for(int j=0;j<N-1;++j)
  if(data[j]>data[j+1]) 
   swap(data[j],data[j+1]);
 if(N>2)
  bubble_sort_function(data,N-1);
}

To jeszcze nie to co trzeba, bo musimy zapisać pętle w postaci rekurencyjnej. Jeśli oznaczymy sobie:

void loop(int * data,int N) {
 for(int j=0;j<N-1;++j)
  if(data[j]>data[j+1])
   swap(data[j],data[j+1]);
}

to łatwo zauważyć, że loop można zdefiniować następująco:

loop(int *data,int N) {
 if(N>0) {
  if(data[0]>data[1]) swap(data[0],data[1]);
  loop(++data,N-1);
 }
}

co natychmiast tłumaczy się na szablony:

template<int N> inline void loop(int *data) {
 if(data[0]>data[1]) std::swap(data[0],data[1]);
 loop<N-1>(++data);
}
template<> inline void loop<0>(int *data) {};

Szablon funkcji bubble_sort_template ma więc postać:

template<int N>  inline void bubble_sort_template(int * data) {
 loop<N-1>(data);
 bubble_sort_template<N-1>(data);
}
template<>  inline void bubble_sort_template<2>(int * data) {
 loop<1>(data);
};

(Źródło: bubble_template.cpp)

Poniżej znów podaję porównanie czasu wykonywania się 100 milionów wywołań funkcji bubble_sort_function i bubble_sort_template dla tablicy zawierającej 12 liczb całkowitych w kolejności malejącej.

bubblesortfunction(a,12) bubblesorttemplate<12>(a)
-O0 43.3 42.2
-O1 21.0 4.8
-O2 20.0 3.5
-O3 20.0 3.6

Widać, że wersja na szablonach jest ok. 4-5 razy szybsza. Zachęcam do własnych eksperymentów.
Zaprojektowany szablon działa tylko dla tablic liczb całkowitych. Jest to ewidentne ograniczenie, które powinniśmy zlikwidować poprzez dodanie doatkowego parametru szablonu. Niestety, prowadzi to do konieczności dokonania specjalizacji częściowej, która nie jest dozwolona dla szablonów funkcji. Na szczeście nie jest trudno przepisać naszą implementację używając szablonów klas. Pozostawiam to jako ćwiczenie dla czytelników :).

7.7 Rozmiar kodu

Jak pokazałem, kod generowany przez szablon bubble_sort_template jest bardziej efektywny. Dzieje się to jednak kosztem jego rozmiaru. Są ku temu dwa powody. Po pierwsze podwójna pętla wewnątrz procedury bubble_sort_function wykonuje (N1)*(N2)/2 iteracji i tyle linijek powinien mieć w pełni rozwinięty kod w szablonie bubble_sort_template. Po drugie każda instancja szablonu jest osobną funkcją, stąd bubble_sort_template<50> i bubble_sort_template<51> generują osobny kod każda. W celu sprawdzenia tych przewidywań przedstawiam poniżej rozmiar wynikowego kodu w bajtach dla programu, który kompilował funkcję bubble_sort_template<N>.

N size
10 9333
30 17846
50 21847
70 40399
90 33296
100 53606

Widać, że choć rozmiar w zasadzie rośnie z N, to ta zależność nie jest nawet monotoniczna. Wynika to pewnie z tego, że dla większych N kompilator nie dokonuje całkowitego rozwiniecią kodu.

7.8 Podsumowanie