Zaawansowane CPP/Wykład 1: Szablony I: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Mirek (dyskusja | edycje)
 
(Nie pokazano 22 wersji utworzonych przez 5 użytkowników)
Linia 1: Linia 1:
==Wprowadzenie==
 


==Szablony funkcji==
==Szablony funkcji==
Linia 80: Linia 80:
makra:
makra:


#define max(a,b) ( (a>b)?a:b) )  
<Source>
 
#define max(a,b) ( (a>b)?a:b) )  
</Source>
lub używanie wskaźników typów ogólnych, takich jak <tt>void *</tt>,
lub używanie wskaźników typów ogólnych, takich jak <tt>void *</tt>,
jak np. w funkcji <tt>qsort</tt> ze standardowej biblioteki C:   
jak np. w funkcji <tt>qsort</tt> ze standardowej biblioteki C:   
 
<Source>
void qsort(void *base, size_t nmemb, size_t size,
void qsort(void *base, size_t nmemb, size_t size,
            int(*compar)(const void *, const void *));  
          int(*compar)(const void *, const void *));  
 
</Source>
Mimo iż użyteczne, żadne z
Mimo iż użyteczne, żadne z
tych rozwiązań nie może zostać uznane za wystarczająco ogólne i bezpieczne.
tych rozwiązań nie może zostać uznane za wystarczająco ogólne i bezpieczne.
Linia 95: Linia 96:
wyrafinowana odmiana rzutowania na <tt>void *</tt>.  Polega  na
wyrafinowana odmiana rzutowania na <tt>void *</tt>.  Polega  na
zdefiniowaniu ogólnego typu dla obiektów, które mogą być porównywane:
zdefiniowaniu ogólnego typu dla obiektów, które mogą być porównywane:
 
<Source>
class GreaterThenComparable {  
class GreaterThanComparable {  
virtual bool  
public:
operator>(const GreaterThenComparable &a,
  virtual bool operator>(const GreaterThanComparable &) const = 0;  
          const GreaterThenComparable &b) <nowiki>=</nowiki> 0;  
};
}
</Source>
 
następnie zdefiniowaniu funkcji <tt>max</tt> w postaci:  
następnie zdefiniowaniu funkcji <tt>max</tt> w postaci:  


const GreaterThenComparable &
<Source>
max(const GreaterThenComparable &a,  
const GreaterThanComparable &
    const GreaterThenComparable &b) {  
max(const GreaterThanComparable &a,  
      return (a>b)? a:b;  
    const GreaterThanComparable &b) {  
    }  
      return (a>b)? a:b;  
 
    }  
([http://osilek.mimuw.edu.pl/images/c/c6/Max_oop.cpp Źródło: max_oop.cpp])
</Source>
([[media:Max_oop.cpp| Źródło: max_oop.cpp]])


i używaniu jej np. w następujący sposób:
i używaniu jej np. w następujący sposób:


class Int:public GreaterThenComparable {  
<Source>
  int val;
class Int:public GreaterThanComparable {  
  public: Int(int i <nowiki>=</nowiki> 0):val(i) {};  
  int val;
  operator int() {return val;};
public: Int(int i <nowiki>=</nowiki> 0):val(i) {};  
  virtual bool  
  operator int() {return val;};
  operator>(const GreaterThenComparable &b) const {
  virtual bool  
    return val > static_cast<const Int&>(b).val;
  operator>(const GreaterThanComparable &b) const {
  }  
    return val > static_cast<const Int&>(b).val;
};<br>
  }  
main() {
};
Int a(1),b(2);
Int c;
c <nowiki>=</nowiki> (const Int &)::max(a,b);
cout<<(int)c<<endl;
}


([http://osilek.mimuw.edu.pl/images/c/c6/Max_oop.cpp Źródło: max_oop.cpp])
main() {
  Int a(1),b(2);
  Int c;
  c = (const Int &)::max(a,b);
  cout<<(int)c<<endl;
}
</Source>
([[media:Max_oop.cpp | Źródło: max_oop.cpp]])


Widać więc wyraźnie, że to podejście wymaga sporego nakładu pracy, a
Widać więc wyraźnie, że to podejście wymaga sporego nakładu pracy, a
Linia 136: Linia 139:
wysoce niepraktyczne. Ogólnie rzecz biorąc ma ono następujące wady:
wysoce niepraktyczne. Ogólnie rzecz biorąc ma ono następujące wady:


# Wymaga dziedziczenia z abstrakcyjnej klasy bazowej <tt>GreaterThenComparable</tt>, czyli może być zastosowane tylko do typów zdefiniowanych przez nas.  Inne typy, w tym typy wbudowane, wymagają kopertowania w klasie opakowującej, takiej jak klasa <tt>Int</tt> w powyższym przykładzie.
# Wymaga dziedziczenia z abstrakcyjnej klasy bazowej <tt>GreaterThanComparable</tt>, czyli może być zastosowane tylko do typów zdefiniowanych przez nas.  Inne typy, w tym typy wbudowane, wymagają kopertowania w klasie opakowującej, takiej jak klasa <tt>Int</tt> w powyższym przykładzie.
# Ponieważ potrzebujemy polimorfizmu funkcja <tt>operator>()</tt> musi być funkcją wirtualną, a więc musi być funkcją składową klasy i nie może być typu <tt>inline</tt>. W przypadku tak prostych funkcji niemożność rozwinięcia ich w miejscu wywołania może prowadzić do dużych narzutów w czasie wykonania.
# Ponieważ potrzebujemy polimorfizmu funkcja <tt>operator>()</tt> musi być funkcją wirtualną, a więc musi być funkcją składową klasy i nie może być typu <tt>inline</tt>. W przypadku tak prostych funkcji niemożność rozwinięcia ich w miejscu wywołania może prowadzić do dużych narzutów w czasie wykonania.
# Funkcja <tt>max</tt> zwraca zawsze referencje do <tt>GreaterThenComparable</tt>, więc konieczne jest rzutowanie na typ wynikowy (tu <tt>Int</tt>).
# Funkcja <tt>max</tt> zwraca zawsze referencje do <tt>GreaterThanComparable</tt>, więc konieczne jest rzutowanie na typ wynikowy (tu <tt>Int</tt>).


===Szablony funkcji===
===Szablony funkcji===
Linia 150: Linia 153:
Definicja szablonu funkcji <tt>max</tt>, odpowiadającej definicji [[#prz.1.1|1.1]] wygląda następująco:
Definicja szablonu funkcji <tt>max</tt>, odpowiadającej definicji [[#prz.1.1|1.1]] wygląda następująco:


template<typename T> T max(T a,T b) {return (a>b)?a:b;};  
<Source>
 
template<typename T> T max(T a,T b) {return (a>b)?a:b;};  
([http://osilek.mimuw.edu.pl/images/c/c4/Max_template.cpp Źródło: max_template.cpp])
</Source>
([[media:Max_template.cpp | Źródło: max_template.cpp]])


Przyjrzyjmy się jej z bliska. Wyrażenie <tt>template<typename T></tt>
Przyjrzyjmy się jej z bliska. Wyrażenie <tt>template<typename T></tt>
Linia 170: Linia 174:
szablonu (w praktyce można uniknąć konieczności jawnej specyfikacji
szablonu (w praktyce można uniknąć konieczności jawnej specyfikacji
argumentów szablonu, opiszemy to w następnych częściach wykładu):
argumentów szablonu, opiszemy to w następnych częściach wykładu):
 
<Source>
int i,j,k;
int i,j,k;
k<nowiki>=</nowiki>max<int>(i,j);
k=max<int>(i,j);
 
</Source>
Takie użycie szablonu spowoduje wygenerowanie identycznej funkcji jak [[#prz.1.1|1.1]]. W powyższym przypadku za <tt>T</tt> podstawiamy <tt>int</tt>. Oczywiście możemy podstawić za <tt>T</tt> dowolny typ i używając szablonów program [[#prz.1.2|1.2]] można zapisać następująco:
Takie użycie szablonu spowoduje wygenerowanie identycznej funkcji jak [[#prz.1.1|1.1]]. W powyższym przypadku za <tt>T</tt> podstawiamy <tt>int</tt>. Oczywiście możemy podstawić za <tt>T</tt> dowolny typ i używając szablonów program [[#prz.1.2|1.2]] można zapisać następująco:
 
<Source>
template<typename T> T max(T a,T b) {return (a>b)?a:b;}<br>
template<typename T> T max(T a,T b) {return (a>b)?a:b;}
main() {
main() {
cout<<::max<int>(7,5)<<endl;
  cout<<::max<int>(7,5)<<endl;
cout<<::max<double>(3.1415,2.71)<<endl;
  cout<<::max<double>(3.1415,2.71)<<endl;
cout<<::max<string>("Ania","Basia")<<endl;
  cout<<::max<string>("Ania","Basia")<<endl;
}
}
 
</Source>
([http://osilek.mimuw.edu.pl/images/c/c4/Max_template.cpp Źródło: max_template.cpp])
([[media:Max_template.cpp | Źródło: max_template.cpp]])


W powyższym kodzie użyliśmy konstrukcji <tt>::max(a,b)</tt>. Dwa dwukropki
W powyższym kodzie użyliśmy konstrukcji <tt>::max(a,b)</tt>. Dwa dwukropki
Linia 194: Linia 198:
kompilacji, np.
kompilacji, np.


complex<double> c1,c2;
<Source>
max<complex<double> >(c1,c2); <i>brak operatora ></i>
complex<double> c1,c2;
 
max<complex<double> >(c1,c2); //brak operatora >
([http://osilek.mimuw.edu.pl/images/c/c4/Max_template.cpp Źródło: max_template.cpp])
</Source>
([[media:Max_template.cpp | Źródło: max_template.cpp]])


lub
lub
<Source>
class X {
private:
  X(const X &){};
};
X a,b;
max<X>(a,b); //prywatny (niewidoczny) konstruktor kopiujący
</Source>


X {
([[media:Max_template.cpp | Źródło: max_template.cpp]])
private:
X(const X &){};
};<br>
X a,b;
max<X>(a,b); <i>prywatny (niewidoczny) konstruktor kopiujący</i>
 
([http://osilek.mimuw.edu.pl/images/c/c4/Max_template.cpp Źródło: max_template.cpp])


Ogólnie rzecz biorąc, każdy szablon definiuje pewną klasę typów, które
Ogólnie rzecz biorąc, każdy szablon definiuje pewną klasę typów, które
Linia 237: Linia 243:
umożliwia automatyczną konwersję typów.  Ilustruje to poniższy kod:
umożliwia automatyczną konwersję typów.  Ilustruje to poniższy kod:


template<typename T> T max(T a,T b) {return (a>b)?a:b;}<br>
<Source>
main() {
template<typename T> T max(T a,T b) {return (a>b)?a:b;}
cout<<::max(3.14,2)<<endl;
main() {
<i>błąd: kompilator nie jest w stanie wydedukowac argumentu szablonu, bo typy  
  cout<<::max(3.14,2)<<endl;
argumentów (int,double) nie pasują  do (T,T)</i><br>
  // błąd: kompilator nie jest w stanie wydedukowac argumentu szablonu, bo typy  
cout<<::max<int>(3.14,2)<<endl;
  // argumentów (double,int) nie pasują  do (T,T)
<i>podając argument jawnie wymuszamy sygnaturę int max(int,int), a co za tym  
 
idzie automatyczną konwersję argumentu 1 do int-a</i><br>
  cout<<::max<int>(3.14,2)<<endl;
cout<<::max<double>(3.14,2)<<endl;
  // podając argument jawnie wymuszamy sygnaturę int max(int,int), a co za tym  
<i>podając argument szablonu jawnie wymuszamy sygnaturę <tt>double max(double,double)</tt>,
  // idzie automatyczną konwersję argumentu 1 do int-a
a co za tym idzie automatyczną konwersję argumentu 2 do double-a</i><br>
int i;
cout<<::max<int *>(&i,i)<<endl;
<i>błąd: nie istnieje konwersja z typu int na int</i>


([http://osilek.mimuw.edu.pl/images/c/c4/Max_template.cpp Źródło: max_template.cpp])
  cout<<::max<double>(3.14,2)<<endl;
  // podając argument szablonu jawnie wymuszamy sygnaturę
  // double max(double,double)
  // a co za tym idzie automatyczną konwersję argumentu 2 do double-a
  int i;
  cout<<::max<int *>(&i,i)<<endl;
  //błąd: nie istnieje konwersja z typu int na int*
</Source>
([[media:Max_template.cpp | Źródło: max_template.cpp]])


Może warto zauważyć, że automatyczna dedukcja parametrów szablonu jest
Może warto zauważyć, że automatyczna dedukcja parametrów szablonu jest
Linia 285: Linia 296:
  }
  }


([http://osilek.mimuw.edu.pl/images/8/84/Convert.cpp Źródło: convert.cpp])
([[media:Convert.cpp | Źródło: convert.cpp]])


===Używanie szablonów===
===Używanie szablonów===
Linia 307: Linia 318:
{{kotwica|rys.1.1|}}<center>
{{kotwica|rys.1.1|}}<center>
<div class="thumb"><div style="width:750px;">
<div class="thumb"><div style="width:750px;">
<flash>file=cpp-1-kod1.swf|width=750|height=500</flash>
[[Grafika:Cpp-1-kod1.png]]
<div.thumbcaption>Rysunek 1.1. Przykład organizacji kodu C++ w przypadku
<div.thumbcaption>Rysunek 1.1. Przykład organizacji kodu C++ w przypadku
użycia zwykłych funkcji.</div>
użycia zwykłych funkcji.</div>
Linia 334: Linia 345:


{{kotwica|rys.1.2|}}<center>
{{kotwica|rys.1.2|}}<center>
<div class="thumb"><div style="width:750px;">
 
<flash>file=cpp-1-kod2.swf|width=750|height=500</flash>
[[File:cpp-1-kod2.svg|750x500px|thumb|center|Rysunek 1.2. Przykład błędnej organizacji kodu w przypadku
<div.thumbcaption>Rysunek 1.2. Przykład błędnej organizacji kodu w przypadku
użycia szablonów]]
użycia szablonów.</div>
</div></div>
</center>


Istnieją różne rozwiązania tego problemu. Najprościej chyba jest
Istnieją różne rozwiązania tego problemu. Najprościej chyba jest
Linia 354: Linia 362:


{{kotwica|rys.1.3|}}<center>
{{kotwica|rys.1.3|}}<center>
<div class="thumb"><div style="width:750px;">
[[File:cpp-1-kod3.svg|750x500px|thumb|center|Rysunek 1.3. Przykład organizacji kodu z szablonami, wykorzystującego strategię włączania.]]
<flash>file=cpp-1-kod3.swf|width=750|height=500</flash>
<div.thumbcaption>Rysunek 1.3. Przykład organizacji kodu z szablonami, wykorzystującego strategię włączania.</div>
</div></div>
</center>
</center>


Linia 386: Linia 391:


{{kotwica|rys.1.4|}}<center>
{{kotwica|rys.1.4|}}<center>
<div class="thumb"><div style="width:750px;">
<div class="thumb"><div style="width:804px;">
<flash>file=cpp-1-kod4.swf|width=750|height=500</flash>
[[Grafika:Cpp-1-kod4.png]]
<div.thumbcaption>Rysunek 1.4. Przykład organizacji kodu z szablonami,
<div.thumbcaption>Rysunek 1.4. Przykład organizacji kodu z szablonami,
wykorzystującego jawną konkretyzację.</div>
wykorzystującego jawną konkretyzację.</div>
Linia 417: Linia 422:
  };
  };


([http://osilek.mimuw.edu.pl/images/0/02/Dot_product.cpp Źródło: dot_product.cpp])
([[media:Dot_product.cpp | Źródło: dot_product.cpp]])


Po raz drugi zwracam uwagę na kolejność parametrów szablonu na liście
Po raz drugi zwracam uwagę na kolejność parametrów szablonu na liście
Linia 430: Linia 435:
  }
  }


([http://osilek.mimuw.edu.pl/images/0/02/Dot_product.cpp Źródło: dot_product.cpp])
([[media:Dot_product.cpp | Źródło: dot_product.cpp]])


Parametry pozatypowe są zresztą "ciężko dedukowalne". Właściwie
Parametry pozatypowe są zresztą "ciężko dedukowalne". Właściwie
Linia 469: Linia 474:
  }
  }


([http://osilek.mimuw.edu.pl/images/1/16/Deduce_N.cpp Źródło: deduce_N.cpp])
([[media:Deduce_N.cpp | Źródło: deduce_N.cpp]])


===Szablony metod===
===Szablony metod===
Linia 475: Linia 480:
Jak na razie definiowaliśmy szablony zwykłych funkcji. C++ umożliwia
Jak na razie definiowaliśmy szablony zwykłych funkcji. C++ umożliwia
również definiowanie szablonów metod klasy np.:
również definiowanie szablonów metod klasy np.:
<Source>
struct Max {
  template<typename T> T max(T a,T b) {return (a>b)?a:b;}
};
main() {
  Max m;
  m.max(1,2);
}
</Source>


struct Max {
([[media:Max_method.cpp | Źródło: max_method.cpp]])
template<typename T> max(T a,T b) {return (a>b)?a:b;}
};<br>
main() {
Max m;
m.max(1,2);
}
 
([http://osilek.mimuw.edu.pl/images/b/bb/Max_method.cpp Źródło: max_method.cpp])


Szablonów metod składowych dotyczą takie same reguły jak szablonów funkcji.
Szablonów metod składowych dotyczą takie same reguły jak szablonów funkcji.
Linia 503: Linia 509:
w prawdziwych aplikacjach:
w prawdziwych aplikacjach:


class Stack {
<Source>
private:<br>
class Stack {
int rep[N];
private:
_size_t top;<br>
  int rep[N];
public:
  size_t top;
static const size_t N<nowiki>=</nowiki>100;
public:
Stack():_top(0) {};<br>
  static const size_t N=100;
void push(int val) {_rep[_top++]<nowiki>=</nowiki>val;}
  Stack():_top(0) {};
int pop()         {return rep[--top];}
  void push(int val) {_rep[_top++]=val;}
bool is_empty {return (top<nowiki>=</nowiki><nowiki>=</nowiki>0);}<br>
  int pop() {return rep[--top];}
}
  bool is_empty {return (top==0);}
 
}
</Source>
Ewidentnie ten kod będzie identyczny dla stosu obiektów dowolnego
Ewidentnie ten kod będzie identyczny dla stosu obiektów dowolnego
innego typu, pod warunkiem, że typ ten posiada zdefiniowany
innego typu, pod warunkiem, że typ ten posiada zdefiniowany
Linia 535: Linia 542:
następująco:
następująco:


template<typename T> class Stack {
<Source>
public:
template<typename T> class Stack {
   static const size_t N<nowiki>=</nowiki>100;
public:
private:<br>
   static const size_t N=100;
   T rep[N];
private:
   size_t top;<br>
   T _rep[N];
public:<br>
   size_t _top;<br>
   Stos_int():_top(0) {};<br>
public:
   void push(T val) {_rep[_top++]<nowiki>=</nowiki>val;}
   Stack():_top(0) {};
   T pop()         {return rep[--top];}
   void push(T val) {_rep[_top++]=val;}
   bool is_empty   {return (top<nowiki>=</nowiki><nowiki>=</nowiki>0);}  
   T pop() {return _rep[--_top];}
   bool is_empty {return (_top==0);}  
  };
  };
</Source>


([http://osilek.mimuw.edu.pl/images/a/a5/Stack.cpp Źródło: Stack.cpp])
([[media:Stack.cpp | Źródło: stack.cpp]])


Tak zdefiniowanego szablonu możemy używać podając jawnie jego argumenty.
Tak zdefiniowanego szablonu możemy używać podając jawnie jego argumenty.


Stack<string> st ;<br>
<Source>
st.push("ania");
Stack<string> st ;
st.push("asia");
st.push("ania");
st.push("basia");<br>
st.push("asia");
while(!st.is_empty() ){
st.push("basia");
        cout<<st.pop()<<endl;
while(!st.is_empty() ){
}
  cout<<st.pop()<<endl;
}
</Source>


([http://osilek.mimuw.edu.pl/images/a/a5/Stack.cpp Źródło: Stack.cpp])
([[media:Stack.cpp | Źródło: stack.cpp]])


Dla szablonów klas nie ma możliwości automatycznej dedukcji argumentów
Dla szablonów klas nie ma możliwości automatycznej dedukcji argumentów
szablonu ponieważ klasy nie posiadają argumentów wywołania które
szablonu, ponieważ klasy nie posiadają argumentów wywołania, które
mogłyby do tej dedukcji posłużyć. Jest natomiast możliwość podania
mogłyby do tej dedukcji posłużyć. Jest natomiast możliwość podania
argumentów domyślnych np.
argumentów domyślnych, np.
 
<Source>
template<typename T <nowiki>=</nowiki> int > Stack {
template<typename T = int> Stack {
...
  ...
}
}
 
</Source>
([http://osilek.mimuw.edu.pl/images/a/a5/Stack.cpp Źródło: Stack.cpp])
([[media:Stack.cpp | Źródło: stack.cpp]])


Wtedy możemy korzystać ze stosu bez podawania argumentów szablonu i
Wtedy możemy korzystać ze stosu bez podawania argumentów szablonu i
wyrażenie
wyrażenie


Stack s;
<Source>Stack s;</Source>


będzie równoważne wyrażeniu:  
będzie równoważne wyrażeniu:  


Stack<int> s;  
<Source>Stack<int> s;</Source>


Dla domyślnych argmentów szablonów klas obowiązują te same reguły co dla
Dla domyślnych argmentów szablonów klas obowiązują te same reguły, co dla
domyślnych argumentów wywołania funkcji.  
domyślnych argumentów wywołania funkcji.  


Należy pamiętać że każda konkretyzacja szablony klasy dla  
Należy pamiętać, że każda konkretyzacja szablonu klasy dla  
różniących się zestawów argumentów jest osobną klasą:  
różniących się zestawów argumentów jest osobną klasą:  
<Source>
Stack<int> si;
Stack<double> sd;
sd=si; //błąd: to są obiekty różnych klas a nie zdefiniowano przypisania
</Source>
([[media:Stack.cpp | Źródło: stack.cpp]])


Stack<int> si;
Okazuje się, że próba zdefiniowania operatora przypisania, który
Stack<double> sd;
np. przypisywałby do siebie stosy różnych typów, nie jest łatwa,
sd<nowiki>=</nowiki>si; <i>błąd: to są obiekty różnych klas</i>
 
([http://osilek.mimuw.edu.pl/images/a/a5/Stack.cpp Źródło: Stack.cpp])
 
Oczywiście stadardowy operator przypisania i tak byłby niepoprawny.
Okazuje się jednak że próba zdefiniowania operatora przypisania który
np. przypisywałby do siebie stosy różnych typów nie jest łatwa
ponieważ  dwa takie stosy nie widzą swoich reprezentacji.
ponieważ  dwa takie stosy nie widzą swoich reprezentacji.


Linia 603: Linia 613:
Zestaw możliwych parametrów szablonów klas jest taki sam jak dla
Zestaw możliwych parametrów szablonów klas jest taki sam jak dla
szablonów funkcji. Podobnie najczęściej wykorzystywane są wyrażenia
szablonów funkcji. Podobnie najczęściej wykorzystywane są wyrażenia
całkowitoliczbowe. W naszym przykładzie ze stosem możemu ich użyć do
całkowitoliczbowe. W naszym przykładzie ze stosem możemy ich użyć do
przekazania rozmiaru stosu:
przekazania rozmiaru stosu:


Linia 617: Linia 627:
  }
  }


([http://osilek.mimuw.edu.pl/images/c/cb/Stack_N.cpp Źródło: Stack_N.cpp])
([[media:Stack_N.cpp | Źródło: stack_N.cpp]])


Podkreślam jeszcze raz że <tt>Stack<int,100></tt> i <tt>Stack<int,101></tt> to
Podkreślam jeszcze raz, że <tt>Stack<int,100></tt> i <tt>Stack<int,101></tt> to
dwie różne klasy.
dwie różne klasy.


===Szablony parametrów szablonu===
===Szablony parametrów szablonu===


Stos jest nie tyle strukturą danych ile sposobem dostępu do nich.
Stos jest nie tyle strukturą danych, ile sposobem dostępu do nich.
Stos realizuje regułę LIFO czyli Last In First Out. W tym sensie nie
Stos realizuje regułę LIFO czyli Last In First Out. W tym sensie nie
jest istotne w jaki sposób dane są na stosie przechowywane. Może to
jest istotne w jaki sposób dane są na stosie przechowywane. Może to
być tablica jak w powyższych przykładach, ale może to być praktycznie
być tablica, jak w powyższych przykładach, ale może to być praktycznie
dowolny inny kontener. Np. w Standardowej Bibliotece Szablonów C++
dowolny inny kontener. Np. w Standardowej Bibliotece Szablonów C++
(stos jest zaimplementowany jako
stos jest zaimplementowany jako
adapter do któregoś z istniejących już kontenerów. Ponieważ kontenery
adapter do któregoś z istniejących już kontenerów. Ponieważ kontenery
STL są szablonami, szablon adaptera mógłby wyglądać następująco:
STL są szablonami, szablon adaptera mógłby wyglądać następująco:


template<typename T,
<Source>
         template<typename X > class Sequence <nowiki>=</nowiki> std::deque >  
template<typename T,
class Stack {
         template<typename X > class Sequence=std::deque >  
Sequence<T> _rep;
class Stack {
public:
  Sequence<T> _rep;
void push(T e) {_rep.push_back(e);};
public:
T pop() {T top <nowiki>=</nowiki> _rep.top();_rep.pop_back();return top;}
  void push(T e) {_rep.push_back(e);};
bool is_empty() const {return _rep.empty();}
  T pop() {T top=_rep.top();_rep.pop_back();return top;}
};
  bool is_empty() const {return _rep.empty();}
};
</Source>


Konkretyzując stos możemy wybrać kontener w którym będą przechowywane
Konkretyzując stos możemy wybrać kontener, w którym będą przechowywane
jego elementy:
jego elementy:


Stack<double,std::vector> sv;
<Source>
Stack<double,std::vector> sv;
</Source>


Można zamiast szablonu użyć zwykłego parametru typu:
Można zamiast szablonu użyć zwykłego parametru typu:


template<typename T,typename C > class stos {
<Source>
C rep;
template<typename T,typename C > class stos {
public:
  C rep;
...
  public:
}
  ...
}  
</Source>


([http://osilek.mimuw.edu.pl/images/3/31/Stack_adapter.cpp Źródło: Stack_adapter.cpp])
([[media:Stack_adapter.cpp | Źródło: stack_adapter.cpp]])


i używać go w następujący sposób:
i używać go w następujący sposób:


stos<double,std::vector<double> > sv;
<Source>
stos<double,std::vector<double> > sv;
</Source>


W  przypadku użycia szablonu jako parametru szablonu  
W  przypadku użycia szablonu jako parametru szablonu  
zapewniemy konsystencję pomiędzy typem <tt>T</tt> i kontenerem <tt>C</tt>,
zapewniamy konsystencję pomiędzy typem <tt>T</tt> i kontenerem <tt>C</tt>,
uniemożliwiajać pomyłkę podstawienia
uniemożliwiając pomyłkę podstawienia
niepasujących parametrów:
niepasujących parametrów:


stos<double,std::vector<int> > sv; <i>błąd: niezgodność typow</i>
<Source>
 
stos<double,std::vector<int> > sv; <i>błąd: niezgodność typow</i>
Uczciwość nakazuje jednak w tym miejscu stwierdzić że właśnie takie
</Source>
rozwiązanie jest zastosowane w STL-u. Ma ono zaletę że możemy
Uczciwość nakazuje jednak w tym miejscu stwierdzić, że właśnie takie
adaptować na stos dowolny kontener niekoniecznie będący szablonem.
rozwiązanie jest zastosowane w STL-u. Ma ono zaletę, że możemy
adaptować na stos dowolny kontener, niekoniecznie będący szablonem.


Na koniec jeszcze jedna uwaga: szablony kontenerów z STL posiadają po
Na koniec jeszcze jedna uwaga: szablony kontenerów z STL posiadają po
dwa parametry typów z tym że drugi posiada wartość domyślną (standard dopuszcza dowolną ilość argumentów w implemetacji kontenerów STL jak długo będa one posiadały wartości domyślne). Autorzy (D. Vandervoorde, N. Josuttis: <i>"C++ Szablony, Vademecum profesjonalisty"</i>) ostrzegają że w tej systuacji kompilator może nie zakceptować wyrażenia:
dwa parametry typów, z tym, że drugi posiada wartość domyślną (standard dopuszcza dowolną ilość argumentów w implemetacji kontenerów STL jak długo będą one posiadały wartości domyślne). Autorzy D. Vandervoorde, N. Josuttis <i>"C++ Szablony, Vademecum profesjonalisty"</i> ostrzegają, że w tej sytuacji kompilator może nie zaakceptować wyrażenia:
 
stos<double,std::vector> sv;


<Source>
stos<double,std::vector> sv;
</Source>
ponieważ ignoruje fakt istnienia wartości domyślnej dla
ponieważ ignoruje fakt istnienia wartości domyślnej dla
drugiego parametru szablony <tt>std::vector</tt>.  
drugiego parametru szablonu <tt>std::vector</tt>.  
Mamy wtedy  niezgodność pomiędzy przekazanym argumentem szablonu
Mamy wtedy  niezgodność pomiędzy przekazanym argumentem szablonu


template<typename T>  
<Source>
std::vector<T,typename A <nowiki>=</nowiki> std::allocator<T> >;
template<typename T>  
 
std::vector<T,typename A = std::allocator<T> >;
</Source>
oraz deklaracją paremetru <tt>Sequence</tt> jako:
oraz deklaracją paremetru <tt>Sequence</tt> jako:


template<typename X > class Sequence ;
<Source>
template<typename X > class Sequence ;
</Source>


która zakłada tylko jeden parametr szablonu.  Można wtedy zmienić
która zakłada tylko jeden parametr szablonu.  Można wtedy zmienić
deklarację szablonu stos i podać domyślny argument dla szablony w
deklarację szablonu <tt>stos</tt> i podać domyślny argument dla szablony w
liscie parametrów:
liście parametrów:


template<typename T,template<typenamszablonye X ,typename A <nowiki>=</nowiki>
<Source>
std::allocator<X> > class C > class stos {...}
template<typename T,template<typename X ,typename A =
std::allocator<X> > class C > class stos {...}
</Source>


W praktyce używane przeze mnie kompilatory (g++ wersja ><nowiki>=</nowiki> 3.3) nie
W praktyce używane przeze mnie kompilatory (g++ wersja ><nowiki>=</nowiki> 3.3) nie
wymagały takiej konstrukcji. Przyznaję że ne udało mi się doczytać czy
wymagały takiej konstrukcji. Przyznaję, że nie udało mi się doczytać czy
jest to cecha kompilatora g++ czy nowego standardu C++ (autorzy (D. Vandervoorde, N. Josuttis: <i>"C++ Szablony, Vademecum profesjonalisty"</i>) opierali się na poprzednim wydaniu standardu).
jest to cecha kompilatora g++, czy nowego standardu C++ (autorzy D. Vandervoorde, N. Josuttis <i>"C++ Szablony, Vademecum profesjonalisty"</i> opierali się na poprzednim wydaniu standardu).


===Konkretyzacja na żądanie===
===Konkretyzacja na żądanie===


Jak już wspomniałem wcześniej konkretyzacja szablonów może odbywać się
Jak już wspomniałem wcześniej, konkretyzacja szablonów może odbywać się
"na żądanie". W takim przypadku kompilator będzie konkretyzował
"na żądanie". W takim przypadku kompilator będzie konkretyzował
tylko funkcje napotkane w kodzie. I tak jeśli np. nie użyjemy w naszym
tylko funkcje napotkane w kodzie. I tak, jeśli np. nie użyjemy w naszym
kodzie funckji <tt>Stack<int>::pop()</tt> to nie zostanie ona
kodzie funckji <tt>Stack<int>::pop()</tt>, to nie zostanie ona
wygenerowana. Można z tego skorzystać i konkretyzować klasy typami
wygenerowana. Można z tego skorzystać i konkretyzować klasy typami,
które nie spełniają wszystkich ograniczeń nałożonych na parametry
które nie spełniają wszystkich ograniczeń nałożonych na parametry
szablonu. Wszystko bedzię w porządku jak długo nie będziemy używać
szablonu. Wszystko bedzię w porządku jak długo nie będziemy używać
funkcji łamiących te ograniczenia. Np. załóżmy że do szablonu
funkcji łamiących te ograniczenia. Np. załóżmy, że do szablonu
<tt>Stack</tt> dodajemy możliwość jego sortowania (wiem, to nie jest zgodne z duchem programowania obiektowego, stos nie posiada operacji sortowania, puryści zastąpić ten przykład kontenerem <tt>list</tt>):
<tt>Stack</tt> dodajemy możliwość jego sortowania (wiem, to nie jest zgodne z duchem programowania obiektowego, stos nie posiada operacji sortowania, puryści mogą zastąpić ten przykład kontenerem <tt>list</tt>):


  template<typename T,int N> void Stack<T,N>::sort() {
  template<typename T,int N> void Stack<T,N>::sort() {
Linia 726: Linia 751:
  sc.sort();
  sc.sort();


([http://osilek.mimuw.edu.pl/images/0/0f/Stack_sort.cpp Źródło: Stack_sort.cpp])
([[media:Stack_sort.cpp | Źródło: stack_sort.cpp]])


Natomiast konkretyzacja jawna
Natomiast konkretyzacja jawna
Linia 732: Linia 757:
  template Stack<std::complex<double> >;
  template Stack<std::complex<double> >;


([http://osilek.mimuw.edu.pl/images/0/0f/Stack_sort.cpp Źródło: Stack_sort.cpp])
([[media:Stack_sort.cpp | Źródło: stack_sort.cpp]])


nie powiedzie się, bo kompilator będzie się starał skonkretyzować
nie powiedzie się, bo kompilator będzie się starał skonkretyzować
wszystkie składowe klasy <tt>Stack</tt> w tym metodę <tt>sort()</tt>.
wszystkie składowe klasy <tt>Stack</tt>, w tym metodę <tt>sort()</tt>.


===Typy stowarzyszone===
===Typy stowarzyszone===
Linia 753: Linia 778:
  template<typename S> void f(S s) {
  template<typename S> void f(S s) {
   typename S::value_type total;  
   typename S::value_type total;  
     <i>słowo <tt>typename</tt> jest wymagane, inaczej kompilator założy że  
     <i>słowo <tt>typename</tt> jest wymagane, inaczej kompilator założy, że  
     <tt>S::valuetype</tt> odnosi się do statycznej składowej klasy</i>
     <tt>S::value_type</tt> odnosi się do statycznej składowej klasy</i>
   while(!s.is_empty() ) {
   while(!s.is_empty() ) {
     total+<nowiki>=</nowiki>s.pop();
     total+<nowiki>=</nowiki>s.pop();
Linia 761: Linia 786:
  }
  }


([http://osilek.mimuw.edu.pl/images/c/cb/Stack_N.cpp Źródło: Stack_N.cpp])
([[media:Stack_N.cpp | Źródło: stack_N.cpp]])


Bez takich możliwości musielibyśmy przekazać typ elementów stosu w
Bez takich możliwości musielibyśmy przekazać typ elementów stosu w
osobnym argumencie. Mechanizm typów stowarzyszonych jest  
osobnym argumencie. Mechanizm typów stowarzyszonych jest  
bardzo czesto używany w uogólnionym kodzie.
bardzo czesto używany w uogólnionym kodzie.
==Podsumowanie==

Aktualna wersja na dzień 12:10, 19 paź 2021


Szablony funkcji

Funkcje uogólnione

W praktyce programowania często spotykamy się z funkcjami (algorytmami), które można zastosować do szerokiej klasy typów i struktur danych. Typowym przykładem jest funkcja obliczająca maksimum dwu wartości. Ten trywialny, aczkolwiek przydatny kod można zapisać np. w postaci:

int max(int a,int b) {
  return (a>b)?a:b;
}; 

Przykład 1.1

Funkcja max wybiera większy z dwu int-ów, ale widać, że kod będzie identyczny dla argumentów dowolnego innego typu pod warunkiem, iż istnieje dla niego operator porównania i konstruktor kopiujący. W językach programowania z silną kontrolą typów, takich jak C, C++ czy Java definiując funkcję musimy jednak podać typy przekazywanych parametrów oraz typ wartości zwracanej. Oznacza to, że dla każdego typu argumentów musimy definiować nową funkcję max:

int    max(int a, int b)      {return (a>b)?a:b;};
double max(double a,double b) {return (a>b)?a:b;};
string max(string a,string b) {return (a>b)?a:b;};
skorzystaliśmy tu z dostępnej w C++ możliwości przeładowywania funkcji
main() { cout<< max(7,5)<<end; cout<< max(3.1415,2.71)<<endl; cout<< max("Ania","Basia")<<endl; }

Przykład 1.2

Takie powtarzanie kodu, poza oczywistym zwiększeniem nakładu pracy, ma inne niepożądane efekty, związane z trudnością zapewnienia synchronizacji kodu każdej z funkcji. Jeśli np. zauważymy błąd w kodzie, to musimy go poprawić w kilku miejscach. To samo dotyczy optymalizacji kodu. W powyższym przykładzie kod jest wyjątkowo prosty, ale taki sam problem dotyczy np. funkcji sortujących. Rozważmy prosty algorytm sortowania bąbelkowego:

inline void swap(double &a,double &b) {
double   tmp=a;a=b;b=tmp;
}
void buble_sort(double *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]);
}

Powyższa funkcja sortuje tablicę zawierającą wartości typu double, ale widać, że znów kod będzie identyczny, jeśli zamiast double użyjemy dowolnego innego typu, którego wartości możemy porównywać za pomocą funkcji operator>() i dla którego zdefiniowany jest operator przypisania. Co więcej, kod nie zmieni się jeśli zamiast tablicy użyjemy dowolnej innej struktury danych, umożliwiającej indeksowany dostęp do swoich składowych, np. std::vector ze Standardowej Biblioteki Szablonów STL. W tym przypadku kod jest już bardziej skomplikowany i kłopoty związane z jego powielaniem będą większe. Przykłady takie można mnożyć, istnieje bowiem wiele takich funkcji czy algorytmów uogólnionych. Ich kod może być znacznie bardziej skomplikowany niż w podanych przykładach, a zależność od typu argumentów nie musi ograniczać się do sygnatury, ale występować również we wnętrzu funkcji, jak np. w przypadku zmiennej tmp w funkcji swap. Powielanie takiego kodu dla różnych typów parametrów może łatwo prowadzić do błędów, utrudnia ich wykrywanie, a konieczność edycji każdego egzemplarza kodu zniechęca do wprowadzania ulepszeń.

Funkcje uogólnione bez szablonów

Jak radzili, a właściwie jak radzą sobie programiści bez możliwości skorzystania z szablonów? Tradycyjne sposoby rozwiązywania tego typu problemów to między innymi makra:

#define max(a,b) ( (a>b)?a:b) )

lub używanie wskaźników typów ogólnych, takich jak void *, jak np. w funkcji qsort ze standardowej biblioteki C:

void qsort(void *base, size_t nmemb, size_t size,
           int(*compar)(const void *, const void *));

Mimo iż użyteczne, żadne z tych rozwiązań nie może zostać uznane za wystarczająco ogólne i bezpieczne.

Można się również pokusić o próbę rozwiązania tego problemu za pomocą mechanizmów programowania obiektowego. W sumie jest to bardziej wyrafinowana odmiana rzutowania na void *. Polega na zdefiniowaniu ogólnego typu dla obiektów, które mogą być porównywane:

class GreaterThanComparable { 
public:
  virtual bool operator>(const GreaterThanComparable &) const = 0; 
};

następnie zdefiniowaniu funkcji max w postaci:

const GreaterThanComparable &
max(const GreaterThanComparable &a, 
    const GreaterThanComparable &b) { 
      return (a>b)? a:b; 
    }

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

i używaniu jej np. w następujący sposób:

class Int:public GreaterThanComparable { 
  int val;
public: Int(int i <nowiki>=</nowiki> 0):val(i) {}; 
  operator int() {return val;};
  virtual bool 
  operator>(const GreaterThanComparable &b) const {
    return val > static_cast<const Int&>(b).val;
  } 
};

main() {
  Int a(1),b(2);
  Int c;
  c = (const Int &)::max(a,b);
  cout<<(int)c<<endl;
}

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

Widać więc wyraźnie, że to podejście wymaga sporego nakładu pracy, a więc w szczególności w przypadku tak prostej funkcji jak max jest wysoce niepraktyczne. Ogólnie rzecz biorąc ma ono następujące wady:

  1. Wymaga dziedziczenia z abstrakcyjnej klasy bazowej GreaterThanComparable, czyli może być zastosowane tylko do typów zdefiniowanych przez nas. Inne typy, w tym typy wbudowane, wymagają kopertowania w klasie opakowującej, takiej jak klasa Int w powyższym przykładzie.
  2. Ponieważ potrzebujemy polimorfizmu funkcja operator>() musi być funkcją wirtualną, a więc musi być funkcją składową klasy i nie może być typu inline. W przypadku tak prostych funkcji niemożność rozwinięcia ich w miejscu wywołania może prowadzić do dużych narzutów w czasie wykonania.
  3. Funkcja max zwraca zawsze referencje do GreaterThanComparable, więc konieczne jest rzutowanie na typ wynikowy (tu Int).

Szablony funkcji

Widać, że podejście obiektowe nie nadaje się najlepiej do rozwiązywania tego szczególnego problemu powielania kodu. Dlatego w C++ wprowadzono nowy mechanizm: szablony. Szablony zezwalają na definiowanie całych rodzin funkcji, które następnie mogą być używane dla różnych typów argumentów.

Definicja szablonu funkcji max, odpowiadającej definicji 1.1 wygląda następująco:

template<typename T> T max(T a,T b) {return (a>b)?a:b;};

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

Przyjrzyjmy się jej z bliska. Wyrażenie template<typename T> oznacza, że mamy do czynienia z szablonem, który posiada jeden parametr formalny nazwany T. Słowo kluczowe typename oznacza, że parametr ten jest typem (nazwą typu). Zamiast słowa typename możmy użyć słowa kluczowego class. Nazwa tego parametru może być następnie wykorzystywana w definicji funkcji w miejscach, gdzie spodziewamy się nazwy typu. I tak powyższe wyrażenie definiuje funkcję max, która przyjmuje dwa argumenty typu T i zwraca wartość typu T, będącą wartością większego z dwu argumentów. Typ T jest na razie niewyspecyfikowany. W tym sensie szablon definiuje całą rodzinę funkcji. Konkretną funkcję z tej rodziny wybieramy poprzez podstawienie za formalny parametr T konkretnego typu będącego argumentem szablonu. Takie podstawienie nazywamy konkretyzacją szablonu. Argument szablonu umieszczamy w nawiasach ostrych za nazwą szablonu (w praktyce można uniknąć konieczności jawnej specyfikacji argumentów szablonu, opiszemy to w następnych częściach wykładu):

int i,j,k;
k=max<int>(i,j);

Takie użycie szablonu spowoduje wygenerowanie identycznej funkcji jak 1.1. W powyższym przypadku za T podstawiamy int. Oczywiście możemy podstawić za T dowolny typ i używając szablonów program 1.2 można zapisać następująco:

template<typename T> T max(T a,T b) {return (a>b)?a:b;}
main() {
  cout<<::max<int>(7,5)<<endl;
  cout<<::max<double>(3.1415,2.71)<<endl;
  cout<<::max<string>("Ania","Basia")<<endl;
}

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

W powyższym kodzie użyliśmy konstrukcji ::max(a,b). Dwa dwukropki oznaczają, że używamy funkcji max zdefiniowanej w ogólnej przestrzeni nazw. Jest to konieczne aby kod się skompilował, ponieważ szablon max istnieje już w standardowej przestrzeni nazw std. W dalszej części wykładu będziemy te podwójne dwukropki pomijać.

Oczywiście istnieją typy których podstawienie spowoduje błędy kompilacji, np.

complex<double> c1,c2;
max<complex<double> >(c1,c2); //brak operatora >

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

lub

class X {
private:
  X(const X &){};
};
X a,b;
max<X>(a,b); //prywatny (niewidoczny) konstruktor kopiujący

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

Ogólnie rzecz biorąc, każdy szablon definiuje pewną klasę typów, które mogą zostać podstawione jako jego argumenty.

Dedukcja argumentów szablonu

Użyteczność szablonów funkcji zwiększa istotnie fakt, że argumenty szablonu nie muszą być podawane jawnie. Kompilator może je wydedukować z argumentów funkcji. Tak więc zamiast

int i,j,k;
k=max<int>(i,j);

możemy napisać

int i,j,k;
k=max(i,j);

i kompilator zauważy, że tylko podstawienie int-a za T umożliwi dopasowanie sygnatury funkcji do parametrów jej wywołania i automatycznie dokona odpowiedniej konkretyzacji.

Może się zdarzyć, że podamy takie argumenty funkcji, że dopasowanie argumentów wzorca będzie niemożliwe, otrzymamy wtedy błąd kompilacji. Trzeba pamiętać, że mechanizm automatycznego dopasowywania argumentów szablonu powoduje wyłączenie automatycznej konwersji argumentów funkcji. Podanie jawnie argumentów szablonu (w nawiasach ostrych za nazwą szablonu) jednoznacznie określa sygnaturę funkcji, a więc umożliwia automatyczną konwersję typów. Ilustruje to poniższy kod:

template<typename T> T max(T a,T b) {return (a>b)?a:b;}
main() {
  cout<<::max(3.14,2)<<endl;
  // błąd: kompilator nie jest w stanie wydedukowac argumentu szablonu, bo typy 
  // argumentów (double,int) nie pasują  do (T,T)

  cout<<::max<int>(3.14,2)<<endl;
  // podając argument jawnie wymuszamy sygnaturę int max(int,int), a co za tym 
  // idzie automatyczną konwersję argumentu 1 do int-a

  cout<<::max<double>(3.14,2)<<endl;
  // podając argument szablonu jawnie wymuszamy sygnaturę 
  // double max(double,double)
  // a co za tym idzie automatyczną konwersję argumentu 2 do double-a
 
  int i;
  cout<<::max<int *>(&i,i)<<endl; 
  //błąd: nie istnieje konwersja z typu int na int*

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

Może warto zauważyć, że automatyczna dedukcja parametrów szablonu jest możliwa tylko wtedy, jeśli parametry wywołania funkcji w jakiś sposób zależą od parametrów szablonu. Jeśli tej zależności nie ma, z przyczyn oczywistych dedukcja nie jest możliwa i trzeba parametry podawać jawnie. Wtedy istotna jest kolejność parametrów na liście. Jeżeli parametry, których nie da się wydedukować, umieścimy jako pierwsze, wystarczy, że tylko je podamy jawnie, a kompilator wydedukuje resztę. Ilustruje to poniższy kod:

template<typename T,typename U> T convert(U u) {
return (T)u;
};
template<typename U,typename T> T inv_convert(U u) {
return (T)u;
};
fukcje różnią się tylko kolejnością parametrów szablonu
main() { cout<<convert(33)<<endl; błąd: kompilator nie jest w stanie wydedukować pierwszego parametru szablonu, bo nie zależy on od parametru wywołania funkcji
cout<<convert<char>(33)<<endl; w porządku: podajemy jawnie argument T, kompilator sam dedukuje argument U z typu argumentu wywołania funkcji
cout<<inv_convert<char>('a')<<endl; błąd: podajemy jawnie argument odpowiadający parametrowi U. Kompilator nie jest w stanie wydedukować argumentu T, bo nie zależy on od argumentu wywołania funkcji
cout<<inv_convert<int,char>(33)<<endl; w porządku: podajemy jawnie oba argumenty szablonu }

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

Używanie szablonów

Z użyciem szablonów wiąże się parę zagadnień niewidocznych w prostych przykładach. W językach C i C++ zwykle rozdzielamy deklarację funkcji od jej definicji i zwyczajowo umieszczamy deklarację w plikach nagłówkowych *.h, a definicję w plikach źródłowych *.c, *.cpp itp. Pliki nagłówkowe są w czasie kompilacji włączane do plików, w których chcemy korzystać z danej funkcji, a pliki źródłowe są pojedynczo kompilowane do plików “obiektowych” *.o. Następnie pliki obiektowe są łączone w jeden plik wynikowy (zob. rysunek 1.1). W pliku korzystającym z danej funkcji nie musimy więc znać jej definicji, a tylko deklarację. Na podstawie nazwy funkcji konsolidator powiąże wywołanie funkcji z jej implementacją znajdującą się w innym pliku obiektowym. W ten sposób tylko zmiana deklaracji funkcji wymaga rekompilacji plików, w których z niej korzystamy, a zmiana definicji wymaga jedynie rekompilacji pliku, w którym dana funkcja jest zdefiniowana.

<div.thumbcaption>Rysunek 1.1. Przykład organizacji kodu C++ w przypadku

użycia zwykłych funkcji.

Taka organizacja umożliwia przestrzeganie "reguły jednej definicji" (one definition rule), wymaganej przez C++. Osobom nieobeznanym z programowaniem w C/C++ zwracam uwagę na konstrukcję

#ifndef _nazwa_pliku_
#define _nazwa_pliku_
...
#endif

uniemożliwiajacą podwójne włączenie tego pliku do jednej jednostki translacyjnej.

Podobne podejście do kompilacje szablonów się nie powiedzie (zob. rysunek 1.2). Powodem jest fakt, że w trakcie kompilacji pliku utils.cpp kompilator nie wie jeszcze, że potrzebna będzie funkcja max<int>, wobec czego nie generuje kodu żadnej funkcji, a jedynie sprawdza poprawność gramatyczną szablonu. Z kolei podczas kompilacji pliku main.cpp kompilator już wie, że ma skonkretyzować szablon dla T = int, ale nie ma dostępu do kodu szablonu.

Rysunek 1.2. Przykład błędnej organizacji kodu w przypadku użycia szablonów

Istnieją różne rozwiązania tego problemu. Najprościej chyba jest zauważyć, że opisane zachowanie jest analogiczne do zachowania podczas kompilacji funkcji rozwijanych w miejscu wywołania (inline), których definicja również musi być dostępna w czasie kompilacji. Podobnie więc jak w tym przypadku możemy zamieścić wszystkie deklaracje i definicje szablonów w pliknu nagłówkowym, włączanym do plików, w ktorych z tych szablonów korzystamy (zob. rysunek 1.3). Podobnie jak w przypadku funkcji inline reguła jednej definicji zezwala na powtarzanie definicji/deklaracji szablonów w różnych jednostkach translacyjnych, pod warunkiem, że są one identyczne. Stąd konieczność umieszczania ich w plikach nagłówkowych.

Rysunek 1.3. Przykład organizacji kodu z szablonami, wykorzystującego strategię włączania.

Ten sposób organizacji pracy z szablonami, nazywany modelem włączenia, jest najbardziej uniwersalny. Jego główną wadą jestkonieczność rekompilacji całego kodu korzystającego z szablonów przy każdej zmianie definicji szablonu. Również jeśli zmienimy coś w pliku, w którym korzystamy z szablonu, to musimy rekompilować cały kod szablonu włączony do tego pliku, nawet jeśli nie uległ on zmianie. Jeśli się uwzględni fakt, że kompilacja szablonu jest bardziej skomplikowana od kompilacji "zwykłego" kodu, to duży projekt intensywnie korzystający z szablonów może wymagać bardzo długich czasów kompilacji.

Możemy też w jakiś sposób dać znać kompilatorowi, że podczas kompilacji pliku utils.cpp powinien wygenerować kod dla funkcji max<int>. Można to zrobić dodając jawne żądanie konkretyzacji szablonu (zob. rysunek 1.4):

template<typename T> T max(T a,T b) {return (a>b)?a:b;}
template int max<int>(int ,int) ; konkretyzacja jawna

Używając konkretyzacji jawnej musimy pamiętać o dokonaniu konkretyzacji każdej używanej funkcji, tak że to podejście nie skaluje się zbyt dobrze. Ponadto w przypadku szablonów klas (omawianych w następnym module) konkretyzacja jawna pociąga za sobą konkretyzację wszystkich metod danej klasy, a konkretyzacja “na żądanie” - jedynie tych używanych w programie.

<div.thumbcaption>Rysunek 1.4. Przykład organizacji kodu z szablonami,

wykorzystującego jawną konkretyzację.

Pozatypowe parametry szablonów

Poza parametrami określającymi typ, takimi jak parametr T w dotychczasowych przykładach, szablony funkcji mogą przyjmować również parametry innego rodzaju. Obecnie mogą to być inne szablony, co omówię w następnym podrozdziale lub parametry określające nie typ, ale wartości. Jak na razie (w obecnym standardzie) te wartości nie mogą być dowolne, ale muszą mieć jeden z poniższych typów:

  1. typ całkowitoliczbowy bądź typ wyliczeniowy
  2. typ wskaźnikowy
  3. typ referencyjny.

Takie parametry określające wartość nazywamy parametrami pozatypowymi. W praktyce z parametrów pozatypowych najczęściej używa się parametrów typu całkowitoliczbowego. Np.

template<size_t N,typename T> T dot_product(T *a,T *b) {
        T total=0.0;
        for(size_t i=0;i<N;++i)
                total += a[i]*b[i] ;
return total; };

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

Po raz drugi zwracam uwagę na kolejność parametrów szablonu na liście parametrów. Dzięki temu, że niededukowalny parametr N jest na pierwszym miejscu wystarczy podać jawnie tylko jego, drugi parametr typu T zostanie sam automatycznie wydedukowany na podstawie przekazanych argumentów wywołania funkcji:

main() {
double x[3],y[3];
dot_product<3>(x,y);
}

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

Parametry pozatypowe są zresztą "ciężko dedukowalne". Właściwie jedynym sposobem na przekazania wartości stałej poprzez typ argumentu wywołania jest skorzystanie z parametrów będących szablonami klas (zob. następny podrozdział).

Używając pozatypowych parametrów szablonów musimy pamiętać, że odpowiadające im argumenty muszą być stałymi wyrażeniami czasu kompilacji. Stąd jeżeli używamy typów wskaźnikowych, muszą to być wskaźniki do obiektów łączonych zewnętrznie, a nie lokalnych. Ponieważ jednak jeszcze ani razu nie używałem pozatypowych parametrów szablonów innych niż typy całkowite, to nie będę podawał żadnych przykładów takich paremtrów na tym wykładzie.

Szablony parametrów szablonu

Jak już wspomniałem w poprzednim podrozdziale, parametrami szablonu funkcji mogą być również szablony klas (zob. następny podrozdział). Szablony parametrów szablonu umożliwiają przekazanie nazwy szablonu jako argumentu szablonu funkcji. Więcej o nich napiszę w drugiej części wykładu. Tutaj tylko pokażę jako ciekawostkę w jaki sposób można dedukować wartości pozatypowych argumentów szablonu.

template< template<int N> class  C,int K>
taka definicja oznacza, że parametr C określa szablon klasy 
posiadający jeden parametr typu int. Parametr N służy tylko 
do definicji szablonu C i nie może być użyty nigdzie indziej
void f(C<K>){
  cout<<K<<endl;
};
template<int N> struct SomeClass {};
main() { SomeClass<1> c1; SomeClass<2> c2;
f(c1); C=SomeClass K=1 f(c2); C=SomeClass K=2 }

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

Szablony metod

Jak na razie definiowaliśmy szablony zwykłych funkcji. C++ umożliwia również definiowanie szablonów metod klasy np.:

struct Max {
  template<typename T> T max(T a,T b) {return (a>b)?a:b;}
};
main() {
  Max m;
  m.max(1,2);
}

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

Szablonów metod składowych dotyczą takie same reguły jak szablonów funkcji.

Szablony klas

Typy uogólnione

Uwagi na początku poprzedniego rozdziału odnoszą się w tej samej mierze do klas, jak i do funkcji. I tutaj mamy do czynienia z kodem, który w niezmienionej postaci musimy powielać dla różnych typów. Sztandarowym przykładem takiego kodu są różnego rodzaju kontenery (pojemniki), czyli obiekty służące do przechowywania innych obiektów. Jest oczywiste, że kod kontenera jest w dużej mierze niezależny od typu obiektów w nim przechowywanych. Jako przykład weźmy sobie stos liczb całkowitych. Możliwa definicja klasy stos może wyglądać następująco, choć nie polecam jej jako wzoru do naśladowania w prawdziwych aplikacjach:

class Stack {
private:
  int rep[N];
  size_t top;
public:
  static const size_t N=100;
  Stack():_top(0) {};
  void push(int val) {_rep[_top++]=val;}
  int pop() {return rep[--top];}
  bool is_empty {return (top==0);}
}

Ewidentnie ten kod będzie identyczny dla stosu obiektów dowolnego innego typu, pod warunkiem, że typ ten posiada zdefiniowany operator=() i konstruktor kopiujący.

W celu zaimplementowania kontenerów bez pomocy szablonów możemy probować podobnych sztuczek jak te opisane w poprzednim rozdziale. W językach takich jak Java czy Smalltalk, które posiadają uniwersalną klasę Object, z której są dziedziczone wszystkie inne klasy, a nie posiadają (Java już posiada) szablonów, uniwersalne kontenery są implementowane właśnie poprzez rzutowanie na ten ogólny typ. W przypadku C++ nawet to rozwiązanie nie jest praktyczne, bo C++ nie posiada pojedynczej hierarchii klas.

Szablony klas

Rozwiązaniem są znów szablony, tym razem szablony klas. Podobnie jak w przypadku szablonów funkcji, szablon klasy definiuje nam w rzeczywistości całą rodzinę klas. Szablon klasy Stack możemy zapisać następująco:

template<typename T> class Stack {
public:
  static const size_t N=100;
private:
  T _rep[N];
  size_t _top;<br>
public:
  Stack():_top(0) {};
  void push(T val) {_rep[_top++]=val;}
  T pop() {return _rep[--_top];}
  bool is_empty {return (_top==0);} 
 };

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

Tak zdefiniowanego szablonu możemy używać podając jawnie jego argumenty.

Stack<string> st ;
st.push("ania");
st.push("asia");
st.push("basia");
while(!st.is_empty() ){
  cout<<st.pop()<<endl;
}

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

Dla szablonów klas nie ma możliwości automatycznej dedukcji argumentów szablonu, ponieważ klasy nie posiadają argumentów wywołania, które mogłyby do tej dedukcji posłużyć. Jest natomiast możliwość podania argumentów domyślnych, np.

template<typename T = int> Stack {
  ...
}

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

Wtedy możemy korzystać ze stosu bez podawania argumentów szablonu i wyrażenie

Stack s;

będzie równoważne wyrażeniu:

Stack<int> s;

Dla domyślnych argmentów szablonów klas obowiązują te same reguły, co dla domyślnych argumentów wywołania funkcji.

Należy pamiętać, że każda konkretyzacja szablonu klasy dla różniących się zestawów argumentów jest osobną klasą:

Stack<int> si;
Stack<double> sd;
sd=si; //błąd: to są obiekty różnych klas a nie zdefiniowano przypisania

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

Okazuje się, że próba zdefiniowania operatora przypisania, który np. przypisywałby do siebie stosy różnych typów, nie jest łatwa, ponieważ dwa takie stosy nie widzą swoich reprezentacji.

Pozatypowe parametry szablonów klas

Zestaw możliwych parametrów szablonów klas jest taki sam jak dla szablonów funkcji. Podobnie najczęściej wykorzystywane są wyrażenia całkowitoliczbowe. W naszym przykładzie ze stosem możemy ich użyć do przekazania rozmiaru stosu:

template<typename T = int , size_t N = 100> class Stack {
private:	
 T rep[N];
 size_t top;
public:
 Stack():_top(0) {};
void push(T val) {_rep[_top++]=val;} T pop() {return rep[--top];} bool is_empty {return (top==0);} }

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

Podkreślam jeszcze raz, że Stack<int,100> i Stack<int,101> to dwie różne klasy.

Szablony parametrów szablonu

Stos jest nie tyle strukturą danych, ile sposobem dostępu do nich. Stos realizuje regułę LIFO czyli Last In First Out. W tym sensie nie jest istotne w jaki sposób dane są na stosie przechowywane. Może to być tablica, jak w powyższych przykładach, ale może to być praktycznie dowolny inny kontener. Np. w Standardowej Bibliotece Szablonów C++ stos jest zaimplementowany jako adapter do któregoś z istniejących już kontenerów. Ponieważ kontenery STL są szablonami, szablon adaptera mógłby wyglądać następująco:

template<typename T,
         template<typename X > class Sequence=std::deque > 
class Stack {
  Sequence<T> _rep;
public:
  void push(T e) {_rep.push_back(e);};
  T pop() {T top=_rep.top();_rep.pop_back();return top;}
  bool is_empty() const {return _rep.empty();}
};

Konkretyzując stos możemy wybrać kontener, w którym będą przechowywane jego elementy:

Stack<double,std::vector> sv;

Można zamiast szablonu użyć zwykłego parametru typu:

template<typename T,typename C > class stos {
  C rep;
  public:
   ...
}

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

i używać go w następujący sposób:

stos<double,std::vector<double> > sv;

W przypadku użycia szablonu jako parametru szablonu zapewniamy konsystencję pomiędzy typem T i kontenerem C, uniemożliwiając pomyłkę podstawienia niepasujących parametrów:

stos<double,std::vector<int> > sv; <i>błąd: niezgodność typow</i>

Uczciwość nakazuje jednak w tym miejscu stwierdzić, że właśnie takie rozwiązanie jest zastosowane w STL-u. Ma ono tę zaletę, że możemy adaptować na stos dowolny kontener, niekoniecznie będący szablonem.

Na koniec jeszcze jedna uwaga: szablony kontenerów z STL posiadają po dwa parametry typów, z tym, że drugi posiada wartość domyślną (standard dopuszcza dowolną ilość argumentów w implemetacji kontenerów STL jak długo będą one posiadały wartości domyślne). Autorzy D. Vandervoorde, N. Josuttis "C++ Szablony, Vademecum profesjonalisty" ostrzegają, że w tej sytuacji kompilator może nie zaakceptować wyrażenia:

stos<double,std::vector> sv;

ponieważ ignoruje fakt istnienia wartości domyślnej dla drugiego parametru szablonu std::vector. Mamy wtedy niezgodność pomiędzy przekazanym argumentem szablonu

template<typename T> 
std::vector<T,typename A = std::allocator<T> >;

oraz deklaracją paremetru Sequence jako:

template<typename X > class Sequence ;

która zakłada tylko jeden parametr szablonu. Można wtedy zmienić deklarację szablonu stos i podać domyślny argument dla szablony w liście parametrów:

template<typename T,template<typename X ,typename A =
std::allocator<X> > class C > class stos {...}

W praktyce używane przeze mnie kompilatory (g++ wersja >= 3.3) nie wymagały takiej konstrukcji. Przyznaję, że nie udało mi się doczytać czy jest to cecha kompilatora g++, czy nowego standardu C++ (autorzy D. Vandervoorde, N. Josuttis "C++ Szablony, Vademecum profesjonalisty" opierali się na poprzednim wydaniu standardu).

Konkretyzacja na żądanie

Jak już wspomniałem wcześniej, konkretyzacja szablonów może odbywać się "na żądanie". W takim przypadku kompilator będzie konkretyzował tylko funkcje napotkane w kodzie. I tak, jeśli np. nie użyjemy w naszym kodzie funckji Stack<int>::pop(), to nie zostanie ona wygenerowana. Można z tego skorzystać i konkretyzować klasy typami, które nie spełniają wszystkich ograniczeń nałożonych na parametry szablonu. Wszystko bedzię w porządku jak długo nie będziemy używać funkcji łamiących te ograniczenia. Np. załóżmy, że do szablonu Stack dodajemy możliwość jego sortowania (wiem, to nie jest zgodne z duchem programowania obiektowego, stos nie posiada operacji sortowania, puryści mogą zastąpić ten przykład kontenerem list):

template<typename T,int N> void Stack<T,N>::sort() {
bubble_sort(_rep,N);
};

Możemy teraz np. używać

Stack<std::complex<double> > sc;
sc.push( std::complex<double>(0,1));
sc.pop();

ale nie

sc.sort();

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

Natomiast konkretyzacja jawna

template Stack<std::complex<double> >;

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

nie powiedzie się, bo kompilator będzie się starał skonkretyzować wszystkie składowe klasy Stack, w tym metodę sort().

Typy stowarzyszone

W klasach poza metodami i polami możemy definiować również typy, które będziemy nazywali stowarzyszonymi z daną klasą. Jest to szczególnie przydatne w przypadku szablonów. Rozważmy następujący przykład:

template<typename T> Stack {
public:
typedef T value_type;
...
}

Możemy teraz używać tej definicji w innych szablonach

template<typename S> void f(S s) {
  typename S::value_type total; 
    słowo typename jest wymagane, inaczej kompilator założy, że 
    S::value_type odnosi się do statycznej składowej klasy
  while(!s.is_empty() ) {
    total+=s.pop();
  }
return total;
}

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

Bez takich możliwości musielibyśmy przekazać typ elementów stosu w osobnym argumencie. Mechanizm typów stowarzyszonych jest bardzo czesto używany w uogólnionym kodzie.