C++ bez cholesterolu

Programowanie generyczne - wprowadzenie

Programowanie generyczne jest już określeniem dość starym i właściwie należy powiedzieć, że C++ jest nawet dość w tyle za językami, w których naturalnie programuje się w sposób generyczny. Jednak nie jest to takie proste dla języka, który ma być wyposażony w ścisłą typizację, zatem właściwości pozwalające na takie programowanie wyglądają w C++ trochę nakładkowo.

Na czym to polega? Spróbujmy sobie wyobrazić funkcję, która przyjmuje dwa argumenty i dzieli jeden przez drugi. Jest jeszcze problem zastosowanego języka programowania. Niech by było w Haskellu, mam nadzieje, że będzie zrozumiałe:

podziel x y = x / y

I spróbujmy wywołać to w jakiś dziwaczny sposób, np.:

podziel "dwa" "trzy"

No i mamy problem. O co tu chodzi? Ano tylko i wyłącznie o to, że "dwa" i "trzy" to nie są argumenty, które jest w stanie przyjąć operator `/'. Tak się to właśnie odbywa w programowaniu w językach tego typu. Nie ma domyślnej konwersji, tylko próbuje się rozpoznać typ argumentu, jaki został przekazany. Można przekazać cokolwiek, aczkolwiek nie każde cokolwiek zostanie zaakceptowane.

Haskell w tej kwestii jest podobny do klonów ML-a, czy też języków takich jak Lisp, czy Prolog (a to, co go od tych języków wyraźnie odróżnia, to ścisła, statyczna typizacja). Właśnie wzorce udostępniły językowi C++ również pewne elementy programowania funkcjonalnego. Z owych języków jednak C++ niczego nie przejął; z ML zostały wzięte częściowo wyjątki, natomiast wzorce i przestrzenie nazw pochodzą z Ady.

W C++ deklaracja owego podziel brzmiałaby:

template <typename T>
T podziel( T x, T y )
{ return x/y; }

Jak widać, składnia jest o wiele dłuższa, ale za to nie tracimy ani typizacji, ani domyślnych konwersji. Do tej funkcji `podziel' akurat należy podać argumenty tego samego typu, a zwraca się też argument tego typu. Można oczywiście zadeklarować więcej typów, aczkolwiek trudno jednak stwierdzić, jaki typ następnie miałby być zwracany (C++ niestety nie posiada mechanizmów domniemania typu zwracanego i jest to od pewnego czasu znany problem). Zatem w powyższym przykładzie można to wywołać jako podziel(10,2), czy podziel(23.54,5.0), ale niestety podziel(2,2.5) już nie przejdzie.

Programowanie generyczne za pomocą wzorców jest trudniejsze i bardziej uciążliwe od pisania generycznego w meta-językach. Daje jednak większe i ciekawsze możliwości, nie wspominając już o szybkości, zwłaszcza że nie tylko typ może być na liście parametrów, ale można je też specyfikować wprost lub częściowo.

Jeszcze parę słów o wzorcach. Same wzorce w C++ też nie wzięły się z powietrza. Są one konsekwencją tego, co już zostało zapoczątkowane wcześniej w języku C (podobnie jak z referencjami). W języku C od zawsze mieliśmy tablice i wskaźniki, które były właśnie takimi meta-typami. Miały swoje właściwości, ale wskaźnik jako-taki sam nigdy nie był typem. Podobnie z tablicą. Ma ona swoje właściwości, operator indeksowania, swój rozmiar itd., aczkolwiek rozmiar tablicy jest zależny np. od rozmiaru elementów. Zatem tablica, jak też wskaźnik były w C właśnie takimi swoistego rodzaju wzorcami, które miały tylko odpowiednie wsparcie składniowe. W C++ rozwinięto ten pomysł, pozwalając użytkownikowi tworzyć samemu takie meta-typy.