STL w praktyce 50 sposobow efektywnego wykorzystania 2

background image

Wydawnictwo Helion
ul. Chopina 6
44-100 Gliwice
tel. (32)230-98-63

e-mail: helion@helion.pl

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 CENNIK

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

background image

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

background image

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

background image

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

background image

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ów
do 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.

background image

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 okazuje

się, ż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, wobec
czego 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 przez

referencję.

&'#()

%"*+,(-./+

$

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 przypadku
przekazywania 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 jest
niemal zawsze prawdą.

Funktory muszą być przekazywane przez wartość, dlatego to na Tobie spoczywa ciężar
sprawdzenia, 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

background image

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 typu
klasy bazowej zostaje obcięty, czyli w czasie kopiowania usunięte będą z niego
odziedziczone elementy. W podrozdziale „Zagadnienie 3.” opisano inny problem, jaki
rozczłonkowanie (ang. slicing) tworzy w czasie stosowania biblioteki STL.

Unikanie rozczłonkowania obiektów i uzyskiwanie dobrej wydajności są oczywiście
bardzo 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 ze
zwykł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ść przekazywania
takich 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%"$%

)

853"$3

(

///4$CD

4%

2

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

!$3

"#$%

& '!(%"$3$%9:;

%

)*+3%$

,+3%$9:;

background image

208

Rozdział 6.

♦ Funktory, klasy-funktory, funkcje i inne

---."#$+$%"D!

$%

!+

"#$!+$9:;747%#

/+

%8

9:;)$"3

%"8*$%9:;

)

"#$!+D%$#%

$79:;

)

!$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 czystymi

Zagadnienie 39. Predykaty powinny być funkcjami czystymi

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

background image

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:);.*2333;

?(5

background image

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<(

%"EE0(

2

2

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

background image

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;<<F4DI"$3#%

2J! $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

*

;<.KIKIKIKIKIKIKIKI

EE;<<F:%$%%%!"$3%%

2$"$33D

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

background image

212

Rozdział 6.

♦ Funktory, klasy-funktory, funkcje i inne

Zagadnienie 40.
Klasy-funktory powinny być adaptowalne

Zagadnienie 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<(:/*

///3D%

23D%$%%

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(4DI$37

W takim przypadku należy funkcję

"

przekazać najpierw do funkcji

, a dopiero potem do adaptora

.

?())<

""(:/((:/

L'($3

"I<(:/*

///3D%

23D%$%%

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

background image

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

M8##)&%% ')&*

)

8##

)

M8##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

background image

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<33

""(/((/$6%$%4

1M8#L.( L.

$$%

?(CTIWOGPV[MQPUVTWMVQTC

?())H<33%

""(/((/33D%7++

background image

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ą tylko
jeden 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 dwie

funkcje

. Jeżeli utworzysz taką klasę, będzie ona adaptowalna tylko w

jednej wersji (tej zgodnej z parametrami przekazywanymi do

). Jak

można się domyślać, funktor adaptowalny tylko w połowie równie dobrze mógłby nie
być 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ą jedynie
wyją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 artefakty
podobne do przedstawionych w podrozdziałach „Zagadnienie 10.” i „Zagadnienie
18.”, 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 zamaskowanie
pewnych 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 zastosowanie

trzech różnych składni takiego wywołania.

"0341-'%$

(%"$3"3D$0/

0/"342-'%$

background image

216

Rozdział 6.

♦ Funktory, klasy-funktory, funkcje i inne

(%"$3"3D$

03$"3D

$/

A"345-'%$

(%"$3"3D$

3$C$$0/

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

?(5++3J%

(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ę

:

"#/(/) *41-$37

Wyobraźmy sobie, że

nie jest zwyczajną funkcją, ale funkcją składową klasy

!"

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

?(*

///

%$33J

///$#3

2%33$+4 %+

W świecie doskonałym moglibyśmy zastosować funkcję

, aby wywołać

funkcję

!"''

dla każdego obiektu wektora

%

:

"#/(/

5?())) *42-$37

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?())) *45-6J7

$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.

background image

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$43

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$%J3

///

"#/(/

'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

.

background image

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:

"#/(/%4LA$37

a te nie:

"#/(/5?())%4HA$3

7

"#/(/5?())%4FA$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.

"#/(/'$374

3$%4L

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ę tutaj
na historyczny już artefakt. Gdy okazało się, że potrzebne są takie adaptory, twórcy
biblioteki STL skupili się na kontenerach wskaźników (W świetle ograniczeń, jakimi
obarczone są te kontenery — opisano je w zagadnieniach 7., 20. i 33. — pewnym
zaskoczeniem 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 adaptor
dla metod i nazwano go

. Później okazało się, że potrzebny jest jeszcze adaptor

background image

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

219

dla kontenerów obiektów, więc nową funkcję nazwano

. Nie jest to zbyt

eleganckie, 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ż, że
domyś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ług

prę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:

333$%

))?()))D$D?(

%3"%%4

))%"?(

?(

*

?(5#?(5#

*

#/0'#/0'

2

2

Nie wygląda to na zbyt dobrą radę i taką nie jest, chyba jednak nie z powodu, o którym
myś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ą

background image

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łych
programistó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 przez
uż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ą. Na

przykł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 biblioteki

Boost. Jest to właśnie inteligentny wskaźnik, o którym można przeczytać w podroz-
działach „Zagadnienie 7.” i „Zagadnienie 50.”.

*

%833"$3))

))#8)$%))#8

G

%"))#8

))#83%$

*+,(-./+

))#85

))#85

*

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 sortowanie
zwykł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ć kilku

niemił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 adres
obiektu, otrzymają wskaźnik na ten obiekt (w podrozdziale „Zagadnienie 18.” opisano
problemy, 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 operatora

mniejszości (

).

background image

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:

M0';)

%"?(?(*

?(5#?(5#

*

#/0'#/0'

2

2

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

!"#

.

?(M0';(

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.


Wyszukiwarka

Podobne podstrony:
STL w praktyce 50 sposobów efektywnego wykorzystania
STL w praktyce 50 sposobow efektywnego wykorzystania
STL w praktyce 50 sposobow efektywnego wykorzystania stlpra
STL w praktyce 50 sposobow efektywnego wykorzystania 2
STL w praktyce 50 sposobow efektywnego wykorzystania stlpra
STL w praktyce 50 sposobow efektywnego wykorzystania stlpra
106 Efektywnosc wykorzystania Nieznany (2)
Gdzie leży klucz do poprawy efektywności wykorzystania energii elektrycznej w Polsce
Kolmasiak PDądela Wbrane elementy efektywnego wykorzystania
Energia jądrowa i sposoby jej wykorzystania, Fizyka Liceum, różne
w sprawie sposobu nadawania i wykorzystywania znaku zgodności z Polską Normą
052 ROZ w sprawie sposobu nadawania i wykorzystywania znaku
W jaki sposób człowiek wykorzystuje warunki naturalne Australii, Konspekty lekcji
Przewodnik po efektywnym wykorzystaniu zasobów ludzkich (ang)
25 Kardas Analiza efektywnosci wykorzystania maszyn
klucz do poprawy efektywnosci wykorzystan
44 w sprawie sposobu nadawania i wykorzystywania znaku zgodności z Polską Normą
Efektywnosc wykorzyst energii 998 2008

więcej podobnych podstron