2.4.pdf

(653 KB) Pobierz
Microsoft Word - Megatutorial.doc
4
SZABLONY
Gdy coś się nie udaje, mówimy,
że to był tylko eksperyment.
Robert Penn Warren
Nieuchronnie, wielkimi krokami, zbliżamy się do końca kursu C++. Przed tobą jeszcze
tylko jedno, ostatnie i arcyważne zagadnienie: tytułowe szablony.
Ten element języka, jak chyba żaden inny, wzbudza wśród wielu programistów różne
niezdrowe emocje i kontrowersje; porównać je można tylko z reakcjami na preprocesor.
Nie są to aczkolwiek reakcje skrajnie negatywne: przeciwnie, szablony powszechnie
uważa się za jeden z największych atutów języka C++.
Problemem jest jednak to, iż obecne ich możliwości (mimo że już teraz ogromne) są
niezadowalające dla biegłych programistów. Dlatego też właśnie szablony są tą częścią
C++, która najszybciej podlega ewolucji. Trzeba jednak uświadomić sobie, że od
odgórnie narzuconego pomysłu Komitetu Standaryzacyjnego do implementacji stosownej
funkcji w kompilatorach wiedzie bardzo daleka droga. Skutek jest taki, że na palcach
jednej ręki można policzyć kompilatory, które w pełni odpowiadają tym zaleceniom i
oferuje szablony całkowicie zgodne ze standardem. Jest to zadziwiające, zważywszy że
sama idea szablonów liczy już sobie kilkanaście (!) lat.
Mam jednak także pocieszającą wiadomość. Otóż można kręcić nosem i narzekać, że
kompilator, którego używamy, nie jest w pełni „na czasie”, lecz dla większości
programistów nie będzie to miało wielkiego znaczenia. Oczywiście, najlepiej jest używać
zawsze najnowszych wersji narzędzi programistycznych; nie oznacza to wszakże, że
starsze ich wersje nie nadają się do niczego.
Skoro już o tym mówię, to przydałoby się wspomnieć, jak wygląda obsługa szablonów w
naszym ulubionym kompilatorze, czyli Visual C++. I tu czeka nas raczej miła
niespodzianka. Przede wszystkim warto wiedzieć, że jego aktualna wersja, zawarta w
pakiecie Microsoft Visual Studio .NET 2003, jest absolutnie zgodna z aktualnym
standardem języka C++ - naturalnie, także pod względem obsługi szablonów. Jeżeli
natomiast chodzi o starszą wersję Visual Studio .NET (nazywaną teraz często .NET 2001),
to tutaj sprawa także przedstawia się nie najgorzej. W codziennym, ani nawet nieco
bardziej egzotycznym programowaniu nie odczujemy bowiem żadnego niedostatku w
obsłudze szablonów przez ten kompilator.
Niestety, podobnie dobrych wiadomości nie mam dla użytkowników Visual C++ 6. To
leciwe już środowisko może szybko okazać się niewystarczające. Warto więc zaopatrzyć
w jego nowszą wersję.
W każdym jednak przypadku, niezależnie od posiadanego kompilatora, znajomość
szablonów jest niezbędna. Wpisały się one w praktykę programistyczną na tyle silnie, że
obecnie mało który program może się bez nich obejść. Poza tym przekonasz się wkrótce
na własnej skórze, że stosowanie szablonów zdecydowanie ułatwia typowe czynności
koderskie i sprawia, że tworzony kod staje się znacznie bardziej uniwersalny i elastyczny.
Najlepszym przykładem tego jest Biblioteka Standardowa języka C++, z której
fragmentów miałeś już okazję korzystać.
Zabierzmy się zatem do poznawania szablonów - na pewno tego nie pożałujesz :D
506
Zaawansowane C++
Podstawy
Na początek przedstawię ci, czym w ogóle są szablony i pokaże kilka przykładów na ich
zastosowanie. Bardziej zaawansowanymi zagadnieniami zajmiemy się bowiem w
następnym podrozdziale. Na razie czas na krótkie wprowadzenie.
Idea szablonów
Mógłbym teraz podwinąć rękami, poprosić cię o uwagę i kawałek po kawałku wyjaśniać,
czym są te całe szablony. Na to również przyjdzie pora, ale najpierw lepiej chyba odkryć,
do czego mogą nam się te dziwne twory przydać. Dzięki temu może łatwiej przyjdzie ci
ich zrozumienie, a potem znajdowanie dlań zastosowań i wreszcie… polubienie ich! Tak,
szablony naprawdę można polubić - za robotę, której nam oszczedzają; nam: ciężko
przecież pracującym programistom ;-)
Zobacz zatem, jakie fundamentalne problemy pomogą ci niedługo rozwiązywać te
nieocenione konstrukcje…
Ścisłość C++ powodem bólu głowy
Pewnie słyszałeś już wcześniej, że C++ jest językiem o ścisłej kontroli typów. Znaczy to,
że typy danych pełnią w nim duże znaczenie i że zawsze istnieje wyraźne rozgraniczenie
pomiędzy nimi.
Jednocześnie wiele mechanizmów tego języka służy, paradoksalnie, właśnie zatarciu
granic pomiędzy typami danych. Wystarczy przypomnieć chociażby niejawne konwersje,
które pozwalają dokonywać „w locie” zamiany z jednego typu na drugi, w sposób
niezauważalny. Ponadto klasy w C++ są skonstruowane tak, aby w razie potrzeby mogły
niemal doskonale imitować typy wbudowane.
Mimo to, ściśły podział informacji na liczby, napisy, struktury itd. może być często sporą
przeszkodą…
Dwa typowe problemy
Kłopoty zaczynają się, gdy chcemy napisać kod, który powinien działać w odniesieniu do
kilku możliwych typów danych. Z grubsza można tu rozdzielić dwie sytuacje: gdy
próbujmy napisać uniwersalną funkcję i gdy podobną próbę czynimy przy definiowaniu
klasy.
Problem 1: te same funkcje dla różnych typów
Tradycyjnym, wręcz klasycznym przykładem tego pierwszego problemu jest funkcja
wyznaczająca większa liczbę spośród dwóch podanych. Prawdopodobnie z takiej funkcji
będziesz często skorzystał, więc kiedyś możesz ją zdefiniować np. jako:
int max( int nLiczba1, int nLiczba2)
{
return (nLiczba1 > nLiczba2 ? nLiczba1 : nLiczba2);
}
Taka funkcja działa dobrze dla liczb całkowitych, ale już całkiem nie radzi sobie z liczbami
typu float czy double , bo zarówno wynik, jak i parametry są zaokrąglane do jedności.
Dla zdefiniowanych przez nas typów danych jest zaś zupełnie nieprzydatna, co chyba
zresztą całkowicie zrozumiałe.
Naturalnie, możemy sobie dodać inne, przeciążone wersje funkcji - jak chociażby taką:
double max( double fLiczba1, double fLiczba2)
{
Szablony
507
return (fLiczba1 > fLiczba2 ? fLiczba1 : fLiczba2);
}
Takich wersji musiałoby być jednak bardzo wiele: za każdym kolejnym typem, dla
którego chcielibyśmy stosować max() , musiałaby iść odrębna funkcja. Ich definiowanie
byłoby uciążliwe i nudne, a podczas wykonywania tej nużącej czynności trudno byłoby nie
zwątpić, czy jest to aby na pewno słuszne rozwiązanie…
Problem 2: klasy operujące na dowolnych typach danych
Innym problemem są klasy, które z jakichś względów muszą być elastyczne i operować
na danych dowolnego typu. Koronnym przykładem są pojemniki, jak np. tablice
dynamiczne, podobne do naszej klasy CIntArray . Jak wiemy, ma ona sporą wadę: przy
jej pomocy nie można bowiem zarządzać tablicą elementów innego typu niż int . Chcąc
to osiągnąć, należałoby napisać nową klasę - zapewne bardzo podobną do wspomnianej.
Tę samę pracę trzebaby wykonać dla każdego następnego typu elementów…
To na pewno nie jest dobre wyjście!
Możliwe rozwiązania
„Ale jakie mamy wyjście?”, spytasz pewnie. Cóż, można sobie jakoś radzić…
Wykorzystanie preprocesora
Ogólną funkcję max() (i podobne) możemy zasymulować przy użyciu parametryzowanych
makr:
#define MAX(a,b) ((a) > (b) ? (a) : (b))
Sądzę jednak, że pamiętasz wady takich makrodefinicji. Nawiasy wokół a i b likwidują
wprawdzie problem pierwszeństwa operatorów, ale nie zabezpieczą przed podwójnym
obliczaniem wyrażeń. Wiesz przecież, że preprocesor działa na kodzie tak jak na tekście,
zatem np. wyrażenie w rodzaju:
MAX( 10 , rand())
nie zwróci nam wcale liczby pseudolosowej równej co najmniej 10 . Zostanie ono bowiem
rozwinięte do:
(( 10 ) > (rand()) ? 10 : (rand()))
Funkcja rand() będzie więc obliczana dwukrotnie, z każdym razem dając oczywiście inny
wynik - bo takie jest jej przeznaczenie. Makro MAX() nie będzie więc zawsze działało
poprawnie.
Używanie ogólnych typów
Jeszcze mniej oczywisty jest sposób na zaimplementowanie ogólnej klasy, np. tablicy
przechowującej dowolny typ elementów. Tutaj aczkolwiek także istnieje pewne
rozwiązanie: można użyć ogólnego wskaźnika, tworząc tablicę elementów typu void * :
class CPtrArray
{
private :
// tablica i jej rozmiar
void ** m_ppvTablica;
unsigned m_uRozmiar;
// itd. (metody i przeciążone operatory)
508
Zaawansowane C++
};
Będziemy musieli się jednak zmagać z niedogodnościami wskaźników void * - przede
wszystkim z utratą informacji o rzeczywistym typie danych:
CPtrArray Tablica( 5 );
// alokacja pamięci dla elementu (!)
Tablica[ 2 ] = new int ;
// przypisanie - nieszczególnie ładne...
*( static_cast < int *>(Tablica[ 2 ])) = 10 ;
Każdorazowe rzutowanie na właściwy typ elementów (tutaj int ) na pewno nie będzie
należało do przyjeności. Poza tym trzeba będzie pamiętać o zwolnieniu pamięci
zaalokowanej dla poszczególnych elementów. W przypadku małych obiektów, jak liczby,
nie ma to żadnego sensu…
Zatem nie! To na pewno nie jest zadowalające wyjście!
Szablony jako rozwiązanie
W porządku, dosyć tych bezowocnych poszukiwań. Myślę, że domyślasz się, iż to
szablony są tym rozwiązaniem, którego poszukujemy. Zatem nie tracąc więcej czasu,
znajdźmy je wreszcie :)
Kod niezależny od typu
Wróćmy wpierw do prób napisania funkcji max() . Patrząc na jej dwie wersje, dla typów
int i double , możemy łatwo zauważyć, że różnią się one bardzo niewiele. Właściwie to
można stwierdzić, że po prostu drugi z wariantów ma wpisane double tam, gdzie w
pierwszym widnieje typ int .
Gdybyśmy więc chcieli napisać ogólny wzorzec dla funkcji max() , wyglądałby on tak:
typ max( typ Parametr1, typ Parametr2)
{
return (Parametr > Parametr2 ? Parametr1 : Parametr2);
}
No dobrze, możemy sobie pisać takie wzorce, ale co nam z tego? Nie znamy przecież
żadnego sposobu, aby przekazać go kompilatorowi do wykorzystania… Czy na pewno?…
Kompilator to potrafi
Ależ nie! Możemy ten wzorzec - ten szablon (ang. template ) - wpisać do kodu, tworząc
ogólną funkcję max() . Trzeba to jedynie zrobić w odpowiedni sposób - tak, aby
kompilator wiedział, z czym ma do czynienia. Zobaczmy więc, jak można tego dokonać.
Składnia szablonu
A zatem: chcąc zdefiniować wzorzec funkcji max() , musimy napisać go w ten oto sposób
sposób:
template < typename TYP> TYP max(TYP Parametr1, TYP Parametr2)
{
return (Parametr > Parametr2 ? Parametr1 : Parametr2);
}
Szablony
509
Dopóki nie wyjaśnimy sobie dokładnie kwestii umieszczania szablonów w plikach
źródłowych, zapamiętaj, aby wpisywać je w całości w plikach nagłówkowych .
W ten sposób tworzymy szablon funkcji (ang. function template ) Zobaczmy, co się na
niego składa.
Zauważyłeś zapewne najpierw zupełnie nową część nagłówka funkcji:
template < typename TYP>
Jest ona obowiązkowa dla każdego rodzaju szablonów, nie tylko funkcji. Słowo kluczowe
template (‘szablon’) mówi bowiem kompilatorowi, że nie ma tu do czynienia ze zwykłym
kodem, lecz właśnie z szablonem.
Dalej następuje, ujęta w nawiasy ostre, lista parametrów szablonu . W tym przypadku
mamy tylko jeden taki parametr: słowo typename (‘nazwa typu’) informuje, że jest nim
typ. Okazuje się bowiem, że parametrami szablonu mogą być także „normalne” wartości,
podobne do argumentów funkcji - nimi też się zajmiemy, ale później. Na razie mamy tu
jeden parametr szablonu będący typem o jakże opisowej nazwie TYP .
Potem przychodzi już normalna definicja funkcji - z jedną drobną różnicą. Jak widać,
używamy w niej nazwy TYP zamiast właściwego typu danych (czyli int , double , itd.).
Stosujemy go jednak w tych samych miejscach, czyli jako typ wartości zwracanej oraz
typ obu przyjmowanych parametrów funkcji.
Treść szablonu odpowiada więc wzorcowi z poprzedniego akapitu. Różnica jest jednak
taka, że o ile tamten „kod” był niezrozumiały dla kompilatora, o tyle ten szablon jest jak
najbardziej poprawny i, co najważniejsze, działa zgodnie z oczekiwaniami. Nasza funkcja
max() potrafi już bowiem operować na dowolnym typie argumentów:
int nMax = max( -1 , 2 ); // TYP = int
unsigned uMax = max( 10u , 65u ); // TYP = unsigned
float fMax = max( -12.4 , 67 ); // TYP = double (!)
Najciekawsze jest to, iż to funkcja na podstawie swych argumentów „sama zgaduje”, jaki
typ danych ma być wstawiony w miejsce symbolicznej nazwy TYP . To właśnie jedna z
zalet szablonów funkcji: używamy ich zwykle tak samo, jak normalnych funkcji, a
jednocześnie zyskujemy zadziwiającą uniwersalność.
Popatrzmy jeszcze na ogólną składnię szablonu w C++:
template < parametry_szablonu > kod
Jak wspomniałem, słówko template jest tu obowiązkowe, bo dzięki nim niemu kompilator
wie, że ma do czynienia z szablonem. parametry_szablonu to najczęściej symboliczne
oznaczenia nieznanych z góry typów danych; oznaczenia te są wykorzystywane w
następującym dalej kodzie .
Na temat obu tych kluczowych części szablonu powiemy sobie jeszcze mnóstwo rzeczy.
Co może być szablonem
Wpierw ustalmy, do jakiego rodzaju kodu w C++ możemy „doczepić” frazę
template <...> , czyniąc ją szablonem. Generalnie mamy dwa rodzaje szablonów:
¾ szablony funkcji - są to więc taki funkcje, które mogą działać w odniesieniu do
dowolnego typu danych. Zazwyczaj kompilator potrafi bezbłędnie ustalić, jaki typ
jest właściwy w konkretnym wywołaniu (por. przykład zastosowania szablonu
max() z poprzedniego punktu)
95461550.002.png 95461550.003.png 95461550.004.png 95461550.005.png 95461550.001.png
Zgłoś jeśli naruszono regulamin