1. Algorytmy uogólnione

1.2. 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 wygląda następująco:


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

Przyjrzyjmy się bliżej temu zagadnieniu. Wyrażenie template < typename T > wskazuje na obecność szablonu, który posiada jeden parametr formalny nazwany T. Użycie słowa kluczowego `typename` oznacza, że ten parametr reprezentuje typ (nazwę typu). Alternatywnie, można użyć słowa kluczowego `class` zamiast `typename`. Nazwa tego parametru może być później wykorzystywana w definicji funkcji w miejscach, gdzie oczekiwane jest podanie nazwy typu.

Przedstawione wyrażenie definiuje funkcję `max`, która przyjmuje dwa argumenty typu T i zwraca wartość typu T, będącą większą z dwóch podanych wartości. Typ T jest obecnie nieokreślony, co oznacza, że ten szablon definiuje całą rodzinę funkcji. Wybieramy konkretną funkcję z tej rodziny, podstawiając konkretne typy jako argumenty szablonu. Ten proces nazywamy konkretyzacją szablonu. Argumenty szablonu umieszczamy w nawiasach ostrych za nazwą szablonu (w praktyce można uniknąć konieczności jawnej specyfikacji argumentów szablonu, opiszę to później).


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

Takie użycie szablonu spowoduje wygenerowanie identycznej funkcji jak pisana wcześniej. W powyższym przypadku za T podstawiamy int. Oczywiście możemy podstawić za T dowolny typ i używając szablonów program 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;
}

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ędę 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 >
class X {
private:
  X(const X &){};
};
X a,b;
max<X>(a,b); //prywatny (niewidoczny) konstruktor kopiujący

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 kodu jawnie specyfikującego typ dla funkcji max, można 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*
}

Warto zaznaczyć, że automatyczna dedukcja parametrów szablonu jest możliwa tylko w przypadku, gdy parametry wywołania funkcji w pewien sposób zależą od parametrów szablonu. Jeżeli brak tej zależności, dedukcja nie jest możliwa z oczywistych powodów, co zmusza do jawnego podawania parametrów. W takim przypadku istotna jest również kolejność parametrów na liście. Umieszczenie parametrów, które nie mogą zostać wydedukowane, jako pierwszych, pozwala jedynie na ich jawną specyfikację, pozostawiając resztę do automatycznej dedukcji przez kompilator. Poniższy kod ilustruje tę koncepcję:


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
}

Korzystanie z 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. Zobaczcie na poniższą ilustrację, pochodzącą z http://wazniak.mimuw.edu.pl

organizacja umożliwia przestrzeganie "reguły jednej definicji" (one definition rule), wymaganej przez C++. To po to w nagłówkach pojawia się fragment uniemożliwiający podwójne włączenie tego pliku do jednej jednostki translacyjnej:


#ifndef _nazwa_pliku_
#define _nazwa_pliku_
...
#endif

W nowszych kompilatorach, można wykorzystać prostszą i krótszą dyrektywę once


#pragma once
...

Podobne podejście do kompilacje szablonów się nie powiedzie. 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.

http://wazniak.mimuw.edu.pl

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 pliku nagłówkowym, włączanym do plików, w których z tych szablonów korzystamy. 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.

http://wazniak.mimuw.edu.pl

Ten sposób organizacji pracy z szablonami, nazywany modelem włączenia, jest najbardziej uniwersalny. Jego główną wadą jest konieczność 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.


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 rozdziale) konkretyzacja jawna pociąga za sobą konkretyzację wszystkich metod danej klasy, a konkretyzacja “na żądanie” - jedynie tych używanych w programie.

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;
};

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

Podkreślam ponownie znaczenie kolejności parametrów szablonu na liście. Poprzez umieszczenie niededukowalnego parametru N na pierwszym miejscu, wystarczy jedynie jawnie podać wartość tego parametru, a drugi parametr typu T zostanie automatycznie wydedukowany na podstawie przekazanych argumentów wywołania funkcji.

Pozatypowe parametry są zazwyczaj trudniejsze do automatycznej dedukcji. W rzeczywistości, jedynym sposobem przekazania wartości stałej poprzez argument typu jest skorzystanie z parametrów będących szablonami klas.

W przypadku używania pozatypowych parametrów szablonów, istotne jest pamiętanie, że odpowiadające im argumenty muszą być stałymi wyrażeniami czasu kompilacji. Dlatego, jeśli korzystamy z typów wskaźnikowych, muszą to być wskaźniki do obiektów łączonych zewnętrznie, a nie lokalnych. Warto jednak zauważyć, że autor nie przedstawił jeszcze żadnych przykładów użycia pozatypowych parametrów szablonów, poza typami całkowitymi, na tym etapie wykładu.

Szablony parametrów szablonu

Parametrami szablonu funkcji mogą być również szablony klas (za chwilę o nich poczytacie). Szablony parametrów szablonu, znane również jako "szablony szablonów" (template template parameters), pozwalają przekazywać same szablony jako argumenty do innych szablonów funkcji. Oznacza to, że zamiast przekazywać konkretny typ do szablonu funkcji, możemy przekazać inny szablon jako argument. Więcej o nich napiszę podczas omawiania szablonów klas. 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
}