Przejdź do zawartości

C++/Szablony klas

Z Wikibooks, biblioteki wolnych podręczników.
< C++

Czym są?

[edytuj]

Szablony (wzorce) są czymś podobnym do makr, tyle że wykonywane są przez kompilator, a nie przez preprocesor. Cechują je pewne własności, których jednak nie ma preprocesor, np. można tworzyć rekurencyjne wywołania. Ale zobaczmy najpierw taką sytuację: chcemy utworzyć klasę o nazwie Punkt o trzech współrzędnych: x, y, z. Potrzebujemy trzy różne implementacje. Jedna ma działać na liczbach typu unsigned int, druga na liczbach typu int, a trzecia na float. Pierwsze, o czym pomyślimy, to napisać coś takiego (jeśli dostrzeżesz błąd, to się nie przejmuj). Mamy przykładową klasę:

 // ...
 class PunktUInt
 {
    public:
       PunktUInt( unsigned argX, unsigned argY, unsigned argZ )
       : x(argX), y(argY), z(argX)
       { }
 
       unsigned x, y, z;
 };
 // ...

I potem co? kopiujemy i zmieniamy unsigned na int:

 // ...
 class PunktInt
 {
    public:
       PunktInt( int argX, int argY, int argZ )
       : x(argX), y(argY), z(argZ)
       { }
 
       int x, y, z;
 };
 // ...

Następnie zamieniamy na float:

 // ...
 class PunktFloat
 {
    public:
       PunktFloat( float argX, float argY, float argZ )
       : x(argX), y(argY), z(argX)
       { }
 
       float x, y, z;
 };
 // ...

Uff! Wreszcie napisaliśmy - pomyślisz sobie. Jednak pisanie wymaga trochę wysiłku. No dobrze, teraz tworzymy funkcję main:

 int main(void)
 {
    PunktInt A(0,-10,0);
    PunktUInt B(0,10,5);
 
    std::cout << "A(" << A.x << "," << A.y << "," << A.z << ")" << std::endl;
    std::cout << "B(" << B.x << "," << B.y << "," << B.z << ")" << std::endl;
 }

I oto po naszych ciężkich staraniach otrzymujemy dość ciekawy i niespodziewany wynik:

A(0,-10,0)
B(0,10,0)

Ojej! - zapewne krzykniesz. Musiał gdzieś się tu zjawić błąd. Trzeba znaleźć go. Aha, mamy go:

 // ...
       PunktUInt( unsigned argX, unsigned argY, unsigned argZ )
       : x(argX), y(argY), '''z(argX)'''
 // ...

Zamiast z(argX) powinno być z(argZ). I trzeba teraz wszystko poprawiać, w tym także resztę funkcji... Dobrze, że to tylko tyle. Ale na szczęście C++ daje nam prostszy sposób.

Wyróżniamy różne możliwości szablonów:


Wykorzystywanie szablonów

[edytuj]

Napiszmy teraz jeszcze raz nasz program. Tym razem z wykorzystaniem szablonów.


 // ...
 template <typename T>
 class Punkt
 {
    public:
       Punkt( T argX, T argY, T argZ )
       : x(argX), y(argY), z(argX)
       { }
 
       T x, y, z;
 };
 // ...

Za pomocą template<typename T> tworzymy nasz szablon. Parametrem jest typ, jaki chcemy użyć, tworzymy go poprzez <typename T>. Teraz, aby utworzyć nasze punkty możemy zapisać:

 Punkt<int> A(0,-10,0);
 Punkt<unsigned> A(0,10,5);

Czyli nasz main będzie wyglądał tak:

 int main(void)
 {
    Punkt<int> A(0,-10,0);
    Punkt<unsigned> B(0,10,5);
 
    std::cout << "A(" << A.x << "," << A.y << "," << A.z << ")" << std::endl;
    std::cout << "B(" << B.x << "," << B.y << "," << B.z << ")" << std::endl;
 }

Ale nie podoba nam się ta notacja, bo na przykład program za bardzo zaczyna nam przypominać HTML. Co mamy zrobić? Nic innego, jak tylko przed funkcją main skorzystać z typedef:

 typedef Punkt<int> PunktInt;
 typedef Punkt<unsigned> PunktUInt;
 typedef Punkt<float> PunktFloat;

I tyle. Main zamieni nam się wtedy w pierwotną formę:

 int main(void)
 {
    PunktInt A(0,-10,0);
    PunktUInt B(0,10,5);
 
    std::cout << "A(" << A.x << "," << A.y << "," << A.z << ")" << std::endl;
    std::cout << "B(" << B.x << "," << B.y << "," << B.z << ")" << std::endl;
 }

Powiesz:

No dobra, ale jak teraz uruchomię ten program to nadal mam ten sam, zły wynik.

I masz rację, bo zobaczysz:

A(0,-10,0)
B(0,10,0)

Powód jest ten sam co poprzedni.

       Punkt( T argX, T argY, T argZ )
       : x(argX), y(argY), '''z(argX)'''
       { }

Musimy zamienić z(argX) na z(argZ) i będzie wszystko w porządku. Tylko tyle. Nie wierzysz? Ale to jest prawda. To zobacz cały nasz program powinien wyglądać w ten sposób:

 #include <iostream>
 
 template <typename T>
 class Punkt
 {
    public:
       Punkt( T argX, T argY, T argZ )
       : x(argX), y(argY), z(argZ)
       { }
 
       T x, y, z;
 };
 
 typedef Punkt<int> PunktInt;
 typedef Punkt<unsigned> PunktUInt;
 typedef Punkt<float> PunktFloat;
 
 
 int main(void)
 {
    PunktInt A(0,-10,0);
    PunktUInt B(0,10,5);
 
    std::cout << "A(" << A.x << "," << A.y << "," << A.z << ")" << std::endl;
    std::cout << "B(" << B.x << "," << B.y << "," << B.z << ")" << std::endl;
 }

Szablony z wieloma parametrami

[edytuj]

Szablon może także mieć więcej niż jeden parametr. Na przykład chcielibyśmy posługiwać się parami obiektów. Należy więc napisać klasę Para, zawierającą dwa elementy: pierwszy o nazwie pierwszy, a drugi o nazwie drugi, jednakże nie wiemy z góry, jakie mają one mieć typy. Możemy to zrobić w ten sposób:

 #include <iostream>
 #include <string>
 template <typename T1, typename T2>
 class Para
 {
    public:
       Para()
       { }

       Para( T1 a, T2 b )
       : pierwszy(a), drugi(b)
       { }

       T1 pierwszy;
       T2 drugi;
 };

 int main(void)
 {
     // tworzymy nasz obiekt
     Para<std::string,int> zmienna("Liczba",10);

     std::cout << zmienna.pierwszy << " " << zmienna.drugi << std::endl;
     return 0;
 }

Za pomocą template<typename T1, typename T2> utworzyliśmy szablon o dwóch parametrach.

Deklaracja zmiennej zmienna określa jej typ poprzez skonkretyzowanie typów w szablonie, pośrednio więc określa też, jakie typy będą miały składowe tej zmiennej.

Można też liczby

[edytuj]

Parametrem szablonu może być także liczba. Zilustrujmy to przykładem:

 #include <iostream>
 #include <cstddef>
 template <typename T, std::size_t N>
 class Tablica
 {
     public:
 
        T &operator[]( std::size_t i )
        {
           return tabl[i];
        }
 
     private:
        T tabl[N];
 };
 
 int main(void)
 {
    Tablica<int,10> A;
 
    for ( int i=0; i<10; ++i )
    {
        A[i]=100+i;
    }
 
    for ( int i=0; i<10; ++i )
    {
       std::cout << "A[" << i << "]=" << A[i] << std::endl;
    }
    return 0;
 }

W powyższym przykładzie użyto typu std::size_t, zadeklarowanego w pliku nagłówkowym dołączanym dyrektywą #include <cstddef>.