STL w praktyce. 50 sposobów efektywnego wykorzystania

21
Wydawnictwo Helion ul. Chopina 6 44-100 Gliwice tel. (32)230-98-63 e-mail: [email protected] PRZYK£ADOWY ROZDZIA£ PRZYK£ADOWY ROZDZIA£ IDZ DO IDZ DO ZAMÓW DRUKOWANY KATALOG ZAMÓW DRUKOWANY KATALOG KATALOG KSI¥¯EK KATALOG KSI¥¯EK TWÓJ KOSZYK TWÓJ KOSZYK CENNIK I INFORMACJE CENNIK I INFORMACJE ZAMÓW INFORMACJE O NOWOCIACH ZAMÓW INFORMACJE O NOWOCIACH ZAMÓW CENNIK ZAMÓW CENNI K CZYTELNIA CZYTELNIA FRAGMENTY KSI¥¯EK ONLINE FRAGMENTY KSI¥¯EK ONLINE SPIS TRECI SPIS TRECI DODAJ DO KOSZYKA DODAJ DO KOSZYKA KATALOG ONLINE KATALOG ONLINE STL w praktyce. 50 sposobów efektywnego wykorzystania Standard Template Library to jedno z najpotê¿niejszych narzêdzi programistycznych charakteryzuj¹ce siê z³o¿onoci¹ i wysokim stopniem komplikacji. W ksi¹¿ce „STL w praktyce. 50 sposobów efektywnego wykorzystania” — przeznaczonej dla zaawansowanych i rednio zaawansowanych programistów C++ — znany ekspert Scott Meyers prezentuje najwa¿niejsze techniki stosowania tego narzêdzia. Poza list¹ zaleceñ, jak postêpowaæ nale¿y, a jak postêpowaæ siê nie powinno, Meyers przedstawia wiele powa¿niejszych aspektów STL pozwalaj¹cych zrozumieæ, dlaczego pewne rozwi¹zania s¹ poprawne, a innych nale¿y unikaæ. Do ka¿dej wskazówki do³¹czony jest kod ród³owy i dok³adne objanienia, które powinny zainteresowaæ zaawansowanych programistów. Je¿eli Twoja wiedza ogranicza siê do informacji dostêpnych w dokumentacji STL, ta ksi¹¿ka uzupe³ni j¹ o bezcenne wskazówki, które pozwol¹ Ci wykorzystaæ STL w praktyce. Ksi¹¿ka przedstawia: • Podstawowe informacje o bibliotece STL i jej zgodnoci z innymi standardami • Wskazówki dotycz¹ce poprawnego doboru i u¿ywania pojemników • Opis pojemników typu vector i string • Omówienie pojemników asocjatywnych • Metody w³aciwego korzystania z iteratorów • Algorytmy wchodz¹ce w sk³ad STL • Funktory, klasy-funktory i funkcje • Programowanie za pomoc¹ biblioteki STL Ksi¹¿kê uzupe³niaj¹ dodatki, w których znajdziesz miêdzy innymi obszern¹ bibliografiê na temat STL oraz cenne wskazówki dla programistów u¿ywaj¹cych kompilatorów firmy Microsoft. „STL w praktyce. 50 sposobów efektywnego wykorzystania” to nieocenione ród³o wiedzy na temat jednego z najwa¿niejszych aspektów programowania w C++. Je¿eli chcesz w praktyce wykorzystaæ STL, nie obêdziesz siê bez informacji zawartych w tej ksi¹¿ce. Autor: Scott Meyers T³umaczenie: Adam Majczak (rozdz. 1-5), Wojciech Moch (rozdz. 6, 7, dod. A-C) ISBN: 83-7361-373-0 Tytu³ orygina³u: Effective STL 50 Specific Ways to Improve Your Use of the Standard Template Library Format: B5, stron: 282

description

Standard Template Library to jedno z najpotężniejszych narzędzi programistycznych charakteryzujące się złożonością i wysokim stopniem komplikacji. W książce "STL w praktyce. 50 sposobów efektywnego wykorzystania" -- przeznaczonej dla zaawansowanych i średnio zaawansowanych programistów C++ -- znany ekspert Scott Meyers prezentuje najważniejsze techniki stosowania tego narzędzia. Poza listą zaleceń, jak postępować należy, a jak postępować się nie powinno, Meyers przedstawia wiele poważniejszych aspektów STL pozwalających zrozumieć, dlaczego pewne rozwiązania są poprawne, a innych należy unikać. Do każdej wskazówki dołączony jest kod źródłowy i dokładne objaśnienia, które powinny zainteresować zaawansowanych programistów.Jeżeli Twoja wiedza ogranicza się do informacji dostępnych w dokumentacji STL, ta książka uzupełni ją o bezcenne wskazówki, które pozwolą Ci wykorzystać STL w praktyce.Książka przedstawia:* Podstawowe informacje o bibliotece STL i jej zgodności z innymi standardami* Wskazówki dotyczące poprawnego doboru i używania pojemników* Opis pojemników typu vector i string* Omówienie pojemników asocjatywnych* Metody właściwego korzystania z iteratorów* Algorytmy wchodzące w skład STL* Funktory, klasy-funktory i funkcje* Programowanie za pomocą biblioteki STLKsiążkę uzupełniają dodatki, w których znajdziesz między innymi obszerną bibliografię na temat STL oraz cenne wskazówki dla programistów używających kompilatorów firmy Microsoft."STL w praktyce. 50 sposobów efektywnego wykorzystania" to nieocenione źródło wiedzy na temat jednego z najważniejszych aspektów programowania w C++. Jeżeli chcesz w praktyce wykorzystać STL, nie obędziesz się bez informacji zawartych w tej książce.

Transcript of STL w praktyce. 50 sposobów efektywnego wykorzystania

Page 1: STL w praktyce. 50 sposobów efektywnego wykorzystania

Wydawnictwo Helion

ul. Chopina 6

44-100 Gliwice

tel. (32)230-98-63

e-mail: [email protected]

PRZYK£ADOWY ROZDZIA£PRZYK£ADOWY ROZDZIA£

IDZ DOIDZ DO

ZAMÓW DRUKOWANY KATALOGZAMÓW DRUKOWANY KATALOG

KATALOG KSI¥¯EKKATALOG KSI¥¯EK

TWÓJ KOSZYKTWÓJ KOSZYK

CENNIK I INFORMACJECENNIK I INFORMACJE

ZAMÓW INFORMACJEO NOWO�CIACH

ZAMÓW INFORMACJEO NOWO�CIACH

ZAMÓW CENNIKZAMÓW CENNIK

CZYTELNIACZYTELNIA

FRAGMENTY KSI¥¯EK ONLINEFRAGMENTY KSI¥¯EK ONLINE

SPIS TRE�CISPIS TRE�CI

DODAJ DO KOSZYKADODAJ DO KOSZYKA

KATALOG ONLINEKATALOG ONLINE

STL w praktyce. 50 sposobówefektywnego wykorzystania

Standard Template Library to jedno z najpotê¿niejszych narzêdzi programistycznych

charakteryzuj¹ce siê z³o¿ono�ci¹ i wysokim stopniem komplikacji. W ksi¹¿ce

„STL w praktyce. 50 sposobów efektywnego wykorzystania” — przeznaczonej dla

zaawansowanych i �rednio zaawansowanych programistów C++ — znany ekspert Scott

Meyers prezentuje najwa¿niejsze techniki stosowania tego narzêdzia. Poza list¹ zaleceñ,

jak postêpowaæ nale¿y, a jak postêpowaæ siê nie powinno, Meyers przedstawia wiele

powa¿niejszych aspektów STL pozwalaj¹cych zrozumieæ, dlaczego pewne rozwi¹zania

s¹ poprawne, a innych nale¿y unikaæ. Do ka¿dej wskazówki do³¹czony jest kod

�ród³owy i dok³adne obja�nienia, które powinny zainteresowaæ zaawansowanych

programistów.

Je¿eli Twoja wiedza ogranicza siê do informacji dostêpnych w dokumentacji STL,

ta ksi¹¿ka uzupe³ni j¹ o bezcenne wskazówki, które pozwol¹ Ci wykorzystaæ STL

w praktyce.

Ksi¹¿ka przedstawia:

• Podstawowe informacje o bibliotece STL i jej zgodno�ci z innymi standardami

• Wskazówki dotycz¹ce poprawnego doboru i u¿ywania pojemników

• Opis pojemników typu vector i string

• Omówienie pojemników asocjatywnych

• Metody w³a�ciwego korzystania z iteratorów

• Algorytmy wchodz¹ce w sk³ad STL

• Funktory, klasy-funktory i funkcje

• Programowanie za pomoc¹ biblioteki STL

Ksi¹¿kê uzupe³niaj¹ dodatki, w których znajdziesz miêdzy innymi obszern¹ bibliografiê

na temat STL oraz cenne wskazówki dla programistów u¿ywaj¹cych kompilatorów

firmy Microsoft.

„STL w praktyce. 50 sposobów efektywnego wykorzystania” to nieocenione �ród³o

wiedzy na temat jednego z najwa¿niejszych aspektów programowania w C++.

Je¿eli chcesz w praktyce wykorzystaæ STL, nie obêdziesz siê bez informacji

zawartych w tej ksi¹¿ce.

Autor: Scott Meyers

T³umaczenie: Adam Majczak (rozdz. 1-5),

Wojciech Moch (rozdz. 6, 7, dod. A-C)

ISBN: 83-7361-373-0

Tytu³ orygina³u: Effective STL 50 Specific Ways

to Improve Your Use of the Standard Template Library

Format: B5, stron: 282

Page 2: STL w praktyce. 50 sposobów efektywnego wykorzystania

Spis treści

Podziękowania................................................................................... 9

Przedmowa...................................................................................... 13

Wprowadzenie ................................................................................. 17

Rozdział 1. Kontenery........................................................................................ 29

Zagadnienie 1. Uważnie dobierajmy kontenery................................................................30

Zagadnienie 2. Nie dajmy się zwieść iluzji o istnieniu kodów niezależnych

do zastosowanego kontenera........................................................................................35

Zagadnienie 3. Kopiowanie obiektów w kontenerach powinno być „tanie”,

łatwe i poprawne ..........................................................................................................40

Zagadnienie 4. Stosujmy metodę empty(), zamiast przyrównywać

rozmiar size() do zera...................................................................................................43

Zagadnienie 5. Preferujmy metody operujące na całych zakresach (podzbiorach),

bo są bardziej efektywne niż ich odpowiedniki operujące pojedynczymi elementami...45

Zagadnienie 6. Bądźmy wyczuleni i przygotowani na najbardziej

kłopotliwą interpretację kompilatora C++ ...................................................................56

Zagadnienie 7. Gdy stosujemy kontenery zawierające wskaźniki zainicjowane

za pomocą operatora new, pamiętajmy o zwolnieniu dynamicznej pamięci

operatorem delete, zanim zawierający je obiekt-kontener sam zostanie usunięty.......59

Zagadnienie 8. Nigdy nie twórzmy kontenerów zawierających wskaźniki

kategorii auto_ptr .........................................................................................................64

Zagadnienie 9. Uważnie dobierajmy opcje do operacji kasowania ..................................67

Zagadnienie 10. Uważajmy na konwencje dynamicznej alokacji pamięci

i stosowne ograniczenia ...............................................................................................73

Zagadnienie 11. Zrozumienie uprawnionych zastosowań alokatorów pamięci

tworzonych przez użytkownika....................................................................................80

Zagadnienie 12. Miejmy umiarkowane i realistyczne oczekiwania odnośnie

poziomu bezpieczeństwa wielu wątków obsługiwanych przez kontenery STL ..........84

Page 3: STL w praktyce. 50 sposobów efektywnego wykorzystania

6 Spis treści

Rozdział 2. Kontenery typu vector oraz string..................................................... 89

Zagadnienie 13. Lepiej stosować kontenery vector oraz string niż dynamicznie

przydzielać pamięć tablicom........................................................................................90

Zagadnienie 14. Stosujmy metodę reserve, by uniknąć zbędnego przemieszczania

elementów w pamięci...................................................................................................93

Zagadnienie 15. Bądźmy ostrożni i wyczuleni na zróżnicowanie implementacji

kontenerów typu string.................................................................................................96

Zagadnienie 16. Powinieneś wiedzieć, jak przesyłać dane z kontenerów vector

i string do klasycznego interfejsu zgodnego z C........................................................102

Zagadnienie 17. Sztuczka programistyczna „swap trick” pozwalająca

na obcięcie nadmiarowej pojemności ........................................................................106

Zagadnienie 18. Unikajmy stosowania wektora typu vector<bool>...............................108

Rozdział 3. Kontenery asocjacyjne ................................................................... 111

Zagadnienie 19. Ten sam czy taki sam — zrozumienie różnicy pomiędzy

relacjami równości a równoważności ........................................................................112

Zagadnienie 20. Określajmy typy porównawcze dla kontenerów asocjacyjnych

zawierających wskaźniki............................................................................................117

Zagadnienie 21. Funkcje porównujące powinny dla dwóch dokładnie równych

wartości zawsze zwracać wartość false......................................................................122

Zagadnienie 22. Unikajmy bezpośredniego modyfikowania klucza w kontenerach

typu set i multiset .......................................................................................................126

Zagadnienie 23. Rozważmy zastąpienie kontenerów asocjacyjnych

posortowanymi wektorami.........................................................................................133

Zagadnienie 24. Gdy efektywność działania jest szczególnie istotna, należy bardzo

uważnie dokonywać wyboru pomiędzy map::operator[]() a map::insert()................140

Zagadnienie 25. Zapoznaj się z niestandardowymi kontenerami mieszanymi ...............146

Rozdział 4. Iteratory ........................................................................................ 151

Zagadnienie 26. Lepiej wybrać iterator niż const_iterator, reverse_iterator

czy const_reverse_iterator..........................................................................................151

Zagadnienie 27. Stosujmy funkcje distance() i advance(), by przekształcić

typ const_iterator na typ iterator ................................................................................155

Zagadnienie 28. Jak stosować metodę base() należącą do klasy reverse_iterator

w celu uzyskania typu iterator?..................................................................................159

Zagadnienie 29. Rozważ zastosowanie iteratorów typu istreambuf_iterator

do wczytywania danych znak po znaku .....................................................................163

Rozdział 5. Algorytmy...................................................................................... 165

Zagadnienie 30. Upewnijmy się, że docelowe zakresy są wystarczająco obszerne .......166

Zagadnienie 31. Pamiętajmy o dostępnych i stosowanych opcjach sortowania .............171

Zagadnienie 32. Stosujmy metodę erase() w ślad za algorytmami kategorii remove(),

jeśli naprawdę chcemy coś skutecznie usunąć z kontenera .......................................177

Zagadnienie 33. Przezornie i ostrożnie stosujmy algorytmy kategorii remove()

wobec kontenerów zawierających wskaźniki ............................................................182

Zagadnienie 34. Zwracajmy uwagę, które z algorytmów oczekują

posortowanych zakresów ...........................................................................................186

Page 4: STL w praktyce. 50 sposobów efektywnego wykorzystania

Spis treści 7

Zagadnienie 35. Implementujmy zwykłe porównywanie łańcuchów znaków bez

rozróżniania wielkości liter za pomocą mismatch() lub lexicographical_compare() ....190

Zagadnienie 36. Zrozum prawidłową implementację algorytmu copy_if()....................196

Zagadnienie 37. Stosujmy accumulate() lub for_each() do operacji grupowych

na zakresach ...............................................................................................................198

Rozdział 6. Funktory, klasy-funktory, funkcje i inne........................................... 205

Zagadnienie 38. Projektowanie klas-funktorów do przekazywania przez wartość ........205

Zagadnienie 39. Predykaty powinny być funkcjami czystymi .......................................208

Zagadnienie 40. Klasy-funktory powinny być adaptowalne...........................................212

Zagadnienie 41. Po co stosować funkcje ptr_fun, mem_fun i mem_fun_ref?................215

Zagadnienie 42. Upewnij się, że less<t>() oznacza operator<() .....................................219

Rozdział 7. Programowanie za pomocą biblioteki STL ....................................... 223

Zagadnienie 43. Używaj algorytmów, zamiast pisać pętle .............................................223

Zagadnienie 44. Zamiast algorytmów stosujmy metody o takich samych nazwach ......231

Zagadnienie 45. Rozróżnianie funkcji count(), find(), binary_search(),

lower_bound(), upper_bound() i equal_range() .........................................................233

Zagadnienie 46. Jako parametry algorytmów stosuj funktory, a nie funkcje .................241

Zagadnienie 47. Unikaj tworzenia kodu „tylko do zapisu” ............................................245

Zagadnienie 48. Zawsze dołączaj właściwe pliki nagłówkowe......................................248

Zagadnienie 49. Naucz się odczytywać komunikaty kompilatora związane

z biblioteką STL.........................................................................................................249

Zagadnienie 50. Poznaj strony WWW związane z biblioteką STL ................................256

Dodatek A Bibliografia .................................................................................... 263

Książki napisane przeze mnie .........................................................................................263

Książki, które nie ja napisałem (choć chciałbym)...........................................................264

Dodatek B Porównywanie ciągów znaków bez uwzględniania wielkości liter ..... 267

Jak wykonywać porównywanie ciągów znaków bez uwzględniania wielkości liter

— artykuł autorstwa Matta Austerna .........................................................................267

Dodatek C Uwagi na temat platformy STL Microsoftu ...................................... 277

Szablony metod w STL...................................................................................................277

MSVC wersje 4 do 6 .......................................................................................................278

Rozwiązania dla kompilatorów MSVC w wersji 4 do 5.................................................279

Dodatkowe rozwiązanie dla kompilatora MSVC w wersji 6. .........................................280

Skorowidz...................................................................................... 283

Page 5: STL w praktyce. 50 sposobów efektywnego wykorzystania

Rozdział 6.

Funktory, klasy-funktory,funkcje i inne

Czy nam się to podoba czy nie, funkcje i podobne do funkcji obiekty — funktory — są

częścią STL. Kontenery asocjacyjne stosują je do porządkowania swoich elemen-

tów, w algorytmach typu ��������� wykorzystywane są do kontroli zachowań tych al-

gorytmów. Algorytmy takie jak ����� �� i ��������� są bez funktorów zupełnie

nieprzydatne, natomiast adaptory typu ������ i ��������� służą do tworzenia funktorów.

Wszędzie gdzie spojrzeć, można w STL znaleźć funktory i klasy-funktory. Znajdą się

one również w Twoich kodach źródłowych. Efektywne zastosowanie STL bez umiejęt-

ności tworzenia dobrych funktorów jest po prostu niemożliwe. Z tego powodu większa

część tego rozdziału będzie opisywać sposoby takiego tworzenia funktorów, aby zacho-

wywały się one zgodnie z wymaganiami STL. Jednak jedno z zagadnień opisuje zupeł-

nie inny temat. Ten podrozdział spodoba się osobom, które zastanawiały się, dlaczego

konieczne jest zaśmiecanie kodu programu wywołaniami funkcji ��������, ��������

i ����������. Oczywiście można zacząć lekturę od tego właśnie podrozdziału, „Za-

gadnienie 41.”, ale proszę nie poprzestawać na nim. Po poznaniu przedstawionych tam

funkcji konieczne będzie zapoznanie się z informacjami z pozostałych, dzięki czemu

Twoje funkcje będą prawidłowo działały zarówno z tymi funkcjami, jak i z resztą STL.

Zagadnienie 38.Projektowanie klas-funktorówdo przekazywania przez wartośćZagadnienie 38. Projektowanie klas-funktorów do przekazywania przez wartość

Języki C i C++ nie pozwalają na przekazywanie funkcji w parametrach innych funkcji.

Konieczne jest przekazywanie wskaźników na te funkcje. Na przykład poniżej znajduje

się deklaracja funkcji ������ z biblioteki standardowej.

Page 6: STL w praktyce. 50 sposobów efektywnego wykorzystania

206 Rozdział 6. ♦ Funktory, klasy-funktory, funkcje i inne

���������������� ������������������������������������������������������� ������ ����� �������

„Zagadnienie 46.” wyjaśnia, dlaczego algorytm ����� jest zazwyczaj lepszym wy-borem od funkcji ������. Teraz omówimy sposób deklarowania parametru ������w funkcji ������. Po dokładniejszym przyjrzeniu się wszystkim gwiazdkom okazujesię, że argument ������, który jest wskaźnikiem na funkcję, jest kopiowany (czyli prze-kazywany przez wartość) z miejsca wywołania do funkcji ������. Jest to typowy przy-kład zasady stosowanej w bibliotekach standardowych języków C i C++, czyli prze-kazywania przez wartość wskaźników na funkcje.

W STL obiekty funkcyjne modelowane są podobnie do wskaźników na funkcje, wobecczego w STL obiekty funkcyjne są również przekazywane do i z funkcji przez war-tość. Najlepszym przykładem może być deklaracja funkcji ����� �� algorytmu po-bierającego i zwracającego przez wartość funktory.

���� ���� ���������� �������������� ������������������������������������������������������� � � �������� �� !"���� �#������� ���"�������������������� ���� ������������������� ��������������������������$ �%� � �������� �� !

Tak na prawdę przekazywanie przez wartość nie jest absolutnie wymagane, ponie-waż w wywołaniu funkcji ����� �� można wyraźnie zaznaczyć typy parametrów.Na przykład w poniższym kodzie funkcja ����� �� pobiera i zwraca funktory przezreferencję.

�� ���&�'���#��()����������� �%�"�����������������*���������������������� ���+, ( ��������-./+���� � �������������������������������������������������� ����$� � �� ��� ����������� ������0��*12���///2�%����"����������))��� ���&����������������%(��� ���$� � �3 �%����"�������������///&�'���#��(����������������������������������������������"��$�� ///"���� �#����������������������������������������%��4 ����"��$�3��"���� �#��������������������������/��(������������������ � ��� ���%���&����������������������������������/����������������������&�'���#��(5��������������������������������������������������������6����������%!�����$ � � ����������������������������������������������������6��� ���������"�����37

Użytkownicy STL prawie nigdy nie przeprowadzają tego typu operacji, co więcej,pewne implementacje niektórych algorytmów STL nie skompilują się w przypadkuprzekazywania funktorów przez referencję. W pozostałej części tego sposobu będę za-kładał, że funktory można przekazywać wyłącznie przez wartość, co w praktyce jestniemal zawsze prawdą.

Funktory muszą być przekazywane przez wartość, dlatego to na Tobie spoczywa ciężarsprawdzenia, czy Twoje funktory przekazywane w ten sposób zachowują się prawidło-wo. Implikuje to dwie rzeczy. Po pierwsze, muszą one być małe, bo kopiowanie dużych

Page 7: STL w praktyce. 50 sposobów efektywnego wykorzystania

Zagadnienie 38. Projektowanie klas-funktorów do przekazywania przez wartość 207

obiektów jest bardzo kosztowne. Po drugie, Twoje funktory nie mogą być polimor-ficzne (muszą być monomorficzne), co oznacza, że nie mogą używać funkcji wirtual-nych. Wynika to z faktu, że obiekt klasy pochodnej przekazywany w parametrze typuklasy bazowej zostaje obcięty, czyli w czasie kopiowania usunięte będą z niegoodziedziczone elementy. W podrozdziale „Zagadnienie 3.” opisano inny problem, jakirozczłonkowanie (ang. slicing) tworzy w czasie stosowania biblioteki STL.

Unikanie rozczłonkowania obiektów i uzyskiwanie dobrej wydajności są oczywiściebardzo ważne, ale trzeba pamiętać, że nie wszystkie funkcje mogą być małe i nie wszy-stkie obiekty mogą być monomorficzne. Jedną z zalet funktorów w porównaniu zezwykłymi funkcjami jest fakt, że mogą one przechowywać informację o swoim stanie.Niektóre funktory muszą być duże, dlatego tak ważna jest możliwość przekazywaniatakich obiektów do algorytmów STL z równą łatwością jak obiektów niewielkich.

Zakaz używania funktorów polimorficznych jest równie nierealistyczny. Język C++pozwala na tworzenie hierarchii dziedziczenia i dynamicznego wiązania. Te możli-wości są tak samo użyteczne w czasie tworzenia klas-funktorów, jak i w innych miej-scach. Klasy-funktory bez dziedziczenia byłyby jak C++ bez „++”. Oczywiście ist-nieje sposób umożliwiający tworzenie dużych i polimorficznych obiektów funkcyjnych,a jednocześnie umożliwiający przekazywanie ich przez wartość, zgodnie z konwencjąprzyjętą w STL.

Ten sposób wymaga przeniesienia wszystkich danych i funkcji polimorficznych do in-nej klasy, a następnie w klasie-funktorze umieszczenie wskaźnika na tę nową klasę.Na przykład utworzenie klasy polimorficznej zawierającej duże ilości danych:

���� ��%��� ���8��� ���9:�;)������������������������������9:�;�<�+9�(�:��%����#����������)�����������������������������������������������;� ��+�=�������������������������������������������������>?���$ �:���"������� �������������������������������������������������@� � A���$��B������� �%�"�������8�������*������������8 �$� � �� ��� ���� ����������������������������������������������3 ���� ������������ ���+, ( ��������-./+������ �)�����?��(�������������������������������8 �$� � �� ���� �������� �%�#���������0��������������������������������� �(������$ �%� ������#�����������///���������������������������������� �� !��%4��%�����"�$%������������)��������� ����������� ��������85�� �������������3���"��$�3 ����� �� ��� �(������///��������������������������������������������������4��$�� ����C���� ���D��4%���2�

wymaga utworzenia małej monomorficznej klasy zawierającej wskaźnik na klasęimplementacji i umieszczenie w klasie implementacji wszystkich danych i funkcjiwirtualnych:

���������� ������ !����������������������$� � ��������� �3���� ����� "#�$����% ��&��� ���� '��������!� ����� (�����������%"�$�� ��3�$� �%�9:�;�������%��)����� *+�������������������������������������������� 3����%�$���� ������� ,+���������������������������������� � �������3����%���$� ����9:�;

Page 8: STL w praktyce. 50 sposobów efektywnego wykorzystania

208 Rozdział 6. ♦ Funktory, klasy-funktory, funkcje i inne

��---��������� ."#�$�����+�������������������$� �%��������"����������D����!��������������������������������������������� ���������$��%��������� ���� ��������������� !� ��� �����+����� ����� "#�$�!�+��������������������$� � �9:�;��7������� 4 ����7������ �%�#/+

���� ��%��� ���8��� ���9:�;)�������������������������������������$ ��������"���� �����3 ����������� �%�"�������8��������*�������$� �%�9:�;���� �)��"#�$�����!� ������+�����������������������D����%�$���� ��������#��%� �������������������������������������������������$� �7�9:�;������)������ ��������������� !� ������������� �������� �� ����� �����$ ��3���(���������������������������������������%��4 �������$� �%�9:�;�������������0��������������+��/��///2�

Funkcja ��������� w klasie ���� przedstawia sposób implementacji wszystkich

niemal wirtualnych funkcji w tej klasie. Wywołują one swoje wirtualne odpowiedniki

z klasy ��������. W efekcie otrzymujemy małą i monomorficzną klasę ����, która ma

dostęp do dużej ilości danych i zachowuje się jak klasa polimorficzna.

Pomijam tutaj pewne szczegóły, ponieważ podstawa naszkicowanej tu techniki jest do-

brze znana programistom C++. Opisywana jest ona w książce Effective C++ w roz-

dziale 34. W książce Design Patterns [6] nazywana jest Bridge Pattern, natomiast Sut-

ter w swojej książce Exceptional C++ [8] nazywa ją Idiomem Pimpl.

Z punktu widzenia STL podstawową rzeczą, o której należy pamiętać, jest fakt, że klasy

stosujące tę technikę muszą rozsądnie obsługiwać kopiowanie. Autor opisywanej wyżej

klasy ���� musiałby odpowiednio zaprojektować jej konstruktor kopiujący, tak aby pra-

widłowo obsługiwał wskazywany przez klasę obiekt ��������. Najprawdopodobniej

najprostszym sposobem będzie zliczanie referencji do niego, podobnie jak w przypadku

szablonu � �����, o którym można przeczytać w podrozdziale „Zagadnienie 50.”.

Tak naprawdę, na potrzeby tego podrozdziału należy jedynie zadbać o właściwe zacho-

wanie konstruktora kopiującego. W końcu funktory są zawsze kopiowane (czyli prze-

kazywane przez wartość) w momencie przekazywania ich do i z funkcji, a to oznacza

dwie rzeczy: muszą być małe i monomorficzne.

Zagadnienie 39.Predykaty powinny byćfunkcjami czystymiZagadnienie 39. Predykaty powinny być funkcjami czystymi

Obawiam się, że będziemy musieli zacząć od zdefiniowania pewnych pojęć.

Page 9: STL w praktyce. 50 sposobów efektywnego wykorzystania

Zagadnienie 39. Predykaty powinny być funkcjami czystymi 209

� Predykat to funkcja zwracająca wartość typu ���� (lub inną dającą się łatwo

przełożyć na ����). Predykaty są funkcjami często wykorzystywanymi

w STL. Są to, na przykład, funkcje porównujące w standardowych

kontenerach asocjacyjnych; wykorzystują je również (pobierają jako

parametry) różnego rodzaju algorytmy sortujące, a także algorytmy typu

���������. Opis algorytmów sortujących znajduje się w podrozdziale

„Zagadnienie 31.”.

� Funkcje czyste to funkcje, których wartość zwracana zależy wyłącznie

od wartości jej parametrów. Jeżeli ��� jest funkcją czystą, a � i � są obiektami,

to wartość zwracana przez tę funkcję może zmienić się wyłącznie

w przypadku zmiany w obiekcie � lub �.

W języku C++ dane, których używa funkcja czysta, są albo przekazywane

jako parametry albo pozostają niezmienne w czasie życia funkcji (oznacza

to, że muszą być zadeklarowane jako �����). Jeżeli funkcja czysta

wykorzystywałaby dane zmieniające się między poszczególnymi jej

wywołaniami, wtedy kolejne wywołania z tymi samymi parametrami mogłyby

zwracać różne wartości, a to byłoby niezgodne z definicją funkcji czystej.

Powinno to wystarczyć do wyjaśnienia dlaczego predykaty powinny być funkcjami

czystymi. Teraz muszę jedynie Cię przekonać, że ta rada ma solidne podstawy. W zwią-

zku z tym będę musiał wyjaśnić jeszcze jedno pojęcie.

� Klasa-predykat jest klasą-funktorem, w której funkcja ��������� jest

predykatem, co znaczy, że zwraca wartości �� lub ���� albo wartość,

którą można bezpośrednio przekształcić na wartość logiczną. Jak można

się domyślić, w miejscach, w których STL oczekuje podania predykatu,

można podać albo rzeczywisty predykat albo obiekt-predykat.

To wszystko. Teraz mogę zacząć udowadniać, że naprawdę warto przestrzegać porad

przedstawionych w tym podrozdziale.

W podrozdziale „Zagadnienie 38.” wyjaśniłem, że funktory przekazywane są przez wa-

rtość, dlatego należy tak je budować, aby można je było łatwo kopiować. W przypadku

funktorów będących predykatami istnieje jeszcze jeden powód takiego projektowania.

Algorytmy mogą pobierać kopie funktorów i przechowywać je przez pewien czas, za-

nim ich użyją. Jak można się spodziewać, implementacje niektórych algorytmów oczy-

wiście korzystają z tej możliwości. Efektem takiego stanu rzeczy jest fakt, że funkcje

predykatów muszą być funkcjami czystymi.

Aby móc udowodnić te twierdzenia, załóżmy, że budując klasę nie zastosujemy się do

nich. Przyjrzyjmy się poniższej (źle zaprojektowanej) klasie-predykatowi. Niezależnie

od przekazywanych jej parametrów zwraca ona wartość �� tylko raz — przy trze-

cim wywołaniu. W pozostałych przypadkach zwraca wartość ����.

�� ���9 �:����� �)������������������������������������� ���+, ( ��������-./+�� 3����������������� �%�"�������?��(���������*��������"��� �3��� ��� �$� �%�� ����3������)��9 �:����� ��)����; ����.��*2���������������3 ��� �3 ��������3�����; ��������������� ��������?��(�5�

Page 10: STL w praktyce. 50 sposobów efektywnego wykorzystania

210 Rozdział 6. ♦ Funktory, klasy-funktory, funkcje i inne

��*����������EE����; �����<<�F���2���� �)������������; �����2�

Przypuśćmy, że chcielibyśmy użyć tej klasy do usunięcia trzeciego elementu wektora

���� !�"��#:

������?��(��������������������������������6�����$��������� !������///���������������������������������������$��$ �������6���/�� ��������'���*-&���������������������G���������������������������������*-������������������������������ ���+, ( ��������FH/+�� 3��������������������������������������������������"��� �3����%��D���������������������"��#��������������������D�$��"��$�3���� ��������������"������������/������

Powyższy kod wygląda zupełnie przyzwoicie, jednak w wielu implementacjach STL

usunie on nie tylko trzeci, ale i szósty element wektora.

Aby zrozumieć, jak jest to możliwe, dobrze jest poznać częsty sposób implementacji

funkcji ��������. Należy pamiętać, że funkcja �������� nie musi być implemen-

towana w ten sposób:

���� ��%��� ��������� ����%��� ���:����� �������� �����������"������ �����(���������� ��������#�������� ��*����(���<�"�����"��(���������������"���(���<<��������������(����������*���������� �����0�<���(�����������������������%��"EE��0���������(���������22

Szczegóły podanego kodu nie są istotne, jednak należy zwrócić uwagę, że predykat

� jest przekazywany najpierw do funkcji ���������, a następnie do funkcji ������$

�������. W obu przypadkach � jest przekazywany do tych algorytmów przez wartość

(czyli kopiowany). Teoretycznie nie musi to być prawdą, jednak w praktyce najczęściej

jest. Więcej informacji na ten temat znajdziesz w podrozdziale „Zagadnienie 38.”.

Początkowe wywołanie funkcji �������� (w kodzie klienta, związane z próbą usu-

nięcia trzeciego elementu wektora �%) tworzy anonimowy obiekt klasy ���������

zawierający wewnętrzną składową ��������� inicjowaną wartością zero. Ten obiekt

(wewnątrz funkcji �������� nazywa się �) jest kopiowany do funkcji ���������,

w związku z czym ona również otrzymuje obiekt klasy ��������� z wyzerowaną

zmienną ���������. Funkcja ��������� „wywołuje” ten obiekt tak długo, aż zwróci

wartość ��, czyli trzykrotnie, a następnie przekazuje sterowanie do funkcji ����

����. Funkcja �������� wznawia swoje działanie i w końcu wywołuje funkcję

�������������, przekazując jej kolejną kopię predykatu �. Jednak wartość

zmiennej ��������� w obiekcie � ma nadal wartość zero. Funkcja ��������� nigdy

Page 11: STL w praktyce. 50 sposobów efektywnego wykorzystania

Zagadnienie 39. Predykaty powinny być funkcjami czystymi 211

nie wywoływała obiektu �, a jedynie jego kopię. W efekcie trzecie wywołanie przez

funkcję ������������� podanego jej predykatu również zwróci wartość ��. Oto

dlaczego funkcja �������� usunie z wektora �% dwa elementy zamiast jednego.

Najprostszym sposobem na uniknięcie tego rodzaju problemów jest deklarowanie

w klasach-predykatach funkcji ��������� jako �����. W tak zadeklarowanych funk-

cjach kompilator nie pozwoli zmienić wartości składników klasy:

�� ���� �:����� �)����������� �%�"�������?��(���������*������)����������� ��������?��(�5���������*����������EE����; �����<<�F������������4D�I���"��$�3 �#�%���������2�������������������������������������������J� ������� !�� �� ����$4 ���%�#���///2�

Ze względu na to, że opisany problem można rozwiązać w tak prosty sposób, byłem

bliski nazwania tego zagadnienia „W klasach-predykatach stosujmy ���������

typu �����”. Niestety, takie rozwiązanie nie jest wystarczające. Nawet funkcje skła-

dowe oznaczone jako ����� mogą używać zmiennych pól klasy, niestałych lokal-

nych obiektów statycznych, niestałych statycznych obiektów klas, niestałych

obiektów w zakresie przestrzeni nazw i niestałych obiektów globalnych. Dobrze za-

projektowana klasa-predykat gwarantuje, że funkcje jej operatora ��������� są nie-

zależne od tego rodzaju obiektów. Zadeklarowanie ��������� jako ����� jest ko-

nieczne dla uzyskania właściwego zachowania klasy, jednak nie jest wystarczające.

Co prawda wystarczyłoby tak zmodyfikować składowe, żeby nie wpływały na wynik

predykatu, jednak dobrze zaprojektowany ��������� wymaga czegoś więcej —

musi być funkcją czystą.

Zaznaczyłem już, że w miejscach, w których STL oczekuje funkcji predykatu, za-

akceptowany zostanie również obiekt klasy-predykatu. Ta zasada obowiązuje również

w przeciwnym kierunku. W miejscach, w których STL oczekuje obiektu klasy-

predykatu, zaakceptowana zostanie również funkcja-predykat (prawdopodobnie zmody-

fikowana przez funkcję ������ — zobacz „Zagadnienie 41.”). Zostało już udowod-

nione, że funkcje ��������� w klasach-predykatach muszą być funkcjami czysty-

mi, co w połączeniu z powyższymi stwierdzeniami oznacza, że funkcje-predykaty

również muszą być funkcjami czystymi. Poniższa funkcja nie jest aż tak złym predy-

katem jak obiekty tworzone na podstawie klasy ���������, ponieważ jej zastoso-

wanie wiąże się z istnieniem tylko jednej kopii zmiennej stanu, jednak i ona narusza

zasady tworzenia predykatów:

����� ��#��9 �:����� �������?��(�5�������?��(�5�*��� ����������; �����<�.�������������K��I�K��I�K��I�K��I�K��I�K��I�K��I�K��I��������EE����; �����<<�F��������������:���%$ %�������%��%!�"��$�3 �����%�%���2���������������������������������������� � $���"��$�3������� 3D�� ��

Niezależnie od tego, w jaki sposób tworzysz swoje predykaty, powinny one zawsze być

funkcjami czystymi.

Page 12: STL w praktyce. 50 sposobów efektywnego wykorzystania

212 Rozdział 6. ♦ Funktory, klasy-funktory, funkcje i inne

Zagadnienie 40.Klasy-funktory powinny być adaptowalneZagadnienie 40. Klasy-funktory powinny być adaptowalne

Przypuśćmy, że mamy listę wskaźników !��"�& i funkcję określającą, czy dany wskaźnik

identyfikuje interesujący nas element.

����?��(�������(�:������������������(�����?��(�������

Jeżeli chcielibyśmy znaleźć pierwszy wskaźnik na interesujący nas element, można by

to zrobić w prosty sposób:

����?��(���))��� �����<�"�����"���(�:��/��(��������(�:��/���������������������������������������������������(���"���I<����(�:��/������*���///���������������������������������������� �3�����D� ������������%�2������������������������������������������������3D�%��� ����$ �%� �%����������

Jeżeli jednak będziemy chcieli uzyskać pierwszy wskaźnik na element nas nieinteresu-

jący, ten najbardziej oczywisty sposób nawet się nie skompiluje.

����?��(���))��� �����<��"�����"���(�:��/��(��������(�:��/������������������1�����������(�������������4D�I������$������3����7

W takim przypadku należy funkcję ���������" przekazać najpierw do funkcji

��������, a dopiero potem do adaptora ������.

����?��(���))��� �����<���"�����"���(�:��/��(��������(�:��/������������������L���'�������������(��������� $�3����������"���I<����(�:��/������*���///������������������������ �3�����D� ������������%�2�����������������������������������3D�%��� ����$ �%� �%����������

Tutaj rodzi się kilka pytań. Dlaczego konieczne jest zastosowanie funkcji �������� na

funkcji ���������"�� przed przekazaniem jej do ������? Co i jak robi funkcja

��������, że umożliwia skompilowanie powyższego kodu?

Odpowiedź na te pytania jest nieco zaskakująca. Funkcja �������� udostępnia jedynie

kilka deklaracji �����. To wszystko. Są to deklaracje wymagane przez adaptor ������

i z tego powodu bezpośrednie przekazanie funkcji ���������"�� do ������ nie bę-

dzie działać. Symbol ���������"�� jest jedynie prostym wskaźnikiem na funkcję,

dlatego brakuje mu deklaracji wymaganych przez adaptor ������.

W STL znajduje się wiele innych komponentów tworzących podobne wymagania. Każ-

dy z czterech podstawowych adaptorów funkcji (������, ������, ��������� i ���������)

wymaga istnienia odpowiednich deklaracji �����, tak jak i wszystkie inne niestan-

dardowe, ale zgodne z STL adaptory tworzone przez różne osoby (na przykład te two-

rzone przez firmy SGI lub Boost — zobacz „Zagadnienie 50.”). Funktory zawierające

te wymagane deklaracje ����� nazywane są adaptowalnymi, natomiast funkcje ich

Page 13: STL w praktyce. 50 sposobów efektywnego wykorzystania

Zagadnienie 40. Klasy-funktory powinny być adaptowalne 213

nieposiadające nazywane są nieadaptowalnymi. Adaptowalnych funktorów można

używać w znacznie większej ilości kontekstów niż nieadaptowalnych, dlatego, gdy

tylko to możliwe, należałoby budować funktory adaptowalne. Taka operacja nie kosz-

tuje wiele, a może znacznie ułatwić pracę użytkownikom Twoich klas-funktorów.

Zapewne już się denerwujesz, że cały czas mówię o „odpowiednich deklaracjach ��$

���”, ale nigdy nie określam, jakie to deklaracje. Są to deklaracje: �"��������,

������"��������, �������"�������� i ��������. Niestety, życie nie jest

całkiem proste, ponieważ w zależności od rodzaju klasy-funktory powinny udostęp-

niać różne zestawy tych nazw. Tak naprawdę, jeżeli nie tworzysz własnych adapto-

rów (a tego w tej książce nie będziemy opisywać), nie musisz znać tych deklaracji.

Wynika to z faktu, że najprostszym sposobem udostępnienia tych deklaracji jest

odziedziczenie ich po klasie bazowej, a właściwie po bazowej strukturze. Klasy-

funktory, w których ��������� przyjmuje jeden argument, powinny być wywo-

dzone z ���''�������������, natomiast klasy-funktory, w których ���������

przyjmuje dwa argumenty, powinny być wywodzone z ���''��������������.

Należy pamiętać, że ������������� i �������������� to szablony, dlatego nie moż-

na bezpośrednio po nich dziedziczyć, ale dziedziczyć po wygenerowanych przez nie

strukturach, a to wymaga określenia typów argumentów. W przypadku �������������

musisz określić typ parametru pobieranego przez ��������� Twojej klasy funkto-

ra, a także typ jego wartości zwracanej. W przypadku �������������� konieczne jest

określenie trzech typów: pierwszego i drugiego parametru ��������� oraz zwraca-

nej przez niego wartości.

Poniżej podaję kilka przykładów:

���� ��%��� ���8��� ���M���8#���#���)���&��� ���%%���� '��������)������ &�����*���� �)�������8�#���#����

������)��M���8#���#��������85�#���#���������������� ��������?��(�5���������///2������?��(�K ��;��� ��)�����%%&���� '��������)������ )������ &�����*����������� ��������?��(�5��#��������?��(�5��#��������2�

Proszę zauważyć, że w obydwu przypadkach typy przekazywane do �������������

i �������������� są identyczne z typami pobieranymi i zwracanymi przez ���$

������ danej klasy funktora. Troszkę dziwny jest tylko sposób przekazania typu warto-

ści zwracanej przez operator jako ostatniego parametru szablonów �������������

lub ��������������.

Zapewne nie uszło Twojej uwadze, że (��) � ��� jest klasą, a !��"�*�������$

jest strukturą. Wynika to z faktu, że (��) � ��� ma składowe opisujące jej

wewnętrzny stan (pole � � ���), dlatego naturalną rzeczą jest zastosowanie w takiej

sytuacji klasy. Z kolei !��"�*������� nie przechowuje informacji o stanie, dlatego

Page 14: STL w praktyce. 50 sposobów efektywnego wykorzystania

214 Rozdział 6. ♦ Funktory, klasy-funktory, funkcje i inne

nie ma potrzeby ukrywania w niej jakichkolwiek danych. Autorzy klas, w których nie

ma elementów prywatnych, często deklarują takie klasy jako struktury. Prawdopo-

dobnie chodzi o możliwość uniknięcia wpisywania w takiej klasie słowa kluczowego

������. Wybór deklaracji takich klas jako klasy lub struktury zależy wyłącznie od prefe-

rencji programisty. Jeżeli cały czas próbujesz wykuć własny styl, a chciałbyś naślado-

wać zawodowców, zauważ, że w bibliotece STL wszystkie klasy nieposiadające stanu

(na przykład ��� )#, ���� )# itd.) deklarowane są jako struktury.

Przyjrzyjmy się jeszcze raz strukturze !��"�*�������:

�����?��(�K ��;��� ��)������������))��� �%�"�������)�������)�������������*������������ �������� )��������#�������� )��������#��������2�

Typ przekazywany do szablonu �������������� to !��"�, mimo że ���������

pobiera argumenty typu ����� !��"�+. Zazwyczaj niebędące wskaźnikami typy prze-

kazywane do szablonu ������������� lub �������������� odzierane są ze znaczni-

ków ����� i referencji. (Nie pytaj dlaczego. Powód nie jest ani dobry, ani interesują-

cy. Jeżeli jednak nadal bardzo chcesz wiedzieć, napisz program testowy i nie usuwaj

w nim tych znaczników, a następnie przeanalizuj wynik działania kompilatora. Jeżeli

po tym wszystkim nadal będziesz zainteresowany tematem, zajrzyj na stronę boost.org

(zobacz „Zagadnienie 50.”) i przejrzyj na niej teksty dotyczące adaptorów funktorów

i cech wywołań).

W przypadku gdy ��������� pobiera wskaźniki jako parametry, opisane wyżej za-

sady ulegają zmianie. Poniżej podaję strukturę podobną do !��"�*�������, która

posługuje się wskaźnikami !��"�&:

�����:�?��(�K ��;��� ��)������������))��� �%�"������������ )������������� )��������������*������������ �������� )��������#�������� )��������#��������2�

W tym przypadku typy przekazywane do �������������� są identyczne z typami

pobieranymi przez ���������. Wszystkie klasy-funktory pobierające lub zwracające

wskaźniki obowiązuje zasada nakazująca przekazywanie do ������������� lub ����$

���������� dokładnie takich samych typów, jakie pobiera lub zwraca ���������.

Nie możemy zapomnieć, z jakiego powodu snujemy te opowieści o klasach bazowych

������������� i �������������� — dostarczają one deklaracji ����� wymaga-

nych przez adaptory funktorów, dlatego dziedziczenie po tych klasach pozwala two-

rzyć funktory adaptowalne. To z kolei pozwala na pisanie tego rodzaju rzeczy:

����?��(������(���///����?��(��))����������� ����L�<������������������ 3��3���� ������������"�����"���(��/���(��������(��/��������������$6�%���������$����%4�������������1�M���8���#�������L.�����������������(����� �� ���L.�����������������������������������������������������$�����$����� ��%�?��(�������������� ��������������?��(��))��� ����H�<�������������������������� 3��3���������%���������"�����"���(��/��(�������(��/������������������ 3��3D�%���7�������+�+

Page 15: STL w praktyce. 50 sposobów efektywnego wykorzystania

Zagadnienie 41. Po co stosować funkcje ptr_fun, mem_fun i mem_fun_ref? 215

����������&���2���?��(�K ��;��� ����������������������D�$������� �� ������������������������������������������������������"����� �%����������������������������������������������������������?��(�K ��;��� ��

Gdyby nasze klasy-funktory nie zostały wywiedzione z klasy ������������� lub ����$����������, powyższe przykłady nawet by się nie skompilowały, ponieważ funkcje������ i ��������� działają tylko z funktorami adaptowalnymi.

W STL funktory modelowane są podobnie do funkcji w języku C++, które mają tylkojeden zestaw typów parametrów i jedną wartość zwracaną. W efekcie przyjmuje się,że każda klasa-funktor ma tylko jedną funkcję ���������, której parametry i wartośćzwracana powinny zostać przekazane do klas ������������� lub ��������������(wynika to z omówionych właśnie zasad dla typów wskaźnikowych i referencyjnych).A z tego wynika z kolei, że nie powinno się łączyć funkcjonalności struktur !��"�$*������� i ��!��"�*������� przez utworzenie jednej klasy mającej dwiefunkcje ���������. Jeżeli utworzysz taką klasę, będzie ona adaptowalna tylko wjednej wersji (tej zgodnej z parametrami przekazywanymi do ��������������). Jakmożna się domyślać, funktor adaptowalny tylko w połowie równie dobrze mógłby niebyć adaptowalny w ogóle.

W niektórych przypadkach utworzenie możliwości wywołania funktora w wielu for-mach (a tym samym rezygnacja z adaptowalności) ma sens, co opisano w zagadnie-niach: 7., 20., 23. i 25. Należy jednak pamiętać, że tego rodzaju funktory są jedyniewyjątkami od zasady. Adaptowalność to cecha, do której należy dążyć w czasie two-rzenia klas-funktorów.

Zagadnienie 41.Po co stosować funkcje ptr_fun,mem_fun i mem_fun_ref?Zagadnienie 41. Po co stosować funkcje ptr_fun, mem_fun i mem_fun_ref?

O co chodzi z tymi funkcjami? Czasami trzeba ich używać, czasami nie. Co one wła-ściwie robią? Wygląda na to, że czepiają się nazw funkcji jak rzep psiego ogona. Nie-łatwo je wpisać, denerwują w czasie czytania i trudno je zrozumieć. Czy są to artefaktypodobne do przedstawionych w podrozdziałach „Zagadnienie 10.” i „Zagadnienie18.”, czy może członkowie komitetu standaryzacyjnego wycięli nam niemiły dowcip?

Spokojnie, te funkcje mają do spełnienia naprawdę ważne zadania i z całą pewno-ścią nie są dziwacznymi żartami. Jednym z ich podstawowych zadań jest zamaskowaniepewnych niekonsekwencji syntaktycznych języka C++.

Jeżeli, posiadając funkcję ��� i obiekt �, chcielibyśmy wywołać ��� na rzecz � i jeste-śmy poza funkcjami składowymi obiektu �, język C++ pozwala na zastosowanietrzech różnych składni takiego wywołania.

"0����������������������������������������34����� �� 1-�'���� � ������%� �$��������������������������������������������(�%�"��$�3 �"�����3�������D�����$��0/0/"���������������������������������������34����� �� 2-�'���� � ������%� �$��

Page 16: STL w praktyce. 50 sposobów efektywnego wykorzystania

216 Rozdział 6. ♦ Funktory, klasy-funktory, funkcje i inne

������������������������������������������(�%�"��$�3 �"�3�������D�����$�������������������������������������������� �0�3�������$���������"�����3D�������$�/�A�"��������������������������������������34����� �� 5-�'���� � ������%� �$��������������������������������������������(�%�"��$�3 �"�3�������D�����$�������������������������������������������� ���3�����$ C��$����� �����$�0/

Teraz załóżmy, że mamy funkcję sprawdzającą elementy:

�������?��(�5���������������������������� ��� �+�+����3�J��������� ����%���������������������������������������������������� �� �(��3 $��+����4 ���%+

i kontener przechowujący te elementy:

������?��(��������������������������������������#���3��������%

Jeżeli chcielibyśmy sprawdzić wszystkie elementy w �%, moglibyśmy w prosty sposób

wykorzystać funkcję ����� ��:

"���� �#��/��(�������/���������������) *�4���� �� 1-��$������3����7�

Wyobraźmy sobie, że ����� nie jest zwyczajną funkcją, ale funkcją składową klasy

!�"��, co oznacza, że obiekty tej klasy mogą same się sprawdzać:

�� ���?��(��*��������///������������������������������%$���3��� ����� ���������3�J�����///�������������������������������$��#�����������3���������2���������������������������������� �� �%�3���3 $��+����4 ���%+

W świecie doskonałym moglibyśmy zastosować funkcję ����� ��, aby wywołać

funkcję !��"�''����� dla każdego obiektu wektora �%:

"���� �#��/��(�������/��������������5?��(�))����������������������) *�4���� �� 2-������$������3����7�

Jeżeli świat byłby naprawdę doskonały, moglibyśmy również zastosować funkcję

����� ��, żeby wywołać funkcję !��"�''����� w elementach kontenera prze-

chowującego wskaźniki !��"�&:

����?��(�����������������������������������������#���3����$ C��$��� �������%"���� �#���/��(��������/��������������5?��(�))����������������������) *�4���� �� 5-��6����J���7�����$������3��

Pomyślmy jednak, co by się działo w tym świecie doskonałym. W przypadku wywoła-

nia nr 1 wewnątrz funkcji ����� �� wywoływalibyśmy zwykłą funkcję, przekazując

jej obiekt, czyli konieczne byłoby zastosowanie składni nr 1. W przypadku wywoła-

nia nr 2 wewnątrz funkcji ����� �� wywoływalibyśmy metodę pewnego obiektu,

czyli konieczne byłoby zastosowanie składni nr 2. Natomiast w przypadku wywołania

nr 3 wewnątrz funkcji ����� �� wywoływalibyśmy metodę obiektu, do którego od-

wołujemy się poprzez wskaźnik, czyli konieczne byłoby zastosowanie składni nr 3. To

wszystko oznacza, że musiałyby istnieć trzy różne wersje funkcji ����� ��, a świat

nie byłby już tak doskonały.

Page 17: STL w praktyce. 50 sposobów efektywnego wykorzystania

Zagadnienie 41. Po co stosować funkcje ptr_fun, mem_fun i mem_fun_ref? 217

W świecie rzeczywistym istnieje tylko jedna wersja funkcji ����� ��. Zapewne nie-

trudno się domyślić, jak wygląda jej implementacja:

���� ��%��� ���������� ����%��� �������������������"���� �#������� �����(����������� ����������������"�*����#������(���I<����������(��EE�2

Proszę zauważyć, że funkcja ����� �� wykorzystuje do wywoływania funkcji ���

składnię nr 1. Jest to ogólnie przyjęta w STL konwencja, funkcje i funktory są wywo-

ływane za pomocą składni stosowanej dla zwykłych funkcji. To wyjaśnia, dlaczego

można skompilować składnię nr 1, ale składni nr 2 i 3 już nie. Wszystkie algorytmy STL

(w tym również ����� ��) wykorzystują składnię nr 1, wobec czego jedynie wywoła-

nie nr 1 jest z nią zgodne.

Teraz powinno być już jasne, dlaczego istnieją funkcje �������� i ����������.

Sprawiają one, że funkcje składowe (które powinny być wywoływane za pomocą

składni nr 2 lub 3) są wywoływane za pomocą składni nr 1.

Funkcje �������� i ���������� wykonują swoje zadania w bardzo prosty sposób,

spojrzenie na deklarację jednej z nich powinno całkowicie wyjaśnić zagadkę. Tak na-

prawdę są to szablony funkcji, istnieje ich kilka wersji różniących się ilością parame-

trów i tym, czy przystosowywana funkcja jest oznaczona jako ����� czy nie. Aby po-

znać sposób działania tych funkcji, wystarczy zobaczyć kod jednej z nich:

���� ��%��� ���N��%��� ���;�������������$� � �3 �"��$�3������"����� ������ ����%�#����"����N��;����������������������������3 $�������"��$�3���$4 ���%�#���'��N;))���"��������������������������������� 3D�%�#�J ��%�#�� � ���6�������������������������������������������;���$� � �� �N���� �� !���� � � ��������������������������������������������$ �%� ��3�"��$�3���$4 ����3

Funkcja �������� pobiera wskaźnik na metodę (���) i zwraca obiekt typu ��������.

Jest to klasa-funktor przechowująca wskaźnik na metodę i udostępniająca ���$

������ wywołujący tę metodę na rzecz obiektu podanego jako parametr tego opera-

tora. Na przykład w kodzie:

����?��(�����������������������������������������3 $��%J�3///"���� �#���/��(��������/�����������������'���5?��(�))��������������� ����7��$������3�

funkcja ����� �� otrzymuje obiekt typu �������� przechowujący wskaźnik na funk-

cję !��"�''���. Dla każdego wskaźnika !��"�& z ��% za pomocą składni nr 1 wy-

woływany jest obiekt ��������, a ten natychmiast wywołuje funkcję !��"�''�����

zgodnie ze składnią nr 3.

Ogólnie, funkcja �������� przystosowuje składnię nr 3 wymaganą przy wywołaniach

funkcji !��"�''����� za pomocą wskaźnika !��"�& na składnię nr 1 stosowaną przez

funkcję ����� ��, wobec czego nie powinno dziwić, że klasy w rodzaju ���,�����

nazywane są adaptorami obiektów funkcyjnych. W podobny sposób funkcja �������

��� przystosowuje składnię nr 2, generując obiekty-adaptory typu ����������.

Page 18: STL w praktyce. 50 sposobów efektywnego wykorzystania

218 Rozdział 6. ♦ Funktory, klasy-funktory, funkcje i inne

Obiekty tworzone przez funkcje �������� i ���������� pozwalają nie tylko zakła-

dać, że wszystkie funkcje wywoływane są za pomocą tej samej składni, ale również,

podobnie jak obiekty generowanie przez funkcję ��������, udostępniają odpowiednie

deklaracje �����. Na temat tych deklaracji była mowa w podrozdziale „Zagadnienie

40.”. Dzięki tym wyjaśnieniom powinno być już jasne, dlaczego ten kod się skompiluje:

"���� �#��/��(�������/����������������%��4 �������L�A��$������3����7

a te nie:

"���� �#��/��(�������/������5?��(�))�����������%��4 �������H�A������$������3���7"���� �#���/��(��������/������5?��(�))���������%��4 �������F�A������$������3���7

Wywołanie nr 1 przekazuje w parametrze funkcję, dlatego nie ma konieczności dosto-

sowania składni do wymagań funkcji ����� ��. Algorytm wywoła otrzymaną funk-

cję za pomocą właściwej składni. Co więcej, funkcja ����� �� nie używa żadnej

z deklaracji ����� udostępnianej przez funkcję ��������, więc nie ma potrzeby

przekazywania funkcji ����� za pośrednictwem tej funkcji. Z drugiej strony, udo-

stępnienie tych deklaracji na nic nie wpłynie, więc poniższy kod zadziała tak samo

jak ten podany wyżej.

"���� �#��/��(�������/���������'��������������$������3����7���� ��� 4 �������������������������������������������������������������3 $��%��4 �������L

Jeżeli teraz już nie wiesz, kiedy stosować funkcję ��������, a kiedy nie — możesz

używać jej przy każdym przekazywaniu funkcji do komponentu STL. Bibliotece nie

zrobi to żadnej różnicy, nie wpłynie też na wydajność programu. Najgorsze, co może

Ci się zdarzyć, to zdziwienie na twarzy osoby czytającej Twój kod pojawiające się

w momencie napotkania nadmiarowego wywołania funkcji ��������. Na ile będzie

Ci to przeszkadzać? Chyba zależy to od Twojej wrażliwości na zdziwione twarze.

Inną strategią dotyczącą stosowania funkcji �������� jest stosowanie jej tylko w przy-

padku, gdy zostaniemy do tego zmuszeni. Oznacza to, że w przypadkach, w których

konieczna będzie obecność deklaracji �����, kompilacja programu zostanie wstrzy-

mana. Wtedy będzie trzeba uzupełnić kod o wywołanie funkcji ��������.

W przypadku funkcji �������� i ���������� mamy zupełnie inną sytuację. Ich

wywołanie jest konieczne przy każdym przekazywaniu metody do komponentu STL,

ponieważ poza udostępnianiem potrzebnych deklaracji ����� dostosowują one

składnię stosowaną przy wywoływaniu metod do składni stosowanej w całej bibliote-

ce STL. Brak odpowiedniej funkcji przy przekazywaniu wskaźników na metody unie-

możliwi poprawną kompilację programu.

Pozostało nam omówić nazwy adaptorów metod. Okazuje się, że natkniemy się tutajna historyczny już artefakt. Gdy okazało się, że potrzebne są takie adaptory, twórcybiblioteki STL skupili się na kontenerach wskaźników (W świetle ograniczeń, jakimiobarczone są te kontenery — opisano je w zagadnieniach 7., 20. i 33. — pewnymzaskoczeniem może być fakt, że to właśnie kontenery wskaźników obsługują klasy poli-morficzne, podczas gdy kontenery obiektów ich nie obsługują). Zbudowano adaptordla metod i nazwano go ��������. Później okazało się, że potrzebny jest jeszcze adaptor

Page 19: STL w praktyce. 50 sposobów efektywnego wykorzystania

Zagadnienie 42. Upewnij się, że less<t>() oznacza operator<() 219

dla kontenerów obiektów, więc nową funkcję nazwano ����������. Nie jest to zbyteleganckie, ale takie rzeczy się zdarzają. Pewnie każdemu zdarzyło się nadać kompo-nentowi nazwę, którą później trudno było dostosować do nowych warunków.

Zagadnienie 42.Upewnij się, że less<t>()oznacza operator<()Zagadnienie 42. Upewnij się, że less<t>() oznacza operator<()

Jak wszyscy dobrze wiemy, „widgety” mają swoją masę i maksymalną prędkość:

�� ���?��(��*������)��///�����������(#���������������� 0'�������������///2�

Oczywiście naturalnym sposobem sortowania widgetów jest sortowanie ich według ma-sy, dlatego operator mniejszości ( ) w tym przypadku powinien wyglądać następująco:

��������� ��������?��(�5��#��������?��(�5��#��*���������#�/���(#�����#�/���(#��2

Przypuśćmy jednak, że chcielibyśmy utworzyć kontener typu ������� !��"�#, w któ-rym widgety sortowane byłyby według ich prędkości maksymalnej. Wiemy już, żedomyślną funkcją porównującą kontenera ������� !��"�# jest ��� !��"�#��.Wiemy też, że domyślnie funkcja ta tylko wywołuje operator mniejszości ( ). Wyglą-da na to, że jedynym sposobem na posortowanie kontenera ������� !��"�# wedługprędkości maksymalnej jego elementów jest zniszczenie połączenia między funkcją��� !��"�#�� a operatorem mniejszości ( ). Można to zrobić, nakazując funkcji��� !��"�#�� kontrolę jedynie prędkości maksymalnej podawanych jej widgetów:

���� ��������������������������������������3�������3 ��� �3 �$� �%�������))�����?��(��)��������������������))��������D� � ���$� �D�?��(����������������������������������������������� �%��3�����" ��%����%�4����))��� �%�"��������?��(��������������������������?��(��������������������������������*����������� ��������?��(�5��#��������?��(�5��#���������*������������#�/� 0'���������#�/� 0'��������22�

Nie wygląda to na zbyt dobrą radę i taką nie jest, chyba jednak nie z powodu, o którymmyślisz. Czyż nie jest zaskakujące, że ten kod w ogóle się kompiluje? Wielu programi-stów zauważy, że nie jest on tylko zwykłą specjalizacją szablonu, ale jest specjalizacją

Page 20: STL w praktyce. 50 sposobów efektywnego wykorzystania

220 Rozdział 6. ♦ Funktory, klasy-funktory, funkcje i inne

szablonu w przestrzeni nazw ���. Będą oni pytać: „Czy przestrzeń ��� nie powinna byćświęta? Dostępna tylko dla twórców biblioteki i będąca poza zasięgiem zwykłychprogramistów? Czy kompilatory nie powinny zakazywać grzebania w pracach twór-ców C++?”.

Zazwyczaj próby modyfikacji komponentów w przestrzeni ��� są rzeczywiście za-bronione, a próby ich wykonania kończą się niezdefiniowanym zachowaniem aplika-cji. Jednak w niektórych przypadkach takie prowizorki są dopuszczalne. W szczegól-ności możliwe jest specjalizowanie szablonów do obsługi typów zdefiniowanych przezużytkownika. Niemal zawsze są inne, lepsze wyjścia niż zabawa z szablonami z prze-strzeni ���, jednak czasami wiele argumentów przemawia właśnie za taką opcją. Naprzykład autorzy klas inteligentnych wskaźników chcieliby, aby ich klasy zachowy-wały się jak zwyczajne wskaźniki, dlatego w takich klasach często spotyka się spe-cjalizacje funkcji ���''�����. Poniższy kod jest częścią klasy � ����� z bibliotekiBoost. Jest to właśnie inteligentny wskaźnik, o którym można przeczytać w podroz-działach „Zagadnienie 7.” i „Zagadnienie 50.”.

� ���� ������*������ ��%��� ���8�����������������������������3 ��� �3 �"��$�3����))���������������������))�# �������8���)���������� �$� �%�����))�# �������8��������������������������������������������������������������G�� ����������� �%�"�����������))�# �������8���������������������������))�# �������8��������3���%��� �$� � ���������������������������*������������������� ��� ���� ���+, ( ��������-./+��������������� ������������))�# �������8�5� �������������������������������))�# �������8�5�������������*������������������8��� /(����/(������������# ������))(����� � �����2���������������������������������������������%$4%���$ C��$��$6�%�3�������������������������������������������������������$����# ��������2�2

Powyższa implementacja nie jest pozbawiona sensu — z cała pewnością nie tworzyżadnych niespodzianek, ponieważ taka specjalizacja zapewnia jedynie, że sortowaniezwykłych wskaźników i wskaźników inteligentnych odbywa się w ten sam sposób.Niestety, nasza specjalizacja funkcji ����� w klasie !��"� może przysporzyć kilkuniemiłych niespodzianek.

Programistom C++ można wybaczyć, że pewne rzeczy uznają za oczywiste. Na przy-kład zakładają oni, że konstruktory kopiujące rzeczywiście kopiują obiekty (jak wy-kazano w podrozdziale „Zagadnienie 8.”, niedopełnienie tej konwencji może prowa-dzić do zadziwiających zachowań programu). Zakładają też, że pobierając adresobiektu, otrzymają wskaźnik na ten obiekt (w podrozdziale „Zagadnienie 18.” opisanoproblemy, jakie powstają, gdy nie jest to prawdą). Przyjmują za oczywiste, że adapto-ry takie jak ��������� i ������ można stosować z funktorami (podrozdział „Zagad-nienie 40.” opisuje problemy wynikające z niespełnienia tego założenia). Zakładająrównież, że operator dodawania (-) dodaje (za wyjątkiem ciągów znaków, ale opera-tor ten jest już od dawna używany do łączenia ciągów), operator odejmowania ($)odejmuje, a operator porównania (..) porównuje obiekty. W końcu przyjmują za oczy-wiste, że zastosowanie funkcji ����� jest równoznaczne z zastosowaniem operatoramniejszości ( ).

Page 21: STL w praktyce. 50 sposobów efektywnego wykorzystania

Zagadnienie 42. Upewnij się, że less<t>() oznacza operator<() 221

Operator mniejszości jest nie tylko domyślną implementacją funkcji �����, ale we-

dług założeń programistów definiuje on sposób działania tej funkcji. Jeżeli funkcji

����� nakażemy robić coś innego niż wywołanie operatora mniejszości ( ), pogwałci-

my w ten sposób oczekiwania programistów. To całkowicie zaprzecza „zasadzie

najmniejszego zaskoczenia” — takie działanie jest nieprzyzwoite, bezduszne i złe. Tak

robić nie wolno.

Nie wolno tego robić, szczególnie dlatego, że nie ma ku temu powodów. W bibliotece

STL nie ma miejsca, w którym nie można by zastąpić funkcji ����� innym rodzajem

porównania. Wracając do naszego początkowego przykładu (czyli kontenera �����$

�� !��"�# sortowanego według prędkości maksymalnej), aby osiągnąć zamierzony

cel, musimy jedynie utworzyć klasę-funktor wykonującą potrzebne nam porównanie.

Można ją nazwać prawie dowolnie, jednak na pewno nie można zastosować nazwy

�����. Oto przykład takiej klasy:

�����M 0'����;��� ��)������������ �%�"�������?��(���?��(���������*������������� ��������?��(�5��#��������?��(�5��#�����������*�������������#�/� 0'���������#�/� 0'����������22�

Tworząc nasz kontener, jako funkcję porównującą wykorzystamy klasę (��/�����$

�� i w ten sposób unikniemy wykorzystania domyślnej funkcji porównującej, czyli

��� !��"�#��.

�������?��(���M 0'����;��� �������(���

Powyższy kod wykonuje dokładnie te operacje. Tworzy on kontener typu �������

elementów !��"� posortowanych zgodnie z definicją zawartą w klasie (��/�������.

Porównajmy to z kodem:

�������?��(������(���

Tworzy on kontener typu ������� elementów !��"� posortowanych w sposób

domyślny. Oznacza to, że do porównań wykorzystywana będzie funkcja ��� !��"�#,

jednak każdy programista założy w tym miejscu, że odpowiadać za to będzie operator

mniejszości ( ).

Nie utrudniajmy życia innym, zmieniając domyślną definicję funkcji �����. Niech

każde zastosowanie funkcji ����� (bezpośrednie lub pośrednie) wiąże się z wykorzy-

staniem operatora mniejszości. Jeżeli chcesz posortować obiekty za pomocą innego

kryterium, zbuduj do tego specjalną klasę-funktor i nie nazywaj jej �����. To prze-

cież takie proste.