IDZ DO
         PRZYK£ADOWY ROZDZIA£

                           SPIS TRE CI   C++. Styl i technika
                                         zaawansowanego
           KATALOG KSI¥¯EK
                      KATALOG ONLINE
                                         programowania
                                         Autor: James O. Coplien
                                         T³umaczenie: Jaromir Senczyk
       ZAMÓW DRUKOWANY KATALOG           ISBN: 83-7361-322-6
                                         Tytu³ orygina³u: Advanced C++
              TWÓJ KOSZYK                Programming Styles and Idioms
                                         Format: B5, stron: 480
                    DODAJ DO KOSZYKA


         CENNIK I INFORMACJE             Zak³adaj¹c znajomo æ podstaw jêzyka C++ ksi¹¿ka ta umo¿liwia programistom
                                         rozwiniêcie zaawansowanych umiejêtno ci programowania poprzez stosowanie styli
                   ZAMÓW INFORMACJE      i idiomów jêzyka C++. Struktura ksi¹¿ki zorganizowana jest wokó³ abstrakcji
                     O NOWO CIACH        wspieranych przez jêzyk C++: abstrakcyjnych typów danych, kombinacji typów
                                         w strukturach dziedziczenia, programowania obiektowego i dziedziczenia wielokrotnego.
                       ZAMÓW CENNIK      W ksi¹¿ce przedstawione zostaj¹ tak¿e te idiomy, które nie znajduj¹ bezpo redniego
                                         wsparcia w jêzyku C++, takie jak wirtualne konstruktory, obiekty prototypów
                                         i zaawansowane techniki odzyskiwania nieu¿ytków.
                 CZYTELNIA               Ksi¹¿ka:
                                            • Przedstawia zalety i potencjalne pu³apki zaawansowanych technik
          FRAGMENTY KSI¥¯EK ONLINE
                                              programowania w jêzyku C++.
                                            • Sposoby efektywnego ³¹czenia abstrakcji jêzyka C++ ilustruje szeregiem krótkich,
                                              ale stanowi¹cych wystarczaj¹cy instrukta¿ przyk³adów.
                                            • Dostarcza wielu praktycznych zasad wykorzystania jêzyka C++ do implementacji
                                              rezultatów projektowania obiektowego.
                                            • Omawia wszystkie w³a ciwo ci edycji 3.0 jêzyka C++, w tym zastosowanie
                                              szablonów w celu wielokrotnego wykorzystania kodu.
                                            • Przedstawia istotne aspekty rozwoju z³o¿onych systemów, w tym projektowanie
                                              bibliotek, obs³ugê wyj¹tków i przetwarzanie rozproszone.
                                         Ksi¹¿ka ta jest wa¿nym podrêcznikiem dla ka¿dego programisty aplikacji lub
                                         programisty systemowego pos³uguj¹cego siê jêzykiem C++.
Wydawnictwo Helion
ul. Chopina 6
44-100 Gliwice
tel. (32)230-98-63
e-mail: helion@helion.pl
Spis treści
                Przedmowa........................................................................................ 9
Rozdział 1. Wprowadzenie ................................................................................. 15
                1.1. Ewolucja języka C++ .................................................................................................15
                1.2. Idiomy jako sposób na zło oność problemów ...........................................................16
                1.3. Obiekty lat 90-tych.....................................................................................................18
                1.4. Projektowanie i język programowania.......................................................................19
                Bibliografia........................................................................................................................20
Rozdział 2. Abstrakcyjne typy danych ................................................................ 21
                2.1. Klasy...........................................................................................................................22
                2.2. Inwersja obiektowa ....................................................................................................25
                2.3. Konstruktory i destruktory .........................................................................................28
                2.4. Funkcje rozwijane w miejscu wywołania ..................................................................32
                2.5. Inicjacja statycznych danych składowych..................................................................34
                2.6. Statyczne funkcje składowe .......................................................................................35
                2.7. Zakresy i słowo kluczowe const.................................................................................36
                2.8. Porządek inicjacji obiektów globalnych, stałych i składowych statycznych .............38
                2.9. Słowo const i funkcje składowe .................................................................................39
                2.10. Wskaźniki funkcji składowych ................................................................................41
                2.11. Konwencje programowania......................................................................................45
                Ćwiczenia ..........................................................................................................................46
                Bibliografia........................................................................................................................47
Rozdział 3. Konkretne typy danych .................................................................... 49
                3.1. Ortodoksyjna postać kanoniczna klasy ......................................................................50
                3.2. Zakresy i kontrola dostępu .........................................................................................56
                3.3. Przecią anie — zmiana semantyki funkcji i operatorów ...........................................59
                3.4. Konwersja typu ..........................................................................................................64
                3.5. Zliczanie referencji i zmienne wykorzystujące „magiczną” pamięć .........................67
                3.6. Operatory new i delete ...............................................................................................80
                3.7. Separacja tworzenia instancji i jej inicjacji ................................................................85
                Ćwiczenia ..........................................................................................................................88
                Bibliografia........................................................................................................................90
Rozdział 4. Dziedziczenie ................................................................................... 91
                4.1. Dziedziczenie pojedyncze ..........................................................................................93
                4.2. Zakresy deklaracji i kontrola dostępu ........................................................................99
                4.3. Konstruktory i destruktory .......................................................................................109
                4.4. Konwersje wskaźników klas ....................................................................................112
6                                                         C++. Styl i technika zaawansowanego programowania


               4.5. Selektory typu ..........................................................................................................114
               Ćwiczenia ........................................................................................................................116
               Bibliografia......................................................................................................................118
Rozdział 5. Programowanie obiektowe............................................................... 119
               5.1. Funkcje wirtualne.....................................................................................................121
               5.2. Interakcje destruktorów i destruktory wirtualne ......................................................128
               5.3. Funkcje wirtualne i zakresy......................................................................................129
               5.4. Funkcje czysto wirtualne i abstrakcyjne klasy bazowe............................................131
               5.5. Klasa kopertowa i klasa listu....................................................................................133
               5.6. Funktory — funkcje jako obiekty ............................................................................161
               5.7. Dziedziczenie wielokrotne .......................................................................................172
               5.8. Kanoniczna postać dziedziczenia.............................................................................182
               Ćwiczenia ........................................................................................................................186
               Przykład iteratora kolejki ................................................................................................187
               Przykład klas prostej aplikacji bankowej..........................................................................188
               Bibliografia......................................................................................................................190
Rozdział 6. Projektowanie obiektowe ............................................................... 191
               6.1. Typy i klasy..............................................................................................................192
               6.2. Czynności projektowania obiektowego ...................................................................196
               6.3. Analiza obiektowa i analiza dziedziny.....................................................................199
               6.4. Związki obiektów i klas ...........................................................................................202
               6.5. Podtypy, dziedziczenie i przekazywanie..................................................................210
               6.6. Praktyczne zasady tworzenia podtypów, stosowania dziedziczenia
                 i niezale ności klas .......................................................................................................229
               Ćwiczenia ........................................................................................................................231
               Bibliografia......................................................................................................................232
Rozdział 7. Ponowne użycie i obiekty ............................................................... 233
               7.1. Gdy analogie przestają działać.................................................................................235
               7.2. Projektowanie z myślą o ponownym u yciu............................................................237
               7.3. Cztery mechanizmy ponownego u ycia kodu..........................................................239
               7.4. Typy parametryczne czyli szablony.........................................................................241
               7.5. Ponowne u ycie i dziedziczenie prywatne...............................................................249
               7.6. Ponowne u ycie pamięci..........................................................................................252
               7.7. Ponowne u ycie interfejsu — warianty ...................................................................253
               7.8. Ponowne u ycie, dziedziczenie i przekazywanie.....................................................255
               7.9. Ponowne u ycie kodu źródłowego...........................................................................256
               7.10. Ogólne uwagi na temat ponownego u ycia............................................................259
               Ćwiczenia ........................................................................................................................260
               Bibliografia......................................................................................................................261
Rozdział 8. Programowanie za pomocą przykładów ........................................... 263
               8.1. Przykład — przykłady pracowników.......................................................................266
               8.2. Konstruktory ogólne — idiom zespołu przykładów ................................................271
               8.3. Autonomiczne konstruktory ogólne .........................................................................273
               8.4. Abstrakcyjne przykłady bazowe ..............................................................................275
               8.5. Ku idiomowi szkieletu przykładu ............................................................................278
               8.6. Uwagi na temat notacji.............................................................................................280
               8.7. Przykłady i administracja kodem programu.............................................................282
               Ćwiczenia ........................................................................................................................283
               Prosty parser wykorzystujący przykłady.........................................................................284
               Przykład wykorzystujący szczeliny ................................................................................286
               Bibliografia......................................................................................................................288
Spis treści                                                                                                                                      7


Rozdział 9. Emulacja języków symbolicznych w C++ ......................................... 289
               9.1. Przyrostowy rozwój programów w języku C++ ......................................................291
               9.2. Symboliczna postać kanoniczna...............................................................................293
               9.3. Przykład — ogólna klasa kolekcji............................................................................304
               9.4. Kod i idiomy obsługujące mechanizm ładowania przyrostowego...........................308
               9.5. Odzyskiwanie nieu ytków .......................................................................................318
               9.6. Hermetyzacja typów podstawowych........................................................................327
               9.7. Wielometody i idiom symboliczny ..........................................................................328
               Ćwiczenia ........................................................................................................................332
               Bibliografia......................................................................................................................333
Rozdział 10. Dynamiczne dziedziczenie wielokrotne ............................................ 335
               10.1. Przykład — system okienkowy..............................................................................336
               10.2. Ograniczenia...........................................................................................................339
Rozdział 11. Zagadnienia systemowe................................................................. 341
               11.1. Statyczne projektowanie systemów .......................................................................342
               11.2. Dynamiczne projektowanie systemów...................................................................350
               Bibliografia......................................................................................................................365
Dodatek A Język C w środowisku języka C++ .................................................. 367
               A.1. Wywołania funkcji ..................................................................................................367
               A.2. Parametry funkcji ....................................................................................................368
               A.3. Prototypy funkcji.....................................................................................................369
               A.4. Przekazywanie parametrów przez referencję ..........................................................370
               A.5. Zmienna liczba parametrów ....................................................................................371
               A.6. Wskaźniki funkcji....................................................................................................373
               A.7. Słowo kluczowe const jako modyfikator typu ........................................................375
               A.8. Interfejs z programami w języku C .........................................................................377
               Ćwiczenia ........................................................................................................................389
               Bibliografia......................................................................................................................390
Dodatek B Reprezentacja figur geometrycznych w języku C++ ......................... 391
Dodatek C Referencje jako wartości zwracane przez operatory......................... 403
Dodatek D Kopiowanie „bit po bicie”............................................................... 407
               D.1. Dlaczego kopiowanie składowych nie rozwiązuje problemu?................................408
Dodatek E      Figury geometryczne i idiom symboliczny........................................ 409
Dodatek F      Programowanie strukturalne w języku C++ ..................................... 447
               F.1. Programowanie strukturalne — wprowadzenie.......................................................447
               F.2. Elementy programowania strukturalnego w języku C++ ........................................448
               F.3. Alternatywa dla bloków z głęboko zagnie d onymi zakresami..............................451
               F.4. Rozwa ania na temat implementacji........................................................................455
               Ćwiczenia ........................................................................................................................456
               Gra wykorzystująca idiom strukturalny ..........................................................................457
               Bibliografia......................................................................................................................460
               Spis rysunków ............................................................................... 461
               Spis listingów ................................................................................ 463
               Skorowidz...................................................................................... 467
Rozdział 9.
Emulacja języków
symbolicznych w C++
   Język C++ dysponuje mechanizmami umo liwiającymi definiowanie abstrakcyjnych
   typów danych i u ywanie ich w programowaniu obiektowym. Jednak elastyczność
   języków obiektowych wysokiego poziomu takich jak Smalltalk czy CLOS jest trudna
   w języku C++ tak blisko związanym z językiem C. Zarówno w języku C, jak i C++
   nazwa zmiennej związana jest z adresem opisywanego przez nią obiektu i tym samym
   nie jest jedynie jego etykietą, która mo e zostać „odklejona” z jednego obiektu i przy-
   porządkowana innemu. Silne powiązanie zmiennej z obiektem pozwala kompilatorowi
   zapewnić, e dana zmienna zawsze będzie u ywana z obiektem określonego typu.
   Właściwość ta pozwala generować bardziej efektywny kod i zapobiegać u yciu obiektów
   tam, gdzie nie były one spodziewane. Efektywność i zgodność typów osiąga się jednak
   kosztem elastyczności działania programu — na przykład zmienna zadeklarowana
   jako typu HNQCV nie mo e zostać u yta podczas działania programu dla obiektu typu
   %QORNGZ, chocia oba typy są ze sobą zgodne pod względem zachowania.

   Smalltalk i większość języków obiektowych bazujących na języku Lisp dysponuje
   dwiema właściwościami wykorzystującymi luźne powiązanie zmiennych i obiektów,
   które nie są bezpośrednio dostępne w języku C++, ale mogą zostać wyra one za pomocą
   odpowiednich idiomów i styli programowania. Pierwszą z tych właściwości jest auto-
   matyczne zarządzanie pamięcią (zliczanie referencji lub zbieranie nieu ytków). W języ-
   kach symbolicznych, gdzie zmienne są jedynie etykietami obiektów, czas istnienia
   obiektów jest niezale ny od opisujących je zmiennych. Środowiska programowania
   w językach symbolicznych u ywają specjalnych technik pozwalających odzyskać
   pamięć zajmowaną przez obiekty, do których nie istnieją ju adne odwołania w pro-
   gramie. W języku C++ mo emy symulować takie rozwiązanie, adresując obiekty za
   pomocą wskaźników. Utworzony w ten sposób dodatkowy poziom dostępu do obiektów
   mo e zostać wykorzystany w celu automatyzacji zarządzania pamięcią, co zostało
   szczegółowo omówione w podrozdziałach 3.5 i 3.6.
290                                 C++. Styl i technika zaawansowanego programowania


      Drugą istotną właściwością języków obiektowych wysokiego poziomu jest wysoki
      stopień polimorfizmu. Idiomy umo liwiające podobnie zaawansowany polimorfizm
      zostały omówione szczegółowo w podrozdziale 5.5. Zaawansowany polimorfizm
      umo liwia tworzenie bardziej elastycznych architektur systemów. Obiekty stają się
      słabiej powiązane i dlatego łatwiej jest nimi zarządzać.

      Automatyczne zarządzanie pamięcią i zaawansowany polimorfizm stanowią o sile
      języków programowania opartych o Lisp, języka Smalltalk i innych języków progra-
      mowania obiektowego o wywodzących się z tradycji programowania symbolicznego.
      Podobną elastyczność mo emy tak e uzyskać w programach tworzonych w języku
      C++ w stopniu, na jaki pozwala nam emulacja wymienionych właściwości za pomocą
      odpowiednich idiomów. Elastyczność taka ma jednak swoja cenę — zawsze odbywa
      się kosztem szybkości działania programu i większego zapotrzebowania na pamięć.
      Równie wykrywanie błędów wykonywane dotychczas przez system typów podczas
      kompilacji programu zostaje odroczone do momentu wykonania idiomów i w związku
      z tym zale y od spójności kodu kontrolującego zgodność typów w programie u yt-
      kownika, a nie od kompilatora. Doświadczenie projektanta pozwala osiągnąć w tej
      mierze wymagany kompromis poprzez wybór idiomów odpowiednich do potrzeb
      konkretnej aplikacji.

      W bie ącym rozdziale omówione zostaną trzy rodzaje idiomów. Pierwszy z nich stanowi
      kontynuację koncepcji przedstawionych w poprzednich rozdziałach, a wspierających
      przyrostowy rozwój programu poprzez redukcję wpływu zmian. Przedstawiona zostanie
      postać kanoniczna tego idiomu, która stanowić będzie podstawę dla pozostałych dwóch
      rodzajów. Drugi z nich umo liwi przyrostową aktualizację programu za pomocą pro-
      stego środowiska czasu wykonania. Natomiast trzeci wykorzystywać będzie techniki
      automatyzacji zwalniania nieu ywanych obiektów i odzyskiwania ich zasobów. Ka dy
      z tych idiomów mo e być stosowany niezale nie bądź w połączeniu z pozostałymi
      idiomami.

      Implementacja drugiego z wymienionych idiomów na dowolnej platformie wymaga
      sporo wiedzy i wysiłku, poniewa zale y od szczegółów implementacji języka C++
      związanych ze sposobem reprezentacji klas. Implementacje przedstawione w tym roz-
      dziale oparte są na objaśnieniach do standardu ANSI języka C++ [1]. Zostały one uru-
      chomione w środowisku AT&T USL C++ Compilation System Release 3 i powinny być
      przenośne do wielu środowisk przy wykorzystaniu kompilatorów innych producentów.

      Nale y zaznaczyć, e technik prezentowanych w niniejszym rozdziale nie nale y
      traktować jako substytutów rozwiązań oferowanych w językach Smalltalk lub CLOS.
      Języki symboliczne oprócz elastyczności umo liwiającej przyrostowy rozwój pro-
      gramów posiadają równie rozbudowane środowiska programowania wyposa one we
      własne, zaawansowane narzędzia obsługujące przyrostowy rozwój oprogramowania.
      Przedstawione tutaj rozwiązania pozwalają tylko w pewnym stopniu zbli yć język C++
      do tych mo liwości, ale za cenę dodatkowej dyscypliny w kodowaniu i kosztem słabszej
      efektywności tworzonych programów. Zadaniem tego rozdziału jest wprowadzenie
      koncepcji wspierających mo liwości przyrostowego rozwoju programów w języku C++
      oraz umo liwiających elastyczną aktualizację aplikacji pracujących w trybie ciągłym.
      Przedstawione rozwiązania mogą równie posłu yć jako model kodu generowanego
Rozdział 9. ♦ Emulacja języków symbolicznych w C++                                      291


       automatycznie przez narzędzia współpracujące z generatorem aplikacji lub kompilator
       języka wysokiego poziomu słu ącego do tworzenia elastycznych i interaktywnych
       aplikacji.



9.1. Przyrostowy rozwój programów
     w języku C++
       Zadaniem przyrostowego rozwoju programów jest szybkie wprowadzanie zmian tak,
       by ciągłość procesu rozwoju programów nie była zakłócana procesem testowania nowych
       wersji. Szybkie iteracje wersji programu stanowią wa ną technikę udoskonalania pro-
       gramu i kontrolę jego zachowań w świetle nowych wymagań. Koszt przyrostowej zmiany
       musi być przy tym niski, aby iteracje takie były efektywne.


Przyrostowość i projektowanie obiektowe
       Idea przyrostowego rozwoju programów doskonale współgra z projektowaniem obiek-
       towym. Hermetyzacja szczegółów implementacji wewnątrz klas sprawia, e stają się
       one naturalnymi jednostkami iteracji. Istnienie wspólnego protokołu umo liwiającego
       posługiwanie się wszystkimi klasami danej hierarchii dziedziczenia umo liwia łatwe
       dodawanie nowych klas. Choć wszystko to sprzyja przyrostowemu tworzeniu pro-
       gramów w języku C++, to jednak same iteracje ze względu na konieczność ponownej
       kompilacji kodu mogą okazać się zdecydowanie wolniejsze ni językach Smalltalk
       czy CLOS. Obecnie coraz częściej powstają zaawansowane środowiska programowania
       w języku C++, które, zrywając z tradycyjną technologią tworzenia oprogramowania,
       umo liwiają przyrostowy rozwój programów. Jednak technologia taka nadal nie jest
       jeszcze dostępna dla wielu platform. Na przykład elastyczność rozwoju lub aktualizacji
       po ądana jest najczęściej w systemach wbudowanych w pewne urządzenia pracujące
       poza kontekstem zawansowanych systemów operacyjnych i narzędzi programistycznych.
       Chocia więc przedstawione w tym rozdziale rozwiązania w zakresie przyrostowego
       rozwoju programów nie będą posiadać takich mo liwości jak oferowane przez zaawan-
       sowane środowiska programowania przyrostowego w języku C++, to jednak umo liwiać
       będą przyrostowy rozwój dla szerszego spektrum platform i systemów.


Redukcja kosztów kompilacji
       Pierwszy krok na drodze do programowania przyrostowego w języku C++ musi polegać
       na redukcji kosztów wynikających z ponownej kompilacji kodu. Najbardziej efektyw-
       nym sposobem realizacji tego zadania będzie ograniczenie samej potrzeby ponownej
       kompilacji. Pomiędzy zmienną, jej typem i sposobem reprezentacji zachodzi w języku
       C++ silny związek ustalany w momencie kompilacji. Jeśli na skutek ewolucji programu
       zmieni się na przykład typ zmiennej, to kod posługujący się taką zmienną musi zostać
       ponownie skompilowany. Zmiana reprezentacji zmiennej na skutek zmiany jej typu mo e
       równie spowodować przesunięcie adresów innych zmiennych i tym samym wymusić
292                                    C++. Styl i technika zaawansowanego programowania


      ponowną kompilację jeszcze innych fragmentów kodu, które posługują się tymi zmien-
      nymi. Na przykład jakakolwiek zmiana interfejsu klasy wymusza zawsze ponowną kom-
      pilację ka dego kodu, który korzysta z jakiegokolwiek elementu tego interfejsu.

      Większość rozwiązań słu ących ograniczaniu ponownej kompilacji kodu polega na
      tworzeniu pośredniej warstwy dostępu do symboli. Przykładami takich rozwiązań mogą
      być, na przykład, idiom koperty i listu (podrozdział 5.5) i jego pochodne, takie jak idiom
      przykładu omówiony w rozdziale 8. Idiomy przedstawione w bie ącym rozdziale
      bazują w znacznej mierze właśnie na idiomie przykładu.

      Zapewnienie odpowiedniej elastyczności w obliczu zmian i przy minimalnym poziomie
      kompilacji odbywa się za cenę mniejszej efektywności działania programu wynikającej
      z zastosowania dodatkowych poziomów dostępu do jego symboli. Zgodnie z duchem
      języków symbolicznych rozwiązania takie oferują równie słabszą kontrolę zgodności
      typów w stosunku do tradycyjnego programowania obiektowego w języku C++. Osią-
      gnięcie odpowiedniego kompromisu mo liwe jest przez wybór właściwych idiomów
      dla konkretnej aplikacji.


Redukcja kosztów konsolidacji i ładowania
      Drugi krok na drodze ku przyrostowemu rozwojowi programów w języku C++ polega
      na redukcji czasu związanego z konsolidacją i ładowaniem kodu. Konsolidacją nazy-
      wamy etap tworzenia wykonywalnego pliku programu z relokowalnych plików wyni-
      kowych powstałych podczas kompilacji, natomiast ładowanie polega na umieszczeniu
      wykonywalnego kodu programu w pamięci w celu jego wykonania. W niektórych
      systemach etapy te traktuje się łącznie, a większość systemów wykonuje podczas nich
      wiązanie symboli z adresami.

      Efektywność konsolidacji i ładowania jest szczególnie istotna w przypadku tworzenia
      zło onych systemów, dla którym równie techniki obiektowe mają najwięcej do zaofe-
      rowania. Przyrostowa konsolidacja i ładowanie kodu nie jest silną stroną większości
      tradycyjnych systemów mikroprocesorowych, jednak wiele nowych wersji systemu
      UNIX oraz innych systemów umo liwia ju przyrostową konsolidację programów.
      W rezultacie konsolidacji przyrostowej powstają zwykle mniejsze moduły wynikowe,
      a przede wszystkim umo liwia ona szybsze zmiany ni pełna konsolidacja.

      Nawet wtedy, gdy konsolidacja jest wystarczająco szybka, wąskim gardłem mo e okazać
      się proces ładowania kodu. Jeśli inicjacja systemu trwa długo, to nawet przyrostowo
      konsolidowane zmiany wymagają sporo czasu dla ka dej iteracji. Natomiast w przy-
      padku, gdy kod mo e być ładowany przyrostowo do zainicjowanego i działającego
      programu, to wprowadzanie zmian odbywa się zdecydowanie szybciej.


Szybkie iteracje
      Szybkie iteracje stanowią najefektywniejsze rozwiązanie na etapie poszukiwania
      docelowej architektury rozwiązania. Powstające w ten sposób tymczasowe prototypy
      słu ą głownie kontroli właściwego rozumienia aplikacji przez projektanta. Tworzenie
Rozdział 9. ♦ Emulacja języków symbolicznych w C++                                           293


       takich prototypów, przy zało eniu pewnych ograniczeń związanych ze stabilnością
       powstającej struktury rozwiązania, mo e być samo w sobie osobną dziedziną w pro-
       cesie rozwoju oprogramowania. Jeśli szybkie iteracje kodu są właściwie zarządzane,
       to mogą stanowić efektywną technikę rozwoju systemu. Natomiast jeśli dotyczą one
       za ka dym razem zasadniczych interfejsów tworzonego systemu, to będą jedynie
       zwiększać entropię i systematycznie niszczyć strukturę systemu.



9.2. Symboliczna postać kanoniczna
       Idiom symboliczny jest alternatywą ortodoksyjnej postaci kanonicznej zaprezentowanej
       w podrozdziale 3.1 (strona 50). Emulacja paradygmatu symbolicznego w języku C++
       nie jest „ortodoksyjna”, ale wymaga pewnych konwencji. Posługując się tą alternatywną
       postacią kanoniczną, mo emy w języku C++ modelować wiele właściwości charakte-
       rystycznych dla symbolicznych języków programowania. Jednak forma ta traci nieco
       na zwartości wyrazu charakterystycznej dla ortodoksyjnej postaci kanonicznej.

 Kiedy używać tego idiomu?
 Idiom ten stosujemy, gdy pożądana jest elastyczność oraz przyrostowość charakterystyczna
 dla języków programowania symbolicznego. Idiom ten może być również używany jako szkielet
 interfejsu pomiędzy środowiskiem programowania w języku C++ a środowiskami programo-
 wania w językach symbolicznych. Idiomy i style przedstawione w tym rozdziale mogą zostać
 wykorzystane do budowy własnego środowiska tworzenia prototypów w języku C++, jeśli pod-
 czas tworzenia aplikacji będziemy stosować się do pewnych konwencji. Idiom ten znajduje również
 zastosowanie w przypadku systemów pracy ciągłej, umożliwiając ich aktualizację bez zatrzymy-
 wania oraz ewolucję w dłuższym horyzoncie czasowym.


       Omawiana tu postać kanoniczna bazuje na koncepcjach w zakresie zarządzania pamię-
       cią i polimorfizmu przedstawionych w poprzednich rozdziałach i uzupełnia je tak, by
       mogły obsługiwać przyrostowość. Do głównych aspektów postaci kanonicznej nale ą:
             Automatyczne zarządzanie pamięcią wykorzystujące klasy listu ze zliczaniem
             referencji (podrozdział 3.5).
             Likwidacja dostępu do obiektów za pomocą wskaźników przy jednoczesnym
             udostępnieniu zachowania obiektów charakterystycznego dla wskaźników
             (podrozdział 3.5).
             Wykorzystanie funkcji wirtualnych dla uzyskania elastyczności w zakresie
             ładowania i wykonania kodu.
             Wykorzystanie idiomu przykładu (rozdział 8.).

       Symboliczną postać kanoniczną tworzy niewielka kolekcja klas bazowych, które
       wykorzystywane są do tworzenia klasy kopertowej i klasy listu dla danej aplikacji.
       Deklaracje klas bazowych umieszczone zostaną w globalnym pliku nagłówkowym k.h
       przedstawionym na listingu 9.1. Plik ten zawiera deklarację dwóch klas — 6QR, która
       jest klasą bazową dla klas kopertowych, oraz 6JKPI, która słu y jako klasa bazowa
       klas listu.
294                                           C++. Styl i technika zaawansowanego programowania


Listing 9.1. Plik nagłówkowy k.h
            plik nagłówkowy k.h
           ENCUU 6QR ]
           RWDNKE
                Obiekty tej klasy nie posiadają danych
                oprócz __vptr dostarczanej przez kompilator.
                Poniewa wszystkie inne klasy powstają jako
                pochodne klasy Top, to dla większości implementacji
                pole __vptr będzie pierwszym elementem ka dego obiektu.
                Niektóre implementacje mogą wymagać innego mechanizmu
                dostępu do __vptr. Dla idiomu symbolicznego od właściwości tej
                zale y jedynie aspekt dynamicznego ładowania.
               XKTVWCN `6QR
 ]
RWUV[
_
               XKTVWCN 6QR
V[RG
 ] TGVWTP VJKU _
                operator delete jest dostępny publicznie
                ze względu na konieczność usuwania
                aktualizowanych obiektów
               UVCVKE XQKF QRGTCVQT FGNGVG
XQKF
R ]
                     QRGTCVQT FGNGVG
R
               _
           RTQVGEVGF
               6QR
 ]
RWUV[
_
               UVCVKE XQKF
QRGTCVQT PGY
UKGAV N ]
                     TGVWTP QRGTCVQT PGY
N
               _
           _

           V[RGFGH WPUKIPGF NQPI 4'(A6;2'

           ENCUU 6JKPI RWDNKE 6QR ]
                Wszystkie składowe dziedziczone po klasie Thing
                Definiuje postać kanoniczną klas listu
           RWDNKE
               6JKPI
  TGH%QWPV8CN
 WRFCVG%QWPV8CN
 ] _
               XKTVWCN 4'(A6;2' FGTGH
 ]                zmniejsza licznik referencji
                   TGVWTP  TGH%QWPV8CN
               _
               XKTVWCN 4'(A6;2' TGH
 ]                  zwiększa licznik referencji
                   TGVWTP 

TGH%QWPV8CN
               _
               XKTVWCN 6JKPI
EWVQXGT
6JKPI
funkcja aktualizacji klasy
               XKTVWCN `6JKPI
 ]
RWUV[
_  destruktor
           RTKXCVG
               4'(A6;2' TGH%QWPV8CN WRFCVG%QWPV8CN
           _


        Klasa 6JKPI sama tak e jest klasą pochodną klasy 6QR, co pozwala zapewnić rozwiązaniu
        jednolitość i przypomina rozwiązanie stosowane w wielu językach symbolicznych
        polegające na istnieniu wspólnej klasy bazowej, od której wywodzą się wszystkie inne
        klasy. Zadanie klas 6QR i 6JKPI przypomina pod tym względem rolę klas 1DLGEV, %NCUU
        i $GJCXKQT w języku Smalltalk, chocia dokładne odwzorowanie pomiędzy tymi klasami
        nie jest ani oczywiste, ani przydatne.
Rozdział 9. ♦ Emulacja języków symbolicznych w C++                                        295


       Klasy te zapewniają elastyczność, wspierają zarządzanie pamięcią, aktualizację w trakcie
       wykonania oraz luźny model typów charakterystyczny dla języków symbolicznych.
       Ka da z klas zostanie omówiona szczegółowo w następnych dwóch podrozdziałach.


Klasa Top
       Klasa 6QR znajduje się na szczycie hierarchii klas systemu. Wszystkie klasy systemu
       są wobec tego jej pochodnymi. Klasa 6QR nie posiada własnych, jawnych danych.
       Większość kompilatorów języka C++ umieszcza w jej obiektach jedynie niejawną
       składową wykorzystywaną do rozpoznania typu obiektu przez mechanizm funkcji
       wirtualnych. Składowa ta nosi nazwę XRVT i jest wskaźnikiem elementu tablicy funkcji
       wirtualnych noszącej nazwę XVDN. Klasa Top posiada wirtualną metodę składową, aby
       wymusić obecność takiego wskaźnika.

       Ró ne implementacje języka C++ mogą ró nie implementować mechanizm funkcji
       wirtualnych, ale rozwiązania te ró nią się co najwy ej w szczegółach. Rozwa my
       przypadek następujących trzech klas [1]:
          ENCUU # ]
          RWDNKE
              KPV C
              XKTVWCN XQKF H
KPV
              XKTVWCN XQKF I
KPV
              XKTVWCN XQKF J
KPV
          _

          ENCUU $  RWDNKE # ]
          RWDNKE
              KPV D
              XQKF I
KPV
          _

          ENCUU %  RWDNKE $ ]
          RWDNKE
              KPV E
              XQKF J
KPV
          _

       W oparciu o powy sze deklaracje mo emy spodziewać się, e reprezentacja obiektu
       klasy C w pamięci będzie wyglądać w następujący sposób:




       Jeśli klasa znajdująca się na szczycie hierarchii nie posiada własnych danych, to wskaź-
       nik XRVT łatwo jest odnaleźć, poniewa znajduje się na początku reprezentacji obiektu.
       Dysponując wskaźnikiem takiego obiektu, dowolna funkcja mo e uzyskać wartość
       wskaźnika XRVT i u yć ją do przeglądania zawartości tablicy XVDN dla klasy obiektu.
       Mo liwość ta jest kluczowa z punktu widzenia zastępowanie funkcji podczas działania
       programu.
296                                   C++. Styl i technika zaawansowanego programowania


      Klasa 6QR posiada równie domyślny (bez parametrów) konstruktor zadeklarowany
      w sekcji o dostępie protected, co zapobiega bezpośredniemu tworzeniu instancji tej klasy.
      Posiada równie wirtualny destruktor, którego ciało nie zawiera adnych instrukcji.
      Destruktor został zadeklarowany jako wirtualny, aby zapewnić wywoływanie destrukto-
      rów odpowiednich klas podczas wykonania programu.

      Deklaracja operatora PGY równie została umieszczona w sekcji RTQVGEVGF, aby zapewnić,
       e obiekty klas kopertowych nie będą tworzone na stercie. Ograniczenie deklaracji
      obiektów klas kopertowych wyłącznie do obiektów lokalnych, globalnych lub składo-
      wych innych obiektów pozwala kompilatorowi całkowicie zautomatyzować ich usu-
      wanie. Jeśli instancje klas kopertowych powinny być równie tworzone na stercie, to
      zawsze istnieje mo liwość przesłonięcia tej deklaracji w klasach pochodnych. Dyna-
      miczny przydział i zwalnianie pamięci klas nale ących do hierarchii klasy listu odbywa
      się za pomocą operatorów klasy listu.

      Funkcja składowa V[RG jest przesłaniana przez klasy pochodne tak, by zwracała wskaź-
      nik odpowiedniego przykładu. Jest on wykorzystywany w celu aktualizacji klasy w czasie
      działania programu, co zostanie omówione w dalszej części tego rozdziału.

      Działanie klasy 6QR jest w znacznym stopniu zale ne od implementacji kompilatora.
      Większość kompilatorów języka C++ bazuje na formacie obiektów opisanym powy ej,
      ale w ogólnym przypadku przeniesienie implementacji tej klasy do dowolnego śro-
      dowiska mo e wymagać dodatkowych wysiłków.


Klasa Thing
      Klasa 6JKPI spełnia rolę klasy bazowej dla wszystkich klas listu. Poniewa klasy listu
      zawierają zasadniczą część inteligencji danej aplikacji, to większość semantyki zwią-
      zanej z dynamiką obiektów znajduje się w publicznym interfejsie klasy 6JKPI. W idiomie
      symbolicznym nawet pewna funkcjonalność związana z zarządzaniem pamięcią — która
      zwykle umieszczana bywa w klasie kopertowej — implementowana jest w klasach
      pochodnych klasy 6JKPI.

      Funkcje FGTGH i TGH operują na prywatnym liczniku referencji TGH%QWPV8CN. Zadekla-
      rowane są jako funkcje wirtualne, aby klasy pochodne mogły je przesłonić. Jednak
      typowa aplikacja idiomu symbolicznego z reguły nie musi ich deklarować jako funkcji
      wirtualnych, a nawet mo e zadeklarować je jako funkcje rozwijane w miejscu wywoła-
      nia. Funkcje te istnieją bowiem głównie dla wygody programisty. Prywatna składowa
      WRFCVG%QWPV8CN wykorzystywana jest podczas ładowania przyrostowego, a sposób jej
      u ycia zostanie opisany w dalszej części rozdziału.

      Funkcja EWVQXGT wykorzystywana jest do przekształcenia istniejącego obiektu danej
      klasy w obiekt reprezentujący inną wersję tej samej klasy. Umo liwia to konwersję
      danych istniejącego obiektu do nowego formatu, gdy do systemu zostaje wprowadzona
      nowa wersja klasy. Funkcja ta jest zwykle przesłaniana w klasach pochodnych, jeśli
      jest rzeczywiście u ywana. Jej domyślna semantyka polega jedynie na zwróceniu
      wskaźnika oryginalnego obiektu. Jej zastosowanie zostanie omówione w dalszej części
      rozdziału.
Rozdział 9. ♦ Emulacja języków symbolicznych w C++                                          297


       Wirtualny destruktor został zadeklarowany jedynie w celu zapewnienia, e dla obiektów
       klas pochodnych klasy 6JKPI wykonywany będzie odpowiedni kod na skutek wywo-
       łania operatora FGNGVG.


Symboliczna postać kanoniczna klas aplikacji
       Dysponując szkieletem zło onym z klas zadeklarowanych w pliku k.h, mo emy scharak-
       teryzować postać kanoniczną klas aplikacji wykorzystywaną przez idiom symboliczny.
       Będzie ona dotyczyć dwóch podstawowych rodzajów klas — kopert, które zarządzają
       tworzeniem i przypisywaniem obiektów, oraz listów, które zawierają zasadniczą seman-
       tykę aplikacji.

       Klasa kopertowa mo e być związana z wieloma klasami listu. Załó my, e projekt
       określa typ 0WODGT jako typ bazowy dla typów QWDNG, $KI+PVGIGT i %QORNGZ. Tradycyjne
       rozwiązanie w języku C++ wykorzystujące dziedziczenie i funkcje wirtualne umo li-
       wia wymienne posługiwanie się obiektami wymienionych klas za pomocą interfejsu
       klasy 0WODGT. Klasa 0WODGT będzie w nim abstrakcyjną klasą bazową dla pozostałych
       trzech klas, a instancje klasy 0WODGT nie będą występować. W rozwiązaniu opartym
       o idiom symboliczny u ytkownik zachowuje mo liwość wymiennego posługiwania się
       obiektami wymienionych trzech klas za pomocą interfejsu 0WODGT. Jednak klasa 0WODGT
       słu y równocześnie za kopertę dla obiektów klas listu QWDNG, $KI+PVGIGT i %QORNGZ.
       Klasy te tworzone są jako klasy pochodne ogólnej klasy bazowej 0WOGTKE4GR charak-
       teryzującej sygnaturę klas pochodnych. Klasa ta stanowi uogólnienie listu dla aplika-
       cji numerycznej. Klasa kopertowa 0WODGT zawiera wskaźnik typu 0WOGTKE4GR
, który
       dotyczy obiektu listu. Ogólna klasa listu jest z kolei klasą pochodna klasy 6JKPI, a klasa
       kopertowa (0WODGT) jest pochodną klasy 6QR. Taka struktura tworzy odpowiednie war-
       stwy pośrednie umo liwiające zaawansowany polimorfizm i aktualizacje podczas
       wykonania programu.

       Zwróćmy uwagę, e poniewa list jest klasą pochodną klasy 6JKPI, a koperta klasą
       pochodna klasy 6QR, to nie mo emy u yć wspólnej klasy bazowej dla listów i kopert,
       tak jak to bywało w poprzednich przykładach.

       Symboliczną postać kanoniczną przedstawimy, posługując się ogólnym przykładem,
       w którym klasa 'PXGNQRG będzie klasą kopertową, a klasa .GVVGT będzie ogólną klasą
       bazową listów. Kompozyt zło ony z pojedynczego obiektu klasy 'PXGNQRG i pojedyn-
       czego obiektu klasy jednej z klas pochodnych klasy .GVVGT stanowić będzie pojedynczą
       abstrakcję wykorzystywaną jako element programu stosującego idiom symboliczny,

       Ka dy program lub system mo e posiadać wiele klas kopertowych wykorzystujących
       postać kanoniczna klasy 'PXGNQRG i wiele klas listu utworzonych na wzór klasy .GVVGT
       i posiadających wyspecjalizowaną semantykę. Na przykład klasa 0WODGT mo e odpo-
       wiadać klasie 'PXGNQRG, klasa 0WOGTKE4GR klasie .GVVGT, a klasy %QORNGZ i inne będą
       pochodnymi klasy 0WOGTKE4GR. Ten sam program mo e równie u ywać klasy 5JCRG
       stworzonej na wzór klasy 'PXGNQRG oraz klasy 5JCRG4GR i jej pochodnych tworzących
       strukturę drzewiastą wzorowaną na hierarchii dziedziczenia klasy .GVVGT. Chocia klasy
       0WODGT i 5JCRG nie mają ze sobą nic wspólnego, to mogą istnieć w jednym programie,
       a ka da z nich u ywa idiomu symbolicznego we własnym zakresie.
298                                       C++. Styl i technika zaawansowanego programowania


        Klasa 'PXGNQRG (listing 9.2) stanowi uogólnienie klasy zajmującej się obsługą operacji
        tworzenia i przypisywania obiektów (czyli w praktyce obsługuje wszelkie operacje
        kopiowania oraz zajmuje się kwestią przydziału pamięci). Często warto zastosować
        w jej przypadku ortodoksyjną postać kanoniczną (podrozdział 3.1) i uczynić ją w ten
        sposób konkretnym typem danych. Koperta przypomina nieco etykietę, która mo e
        być stosowana do ró nych obiektów. Przypisanie oznacza wtedy powiązanie etykiety
        z obiektem. Usunięcie ostatniej etykiety obiektu jest równowa ne z jego zwróceniem
        do puli obiektów nieu ywanych.

Listing 9.2. Klasa Envelope
            KPENWFG MJ           z poprzedniego listingu

            KPENWFG J         funkcje składowe klasy Envelope

           GZVGTP 6JKPI
GPXGNQRG
NGVVGT  wskaźniki przykładów

           ENCUU 'PXGNQRG RWDNKE 6QR ]      klasa Top zdefiniowana w k.h
           RWDNKE
               .GVVGT
QRGTCVQT 
 EQPUV ]  przekazuje wszystkie operacje
                   TGVWTP TGR               obiektowi rep
               _
               'PXGNQRG
    ] TGR  NGVVGT OCMG
 _
               'PXGNQRG
.GVVGT 
               `'PXGNQRG
 ]
                   KH 
TGR    TGR FGTGH
   FGNGVG TGR
               _
               'PXGNQRG
'PXGNQRG Z ]
                   
TGR  ZTGR TGH

               _
               'PXGNQRG QRGTCVQT
'PXGNQRG Z ]
                   KH 
TGR  ZTGR ]
                       KH 
TGR    TGR FGTGH
   FGNGVG TGR
                       
TGR  ZTGR TGH

                   _
                   TGVWTP
VJKU
               _
               6JKPI
V[RG
 ] TGVWTP GPXGNQRG _
           RTKXCVG
               UVCVKE XQKF
QRGTCVQT PGY
UKGAV ]
                   5[UA'TTQT
 JGCR 'PXGNQRG 
               _
               UVCVKE XQKF QRGTCVQT FGNGVG
XQKF
] _
               .GVVGT
TGR
           _


        Klasa kopertowa zachowuje się jak abstrakcja o luźnym typie, a jej instancje symulują
        zmienne, które zachowują się jak etykiety, które nie posiadają typu, podobne do sto-
        sowanych w wielu językach symbolicznych. Na przykład funkcje składowe koperty nie
        przekazują szczegółowej semantyki obiektu listu, który zawiera koperta. Jednak koperta
        przyjmuje działanie przechowywanego obiektu klasy listu, posługując się mechanizmem
        operatora  opisanym w podrozdziale 3.5 w taki sam sposób, jak zmienna języka sym-
        bolicznego przyjmuje zachowanie obiektu, do którego została „przyklejona”. W jaki
        sposób interfejs koperty przekazuje jednak wiedzę zawieranego obiektu klasy listu?
Rozdział 9. ♦ Emulacja języków symbolicznych w C++                                              299


         Odpowiedź na to pytanie tkwi w typie zwracanym przez operator  . Typem tym jest
         .GVVGT
. Zwrócony wskaźnik jest jednym z rzeczywiście niewielu wskaźników wystę-
         pujących w idiomie symbolicznym. Stanowi on jednak tylko przejściową wartość, która
         nie jest zwykle przechowywana do dalszego u ytku.

         Klasa kopertowa posiada konstruktory, ale zasadnicza inicjacja obiektu wykonywana
         jest przez funkcje wirtualne OCMG klasy listu. Taki sposób działania jest korzystny z punktu
         widzenia przyrostowości i zostanie omówiony w dalszej części rozdziału. Konstruktory
         klasy kopertowej słu ą jedynie inicjacji oraz konwersji wykonywanej przez kompilator.
         Dwa z tych konstruktorów są charakterystyczne dla ortodoksyjnej postaci kanonicznej
         — konstruktor domyślny (nie posiada parametrów) oraz konstruktor kopiujący. Nie-
         zbędny jest tak e konstruktor tworzący nową kopertę dla instancji klas listu. Konstruk-
         tor ten dokonuje konwersji wyników wewnętrznych obliczeń klas listu na obiekty, które
         u ywane są przez klientów kompozytu zło onego z koperty i listu.

         Konstruktor kopiujący, operator przypisania i destruktor modyfikują licznik referencji
         obiektu w sposób opisany w podrozdziale 3.5. Destruktor sprawdza, czy licznik refe-
         rencji listu jest równy zero. Jeśli tak, to znaczy to, e usuwana jest ostatnia koperta
         posiadająca referencję tego listu i mo e być on usunięty.

         Ostatnią z funkcji klasy 'PXGNQRG, ale za to najwa niejszą dla jej działania, jest ope-
         rator  , który automatyzuje przekazywanie wywołań funkcji składowych koperty do
         obiektu listu. Taki sam efekt uzyskalibyśmy, powielając w interfejsie koperty sygnaturę
         listu, a ka da z funkcji składowych koperty przekazywałaby swoje działanie odpo-
         wiadającej jej funkcji listu. Jednak rozwiązanie takie wymaga dodatkowego wysiłku
         związanego z powieleniem funkcji listu w klasie kopertowej.

         Klasa .GVVGT (listing 9.3 i 9.4) definiuje interfejs wszystkich klas obsługiwanych za
         pośrednictwem interfejsu klasy 'PXGNQRG. Klasa .GVVGT sama jest klasą bazową, zwy-
         kle abstrakcyjną, dla grupy klas, których obiekty obsługiwane są za pośrednictwem
         interfejsu klasy 'PXGNQRG. Jeden obiekt klasy 'PXGNQRG mo e być wykorzystywany
         w cyklu swojego ycia jako interfejs dla wielu ró nych obiektów listu. Na przykład
         obiekt klasy 0WODGT mo e być początkowo interfejsem listu klasy %QORNGZ, jednak
         w następstwie wykonywanych obliczeń lub operacji przypisania obiekt listu mo e
         zostać zastąpiony obiektem listu innej klasy.

Listing 9.3. Klasa Letter
            ENCUU .GVVGT RWDNKE 6JKPI ]
            RWDNKE
FGMNCTCELG YU[UVMKEJ QRGTCVQTÎY FGHKPKQYCP[EJ RTG
W [VMQYPKMC RQYKPP[ PCNG è UKú Y V[O OKGLUEW
G YINúFW PC CUVQUQYCPKG QRGTCVQTC
U[IPCVWTC VGL MNCU[ PKG OWUK D[è RQYKGNCPC Y MNCUKG 'PXGNQRG
,GFPCM PCNG [ RCOKúVCè CD[ UM CFQYC TGR MNCU[
'PXGNQRG D[ C Y C EKYGIQ V[RW
1RGTCVQT RT[RKUCPKC FGHKPKQYCP[ LGUV Y MNCUKG 'PXGNQRG
V[R TGVWTPAV[RG YTCECP[ RTG FGMNCTQYCPG VWVCL HWPMELG W [VMQYPKMC RQYKPKGP
D[è V[RGO RQFUVCYQY[O V[RGO 'PXGNQRG V[RGO 'PXGNQRG
NWD MQPMTGVP[O V[RGO FCP[EJ
300                                       C++. Styl i technika zaawansowanego programowania


               XKTVWCN XQKF UGPF
5VTKPI PCOG 5VTKPI CFFTGUU
               XKTVWCN FQWDNG RQUVCIG

               XKTVWCN TGVWTPAV[RG HWPMELCAW [VMQYPKMC
                  
               XKTVWCN 'PXGNQRG OCMG
           konstruktor
               XKTVWCN 'PXGNQRG OCMG
FQWDNG     kolejny konstruktor
               XKTVWCN 'PXGNQRG OCMG
KPV FC[U FQWDNG YGKIJV
               XKTVWCN 6JKPI
EWVQXGT
6JKPI

C++. Styl i technika zaawansowanego programowania

  • 1.
    IDZ DO PRZYK£ADOWY ROZDZIA£ SPIS TRE CI C++. Styl i technika zaawansowanego KATALOG KSI¥¯EK KATALOG ONLINE programowania Autor: James O. Coplien T³umaczenie: Jaromir Senczyk ZAMÓW DRUKOWANY KATALOG ISBN: 83-7361-322-6 Tytu³ orygina³u: Advanced C++ TWÓJ KOSZYK Programming Styles and Idioms Format: B5, stron: 480 DODAJ DO KOSZYKA CENNIK I INFORMACJE Zak³adaj¹c znajomo æ podstaw jêzyka C++ ksi¹¿ka ta umo¿liwia programistom rozwiniêcie zaawansowanych umiejêtno ci programowania poprzez stosowanie styli ZAMÓW INFORMACJE i idiomów jêzyka C++. Struktura ksi¹¿ki zorganizowana jest wokó³ abstrakcji O NOWO CIACH wspieranych przez jêzyk C++: abstrakcyjnych typów danych, kombinacji typów w strukturach dziedziczenia, programowania obiektowego i dziedziczenia wielokrotnego. ZAMÓW CENNIK W ksi¹¿ce przedstawione zostaj¹ tak¿e te idiomy, które nie znajduj¹ bezpo redniego wsparcia w jêzyku C++, takie jak wirtualne konstruktory, obiekty prototypów i zaawansowane techniki odzyskiwania nieu¿ytków. CZYTELNIA Ksi¹¿ka: • Przedstawia zalety i potencjalne pu³apki zaawansowanych technik FRAGMENTY KSI¥¯EK ONLINE programowania w jêzyku C++. • Sposoby efektywnego ³¹czenia abstrakcji jêzyka C++ ilustruje szeregiem krótkich, ale stanowi¹cych wystarczaj¹cy instrukta¿ przyk³adów. • Dostarcza wielu praktycznych zasad wykorzystania jêzyka C++ do implementacji rezultatów projektowania obiektowego. • Omawia wszystkie w³a ciwo ci edycji 3.0 jêzyka C++, w tym zastosowanie szablonów w celu wielokrotnego wykorzystania kodu. • Przedstawia istotne aspekty rozwoju z³o¿onych systemów, w tym projektowanie bibliotek, obs³ugê wyj¹tków i przetwarzanie rozproszone. Ksi¹¿ka ta jest wa¿nym podrêcznikiem dla ka¿dego programisty aplikacji lub programisty systemowego pos³uguj¹cego siê jêzykiem C++. Wydawnictwo Helion ul. Chopina 6 44-100 Gliwice tel. (32)230-98-63 e-mail: [email protected]
  • 2.
    Spis treści Przedmowa........................................................................................ 9 Rozdział 1. Wprowadzenie ................................................................................. 15 1.1. Ewolucja języka C++ .................................................................................................15 1.2. Idiomy jako sposób na zło oność problemów ...........................................................16 1.3. Obiekty lat 90-tych.....................................................................................................18 1.4. Projektowanie i język programowania.......................................................................19 Bibliografia........................................................................................................................20 Rozdział 2. Abstrakcyjne typy danych ................................................................ 21 2.1. Klasy...........................................................................................................................22 2.2. Inwersja obiektowa ....................................................................................................25 2.3. Konstruktory i destruktory .........................................................................................28 2.4. Funkcje rozwijane w miejscu wywołania ..................................................................32 2.5. Inicjacja statycznych danych składowych..................................................................34 2.6. Statyczne funkcje składowe .......................................................................................35 2.7. Zakresy i słowo kluczowe const.................................................................................36 2.8. Porządek inicjacji obiektów globalnych, stałych i składowych statycznych .............38 2.9. Słowo const i funkcje składowe .................................................................................39 2.10. Wskaźniki funkcji składowych ................................................................................41 2.11. Konwencje programowania......................................................................................45 Ćwiczenia ..........................................................................................................................46 Bibliografia........................................................................................................................47 Rozdział 3. Konkretne typy danych .................................................................... 49 3.1. Ortodoksyjna postać kanoniczna klasy ......................................................................50 3.2. Zakresy i kontrola dostępu .........................................................................................56 3.3. Przecią anie — zmiana semantyki funkcji i operatorów ...........................................59 3.4. Konwersja typu ..........................................................................................................64 3.5. Zliczanie referencji i zmienne wykorzystujące „magiczną” pamięć .........................67 3.6. Operatory new i delete ...............................................................................................80 3.7. Separacja tworzenia instancji i jej inicjacji ................................................................85 Ćwiczenia ..........................................................................................................................88 Bibliografia........................................................................................................................90 Rozdział 4. Dziedziczenie ................................................................................... 91 4.1. Dziedziczenie pojedyncze ..........................................................................................93 4.2. Zakresy deklaracji i kontrola dostępu ........................................................................99 4.3. Konstruktory i destruktory .......................................................................................109 4.4. Konwersje wskaźników klas ....................................................................................112
  • 3.
    6 C++. Styl i technika zaawansowanego programowania 4.5. Selektory typu ..........................................................................................................114 Ćwiczenia ........................................................................................................................116 Bibliografia......................................................................................................................118 Rozdział 5. Programowanie obiektowe............................................................... 119 5.1. Funkcje wirtualne.....................................................................................................121 5.2. Interakcje destruktorów i destruktory wirtualne ......................................................128 5.3. Funkcje wirtualne i zakresy......................................................................................129 5.4. Funkcje czysto wirtualne i abstrakcyjne klasy bazowe............................................131 5.5. Klasa kopertowa i klasa listu....................................................................................133 5.6. Funktory — funkcje jako obiekty ............................................................................161 5.7. Dziedziczenie wielokrotne .......................................................................................172 5.8. Kanoniczna postać dziedziczenia.............................................................................182 Ćwiczenia ........................................................................................................................186 Przykład iteratora kolejki ................................................................................................187 Przykład klas prostej aplikacji bankowej..........................................................................188 Bibliografia......................................................................................................................190 Rozdział 6. Projektowanie obiektowe ............................................................... 191 6.1. Typy i klasy..............................................................................................................192 6.2. Czynności projektowania obiektowego ...................................................................196 6.3. Analiza obiektowa i analiza dziedziny.....................................................................199 6.4. Związki obiektów i klas ...........................................................................................202 6.5. Podtypy, dziedziczenie i przekazywanie..................................................................210 6.6. Praktyczne zasady tworzenia podtypów, stosowania dziedziczenia i niezale ności klas .......................................................................................................229 Ćwiczenia ........................................................................................................................231 Bibliografia......................................................................................................................232 Rozdział 7. Ponowne użycie i obiekty ............................................................... 233 7.1. Gdy analogie przestają działać.................................................................................235 7.2. Projektowanie z myślą o ponownym u yciu............................................................237 7.3. Cztery mechanizmy ponownego u ycia kodu..........................................................239 7.4. Typy parametryczne czyli szablony.........................................................................241 7.5. Ponowne u ycie i dziedziczenie prywatne...............................................................249 7.6. Ponowne u ycie pamięci..........................................................................................252 7.7. Ponowne u ycie interfejsu — warianty ...................................................................253 7.8. Ponowne u ycie, dziedziczenie i przekazywanie.....................................................255 7.9. Ponowne u ycie kodu źródłowego...........................................................................256 7.10. Ogólne uwagi na temat ponownego u ycia............................................................259 Ćwiczenia ........................................................................................................................260 Bibliografia......................................................................................................................261 Rozdział 8. Programowanie za pomocą przykładów ........................................... 263 8.1. Przykład — przykłady pracowników.......................................................................266 8.2. Konstruktory ogólne — idiom zespołu przykładów ................................................271 8.3. Autonomiczne konstruktory ogólne .........................................................................273 8.4. Abstrakcyjne przykłady bazowe ..............................................................................275 8.5. Ku idiomowi szkieletu przykładu ............................................................................278 8.6. Uwagi na temat notacji.............................................................................................280 8.7. Przykłady i administracja kodem programu.............................................................282 Ćwiczenia ........................................................................................................................283 Prosty parser wykorzystujący przykłady.........................................................................284 Przykład wykorzystujący szczeliny ................................................................................286 Bibliografia......................................................................................................................288
  • 4.
    Spis treści 7 Rozdział 9. Emulacja języków symbolicznych w C++ ......................................... 289 9.1. Przyrostowy rozwój programów w języku C++ ......................................................291 9.2. Symboliczna postać kanoniczna...............................................................................293 9.3. Przykład — ogólna klasa kolekcji............................................................................304 9.4. Kod i idiomy obsługujące mechanizm ładowania przyrostowego...........................308 9.5. Odzyskiwanie nieu ytków .......................................................................................318 9.6. Hermetyzacja typów podstawowych........................................................................327 9.7. Wielometody i idiom symboliczny ..........................................................................328 Ćwiczenia ........................................................................................................................332 Bibliografia......................................................................................................................333 Rozdział 10. Dynamiczne dziedziczenie wielokrotne ............................................ 335 10.1. Przykład — system okienkowy..............................................................................336 10.2. Ograniczenia...........................................................................................................339 Rozdział 11. Zagadnienia systemowe................................................................. 341 11.1. Statyczne projektowanie systemów .......................................................................342 11.2. Dynamiczne projektowanie systemów...................................................................350 Bibliografia......................................................................................................................365 Dodatek A Język C w środowisku języka C++ .................................................. 367 A.1. Wywołania funkcji ..................................................................................................367 A.2. Parametry funkcji ....................................................................................................368 A.3. Prototypy funkcji.....................................................................................................369 A.4. Przekazywanie parametrów przez referencję ..........................................................370 A.5. Zmienna liczba parametrów ....................................................................................371 A.6. Wskaźniki funkcji....................................................................................................373 A.7. Słowo kluczowe const jako modyfikator typu ........................................................375 A.8. Interfejs z programami w języku C .........................................................................377 Ćwiczenia ........................................................................................................................389 Bibliografia......................................................................................................................390 Dodatek B Reprezentacja figur geometrycznych w języku C++ ......................... 391 Dodatek C Referencje jako wartości zwracane przez operatory......................... 403 Dodatek D Kopiowanie „bit po bicie”............................................................... 407 D.1. Dlaczego kopiowanie składowych nie rozwiązuje problemu?................................408 Dodatek E Figury geometryczne i idiom symboliczny........................................ 409 Dodatek F Programowanie strukturalne w języku C++ ..................................... 447 F.1. Programowanie strukturalne — wprowadzenie.......................................................447 F.2. Elementy programowania strukturalnego w języku C++ ........................................448 F.3. Alternatywa dla bloków z głęboko zagnie d onymi zakresami..............................451 F.4. Rozwa ania na temat implementacji........................................................................455 Ćwiczenia ........................................................................................................................456 Gra wykorzystująca idiom strukturalny ..........................................................................457 Bibliografia......................................................................................................................460 Spis rysunków ............................................................................... 461 Spis listingów ................................................................................ 463 Skorowidz...................................................................................... 467
  • 5.
    Rozdział 9. Emulacja języków symbolicznychw C++ Język C++ dysponuje mechanizmami umo liwiającymi definiowanie abstrakcyjnych typów danych i u ywanie ich w programowaniu obiektowym. Jednak elastyczność języków obiektowych wysokiego poziomu takich jak Smalltalk czy CLOS jest trudna w języku C++ tak blisko związanym z językiem C. Zarówno w języku C, jak i C++ nazwa zmiennej związana jest z adresem opisywanego przez nią obiektu i tym samym nie jest jedynie jego etykietą, która mo e zostać „odklejona” z jednego obiektu i przy- porządkowana innemu. Silne powiązanie zmiennej z obiektem pozwala kompilatorowi zapewnić, e dana zmienna zawsze będzie u ywana z obiektem określonego typu. Właściwość ta pozwala generować bardziej efektywny kod i zapobiegać u yciu obiektów tam, gdzie nie były one spodziewane. Efektywność i zgodność typów osiąga się jednak kosztem elastyczności działania programu — na przykład zmienna zadeklarowana jako typu HNQCV nie mo e zostać u yta podczas działania programu dla obiektu typu %QORNGZ, chocia oba typy są ze sobą zgodne pod względem zachowania. Smalltalk i większość języków obiektowych bazujących na języku Lisp dysponuje dwiema właściwościami wykorzystującymi luźne powiązanie zmiennych i obiektów, które nie są bezpośrednio dostępne w języku C++, ale mogą zostać wyra one za pomocą odpowiednich idiomów i styli programowania. Pierwszą z tych właściwości jest auto- matyczne zarządzanie pamięcią (zliczanie referencji lub zbieranie nieu ytków). W języ- kach symbolicznych, gdzie zmienne są jedynie etykietami obiektów, czas istnienia obiektów jest niezale ny od opisujących je zmiennych. Środowiska programowania w językach symbolicznych u ywają specjalnych technik pozwalających odzyskać pamięć zajmowaną przez obiekty, do których nie istnieją ju adne odwołania w pro- gramie. W języku C++ mo emy symulować takie rozwiązanie, adresując obiekty za pomocą wskaźników. Utworzony w ten sposób dodatkowy poziom dostępu do obiektów mo e zostać wykorzystany w celu automatyzacji zarządzania pamięcią, co zostało szczegółowo omówione w podrozdziałach 3.5 i 3.6.
  • 6.
    290 C++. Styl i technika zaawansowanego programowania Drugą istotną właściwością języków obiektowych wysokiego poziomu jest wysoki stopień polimorfizmu. Idiomy umo liwiające podobnie zaawansowany polimorfizm zostały omówione szczegółowo w podrozdziale 5.5. Zaawansowany polimorfizm umo liwia tworzenie bardziej elastycznych architektur systemów. Obiekty stają się słabiej powiązane i dlatego łatwiej jest nimi zarządzać. Automatyczne zarządzanie pamięcią i zaawansowany polimorfizm stanowią o sile języków programowania opartych o Lisp, języka Smalltalk i innych języków progra- mowania obiektowego o wywodzących się z tradycji programowania symbolicznego. Podobną elastyczność mo emy tak e uzyskać w programach tworzonych w języku C++ w stopniu, na jaki pozwala nam emulacja wymienionych właściwości za pomocą odpowiednich idiomów. Elastyczność taka ma jednak swoja cenę — zawsze odbywa się kosztem szybkości działania programu i większego zapotrzebowania na pamięć. Równie wykrywanie błędów wykonywane dotychczas przez system typów podczas kompilacji programu zostaje odroczone do momentu wykonania idiomów i w związku z tym zale y od spójności kodu kontrolującego zgodność typów w programie u yt- kownika, a nie od kompilatora. Doświadczenie projektanta pozwala osiągnąć w tej mierze wymagany kompromis poprzez wybór idiomów odpowiednich do potrzeb konkretnej aplikacji. W bie ącym rozdziale omówione zostaną trzy rodzaje idiomów. Pierwszy z nich stanowi kontynuację koncepcji przedstawionych w poprzednich rozdziałach, a wspierających przyrostowy rozwój programu poprzez redukcję wpływu zmian. Przedstawiona zostanie postać kanoniczna tego idiomu, która stanowić będzie podstawę dla pozostałych dwóch rodzajów. Drugi z nich umo liwi przyrostową aktualizację programu za pomocą pro- stego środowiska czasu wykonania. Natomiast trzeci wykorzystywać będzie techniki automatyzacji zwalniania nieu ywanych obiektów i odzyskiwania ich zasobów. Ka dy z tych idiomów mo e być stosowany niezale nie bądź w połączeniu z pozostałymi idiomami. Implementacja drugiego z wymienionych idiomów na dowolnej platformie wymaga sporo wiedzy i wysiłku, poniewa zale y od szczegółów implementacji języka C++ związanych ze sposobem reprezentacji klas. Implementacje przedstawione w tym roz- dziale oparte są na objaśnieniach do standardu ANSI języka C++ [1]. Zostały one uru- chomione w środowisku AT&T USL C++ Compilation System Release 3 i powinny być przenośne do wielu środowisk przy wykorzystaniu kompilatorów innych producentów. Nale y zaznaczyć, e technik prezentowanych w niniejszym rozdziale nie nale y traktować jako substytutów rozwiązań oferowanych w językach Smalltalk lub CLOS. Języki symboliczne oprócz elastyczności umo liwiającej przyrostowy rozwój pro- gramów posiadają równie rozbudowane środowiska programowania wyposa one we własne, zaawansowane narzędzia obsługujące przyrostowy rozwój oprogramowania. Przedstawione tutaj rozwiązania pozwalają tylko w pewnym stopniu zbli yć język C++ do tych mo liwości, ale za cenę dodatkowej dyscypliny w kodowaniu i kosztem słabszej efektywności tworzonych programów. Zadaniem tego rozdziału jest wprowadzenie koncepcji wspierających mo liwości przyrostowego rozwoju programów w języku C++ oraz umo liwiających elastyczną aktualizację aplikacji pracujących w trybie ciągłym. Przedstawione rozwiązania mogą równie posłu yć jako model kodu generowanego
  • 7.
    Rozdział 9. ♦Emulacja języków symbolicznych w C++ 291 automatycznie przez narzędzia współpracujące z generatorem aplikacji lub kompilator języka wysokiego poziomu słu ącego do tworzenia elastycznych i interaktywnych aplikacji. 9.1. Przyrostowy rozwój programów w języku C++ Zadaniem przyrostowego rozwoju programów jest szybkie wprowadzanie zmian tak, by ciągłość procesu rozwoju programów nie była zakłócana procesem testowania nowych wersji. Szybkie iteracje wersji programu stanowią wa ną technikę udoskonalania pro- gramu i kontrolę jego zachowań w świetle nowych wymagań. Koszt przyrostowej zmiany musi być przy tym niski, aby iteracje takie były efektywne. Przyrostowość i projektowanie obiektowe Idea przyrostowego rozwoju programów doskonale współgra z projektowaniem obiek- towym. Hermetyzacja szczegółów implementacji wewnątrz klas sprawia, e stają się one naturalnymi jednostkami iteracji. Istnienie wspólnego protokołu umo liwiającego posługiwanie się wszystkimi klasami danej hierarchii dziedziczenia umo liwia łatwe dodawanie nowych klas. Choć wszystko to sprzyja przyrostowemu tworzeniu pro- gramów w języku C++, to jednak same iteracje ze względu na konieczność ponownej kompilacji kodu mogą okazać się zdecydowanie wolniejsze ni językach Smalltalk czy CLOS. Obecnie coraz częściej powstają zaawansowane środowiska programowania w języku C++, które, zrywając z tradycyjną technologią tworzenia oprogramowania, umo liwiają przyrostowy rozwój programów. Jednak technologia taka nadal nie jest jeszcze dostępna dla wielu platform. Na przykład elastyczność rozwoju lub aktualizacji po ądana jest najczęściej w systemach wbudowanych w pewne urządzenia pracujące poza kontekstem zawansowanych systemów operacyjnych i narzędzi programistycznych. Chocia więc przedstawione w tym rozdziale rozwiązania w zakresie przyrostowego rozwoju programów nie będą posiadać takich mo liwości jak oferowane przez zaawan- sowane środowiska programowania przyrostowego w języku C++, to jednak umo liwiać będą przyrostowy rozwój dla szerszego spektrum platform i systemów. Redukcja kosztów kompilacji Pierwszy krok na drodze do programowania przyrostowego w języku C++ musi polegać na redukcji kosztów wynikających z ponownej kompilacji kodu. Najbardziej efektyw- nym sposobem realizacji tego zadania będzie ograniczenie samej potrzeby ponownej kompilacji. Pomiędzy zmienną, jej typem i sposobem reprezentacji zachodzi w języku C++ silny związek ustalany w momencie kompilacji. Jeśli na skutek ewolucji programu zmieni się na przykład typ zmiennej, to kod posługujący się taką zmienną musi zostać ponownie skompilowany. Zmiana reprezentacji zmiennej na skutek zmiany jej typu mo e równie spowodować przesunięcie adresów innych zmiennych i tym samym wymusić
  • 8.
    292 C++. Styl i technika zaawansowanego programowania ponowną kompilację jeszcze innych fragmentów kodu, które posługują się tymi zmien- nymi. Na przykład jakakolwiek zmiana interfejsu klasy wymusza zawsze ponowną kom- pilację ka dego kodu, który korzysta z jakiegokolwiek elementu tego interfejsu. Większość rozwiązań słu ących ograniczaniu ponownej kompilacji kodu polega na tworzeniu pośredniej warstwy dostępu do symboli. Przykładami takich rozwiązań mogą być, na przykład, idiom koperty i listu (podrozdział 5.5) i jego pochodne, takie jak idiom przykładu omówiony w rozdziale 8. Idiomy przedstawione w bie ącym rozdziale bazują w znacznej mierze właśnie na idiomie przykładu. Zapewnienie odpowiedniej elastyczności w obliczu zmian i przy minimalnym poziomie kompilacji odbywa się za cenę mniejszej efektywności działania programu wynikającej z zastosowania dodatkowych poziomów dostępu do jego symboli. Zgodnie z duchem języków symbolicznych rozwiązania takie oferują równie słabszą kontrolę zgodności typów w stosunku do tradycyjnego programowania obiektowego w języku C++. Osią- gnięcie odpowiedniego kompromisu mo liwe jest przez wybór właściwych idiomów dla konkretnej aplikacji. Redukcja kosztów konsolidacji i ładowania Drugi krok na drodze ku przyrostowemu rozwojowi programów w języku C++ polega na redukcji czasu związanego z konsolidacją i ładowaniem kodu. Konsolidacją nazy- wamy etap tworzenia wykonywalnego pliku programu z relokowalnych plików wyni- kowych powstałych podczas kompilacji, natomiast ładowanie polega na umieszczeniu wykonywalnego kodu programu w pamięci w celu jego wykonania. W niektórych systemach etapy te traktuje się łącznie, a większość systemów wykonuje podczas nich wiązanie symboli z adresami. Efektywność konsolidacji i ładowania jest szczególnie istotna w przypadku tworzenia zło onych systemów, dla którym równie techniki obiektowe mają najwięcej do zaofe- rowania. Przyrostowa konsolidacja i ładowanie kodu nie jest silną stroną większości tradycyjnych systemów mikroprocesorowych, jednak wiele nowych wersji systemu UNIX oraz innych systemów umo liwia ju przyrostową konsolidację programów. W rezultacie konsolidacji przyrostowej powstają zwykle mniejsze moduły wynikowe, a przede wszystkim umo liwia ona szybsze zmiany ni pełna konsolidacja. Nawet wtedy, gdy konsolidacja jest wystarczająco szybka, wąskim gardłem mo e okazać się proces ładowania kodu. Jeśli inicjacja systemu trwa długo, to nawet przyrostowo konsolidowane zmiany wymagają sporo czasu dla ka dej iteracji. Natomiast w przy- padku, gdy kod mo e być ładowany przyrostowo do zainicjowanego i działającego programu, to wprowadzanie zmian odbywa się zdecydowanie szybciej. Szybkie iteracje Szybkie iteracje stanowią najefektywniejsze rozwiązanie na etapie poszukiwania docelowej architektury rozwiązania. Powstające w ten sposób tymczasowe prototypy słu ą głownie kontroli właściwego rozumienia aplikacji przez projektanta. Tworzenie
  • 9.
    Rozdział 9. ♦Emulacja języków symbolicznych w C++ 293 takich prototypów, przy zało eniu pewnych ograniczeń związanych ze stabilnością powstającej struktury rozwiązania, mo e być samo w sobie osobną dziedziną w pro- cesie rozwoju oprogramowania. Jeśli szybkie iteracje kodu są właściwie zarządzane, to mogą stanowić efektywną technikę rozwoju systemu. Natomiast jeśli dotyczą one za ka dym razem zasadniczych interfejsów tworzonego systemu, to będą jedynie zwiększać entropię i systematycznie niszczyć strukturę systemu. 9.2. Symboliczna postać kanoniczna Idiom symboliczny jest alternatywą ortodoksyjnej postaci kanonicznej zaprezentowanej w podrozdziale 3.1 (strona 50). Emulacja paradygmatu symbolicznego w języku C++ nie jest „ortodoksyjna”, ale wymaga pewnych konwencji. Posługując się tą alternatywną postacią kanoniczną, mo emy w języku C++ modelować wiele właściwości charakte- rystycznych dla symbolicznych języków programowania. Jednak forma ta traci nieco na zwartości wyrazu charakterystycznej dla ortodoksyjnej postaci kanonicznej. Kiedy używać tego idiomu? Idiom ten stosujemy, gdy pożądana jest elastyczność oraz przyrostowość charakterystyczna dla języków programowania symbolicznego. Idiom ten może być również używany jako szkielet interfejsu pomiędzy środowiskiem programowania w języku C++ a środowiskami programo- wania w językach symbolicznych. Idiomy i style przedstawione w tym rozdziale mogą zostać wykorzystane do budowy własnego środowiska tworzenia prototypów w języku C++, jeśli pod- czas tworzenia aplikacji będziemy stosować się do pewnych konwencji. Idiom ten znajduje również zastosowanie w przypadku systemów pracy ciągłej, umożliwiając ich aktualizację bez zatrzymy- wania oraz ewolucję w dłuższym horyzoncie czasowym. Omawiana tu postać kanoniczna bazuje na koncepcjach w zakresie zarządzania pamię- cią i polimorfizmu przedstawionych w poprzednich rozdziałach i uzupełnia je tak, by mogły obsługiwać przyrostowość. Do głównych aspektów postaci kanonicznej nale ą: Automatyczne zarządzanie pamięcią wykorzystujące klasy listu ze zliczaniem referencji (podrozdział 3.5). Likwidacja dostępu do obiektów za pomocą wskaźników przy jednoczesnym udostępnieniu zachowania obiektów charakterystycznego dla wskaźników (podrozdział 3.5). Wykorzystanie funkcji wirtualnych dla uzyskania elastyczności w zakresie ładowania i wykonania kodu. Wykorzystanie idiomu przykładu (rozdział 8.). Symboliczną postać kanoniczną tworzy niewielka kolekcja klas bazowych, które wykorzystywane są do tworzenia klasy kopertowej i klasy listu dla danej aplikacji. Deklaracje klas bazowych umieszczone zostaną w globalnym pliku nagłówkowym k.h przedstawionym na listingu 9.1. Plik ten zawiera deklarację dwóch klas — 6QR, która jest klasą bazową dla klas kopertowych, oraz 6JKPI, która słu y jako klasa bazowa klas listu.
  • 10.
    294 C++. Styl i technika zaawansowanego programowania Listing 9.1. Plik nagłówkowy k.h plik nagłówkowy k.h ENCUU 6QR ] RWDNKE Obiekty tej klasy nie posiadają danych oprócz __vptr dostarczanej przez kompilator. Poniewa wszystkie inne klasy powstają jako pochodne klasy Top, to dla większości implementacji pole __vptr będzie pierwszym elementem ka dego obiektu. Niektóre implementacje mogą wymagać innego mechanizmu dostępu do __vptr. Dla idiomu symbolicznego od właściwości tej zale y jedynie aspekt dynamicznego ładowania. XKTVWCN `6QR ]
  • 11.
  • 12.
    _ XKTVWCN 6QR
  • 13.
    V[RG ] TGVWTPVJKU _ operator delete jest dostępny publicznie ze względu na konieczność usuwania aktualizowanych obiektów UVCVKE XQKF QRGTCVQT FGNGVG XQKF
  • 14.
    R ] QRGTCVQT FGNGVG R _ RTQVGEVGF 6QR ]
  • 15.
  • 16.
    _ UVCVKE XQKF
  • 17.
    QRGTCVQT PGY UKGAV N] TGVWTP QRGTCVQT PGY N _ _ V[RGFGH WPUKIPGF NQPI 4'(A6;2' ENCUU 6JKPI RWDNKE 6QR ] Wszystkie składowe dziedziczone po klasie Thing Definiuje postać kanoniczną klas listu RWDNKE 6JKPI TGH%QWPV8CN WRFCVG%QWPV8CN ] _ XKTVWCN 4'(A6;2' FGTGH ] zmniejsza licznik referencji TGVWTP TGH%QWPV8CN _ XKTVWCN 4'(A6;2' TGH ] zwiększa licznik referencji TGVWTP TGH%QWPV8CN _ XKTVWCN 6JKPI
  • 18.
  • 19.
  • 20.
  • 21.
    _ destruktor RTKXCVG 4'(A6;2' TGH%QWPV8CN WRFCVG%QWPV8CN _ Klasa 6JKPI sama tak e jest klasą pochodną klasy 6QR, co pozwala zapewnić rozwiązaniu jednolitość i przypomina rozwiązanie stosowane w wielu językach symbolicznych polegające na istnieniu wspólnej klasy bazowej, od której wywodzą się wszystkie inne klasy. Zadanie klas 6QR i 6JKPI przypomina pod tym względem rolę klas 1DLGEV, %NCUU i $GJCXKQT w języku Smalltalk, chocia dokładne odwzorowanie pomiędzy tymi klasami nie jest ani oczywiste, ani przydatne.
  • 22.
    Rozdział 9. ♦Emulacja języków symbolicznych w C++ 295 Klasy te zapewniają elastyczność, wspierają zarządzanie pamięcią, aktualizację w trakcie wykonania oraz luźny model typów charakterystyczny dla języków symbolicznych. Ka da z klas zostanie omówiona szczegółowo w następnych dwóch podrozdziałach. Klasa Top Klasa 6QR znajduje się na szczycie hierarchii klas systemu. Wszystkie klasy systemu są wobec tego jej pochodnymi. Klasa 6QR nie posiada własnych, jawnych danych. Większość kompilatorów języka C++ umieszcza w jej obiektach jedynie niejawną składową wykorzystywaną do rozpoznania typu obiektu przez mechanizm funkcji wirtualnych. Składowa ta nosi nazwę XRVT i jest wskaźnikiem elementu tablicy funkcji wirtualnych noszącej nazwę XVDN. Klasa Top posiada wirtualną metodę składową, aby wymusić obecność takiego wskaźnika. Ró ne implementacje języka C++ mogą ró nie implementować mechanizm funkcji wirtualnych, ale rozwiązania te ró nią się co najwy ej w szczegółach. Rozwa my przypadek następujących trzech klas [1]: ENCUU # ] RWDNKE KPV C XKTVWCN XQKF H KPV XKTVWCN XQKF I KPV XKTVWCN XQKF J KPV _ ENCUU $ RWDNKE # ] RWDNKE KPV D XQKF I KPV _ ENCUU % RWDNKE $ ] RWDNKE KPV E XQKF J KPV _ W oparciu o powy sze deklaracje mo emy spodziewać się, e reprezentacja obiektu klasy C w pamięci będzie wyglądać w następujący sposób: Jeśli klasa znajdująca się na szczycie hierarchii nie posiada własnych danych, to wskaź- nik XRVT łatwo jest odnaleźć, poniewa znajduje się na początku reprezentacji obiektu. Dysponując wskaźnikiem takiego obiektu, dowolna funkcja mo e uzyskać wartość wskaźnika XRVT i u yć ją do przeglądania zawartości tablicy XVDN dla klasy obiektu. Mo liwość ta jest kluczowa z punktu widzenia zastępowanie funkcji podczas działania programu.
  • 23.
    296 C++. Styl i technika zaawansowanego programowania Klasa 6QR posiada równie domyślny (bez parametrów) konstruktor zadeklarowany w sekcji o dostępie protected, co zapobiega bezpośredniemu tworzeniu instancji tej klasy. Posiada równie wirtualny destruktor, którego ciało nie zawiera adnych instrukcji. Destruktor został zadeklarowany jako wirtualny, aby zapewnić wywoływanie destrukto- rów odpowiednich klas podczas wykonania programu. Deklaracja operatora PGY równie została umieszczona w sekcji RTQVGEVGF, aby zapewnić, e obiekty klas kopertowych nie będą tworzone na stercie. Ograniczenie deklaracji obiektów klas kopertowych wyłącznie do obiektów lokalnych, globalnych lub składo- wych innych obiektów pozwala kompilatorowi całkowicie zautomatyzować ich usu- wanie. Jeśli instancje klas kopertowych powinny być równie tworzone na stercie, to zawsze istnieje mo liwość przesłonięcia tej deklaracji w klasach pochodnych. Dyna- miczny przydział i zwalnianie pamięci klas nale ących do hierarchii klasy listu odbywa się za pomocą operatorów klasy listu. Funkcja składowa V[RG jest przesłaniana przez klasy pochodne tak, by zwracała wskaź- nik odpowiedniego przykładu. Jest on wykorzystywany w celu aktualizacji klasy w czasie działania programu, co zostanie omówione w dalszej części tego rozdziału. Działanie klasy 6QR jest w znacznym stopniu zale ne od implementacji kompilatora. Większość kompilatorów języka C++ bazuje na formacie obiektów opisanym powy ej, ale w ogólnym przypadku przeniesienie implementacji tej klasy do dowolnego śro- dowiska mo e wymagać dodatkowych wysiłków. Klasa Thing Klasa 6JKPI spełnia rolę klasy bazowej dla wszystkich klas listu. Poniewa klasy listu zawierają zasadniczą część inteligencji danej aplikacji, to większość semantyki zwią- zanej z dynamiką obiektów znajduje się w publicznym interfejsie klasy 6JKPI. W idiomie symbolicznym nawet pewna funkcjonalność związana z zarządzaniem pamięcią — która zwykle umieszczana bywa w klasie kopertowej — implementowana jest w klasach pochodnych klasy 6JKPI. Funkcje FGTGH i TGH operują na prywatnym liczniku referencji TGH%QWPV8CN. Zadekla- rowane są jako funkcje wirtualne, aby klasy pochodne mogły je przesłonić. Jednak typowa aplikacja idiomu symbolicznego z reguły nie musi ich deklarować jako funkcji wirtualnych, a nawet mo e zadeklarować je jako funkcje rozwijane w miejscu wywoła- nia. Funkcje te istnieją bowiem głównie dla wygody programisty. Prywatna składowa WRFCVG%QWPV8CN wykorzystywana jest podczas ładowania przyrostowego, a sposób jej u ycia zostanie opisany w dalszej części rozdziału. Funkcja EWVQXGT wykorzystywana jest do przekształcenia istniejącego obiektu danej klasy w obiekt reprezentujący inną wersję tej samej klasy. Umo liwia to konwersję danych istniejącego obiektu do nowego formatu, gdy do systemu zostaje wprowadzona nowa wersja klasy. Funkcja ta jest zwykle przesłaniana w klasach pochodnych, jeśli jest rzeczywiście u ywana. Jej domyślna semantyka polega jedynie na zwróceniu wskaźnika oryginalnego obiektu. Jej zastosowanie zostanie omówione w dalszej części rozdziału.
  • 24.
    Rozdział 9. ♦Emulacja języków symbolicznych w C++ 297 Wirtualny destruktor został zadeklarowany jedynie w celu zapewnienia, e dla obiektów klas pochodnych klasy 6JKPI wykonywany będzie odpowiedni kod na skutek wywo- łania operatora FGNGVG. Symboliczna postać kanoniczna klas aplikacji Dysponując szkieletem zło onym z klas zadeklarowanych w pliku k.h, mo emy scharak- teryzować postać kanoniczną klas aplikacji wykorzystywaną przez idiom symboliczny. Będzie ona dotyczyć dwóch podstawowych rodzajów klas — kopert, które zarządzają tworzeniem i przypisywaniem obiektów, oraz listów, które zawierają zasadniczą seman- tykę aplikacji. Klasa kopertowa mo e być związana z wieloma klasami listu. Załó my, e projekt określa typ 0WODGT jako typ bazowy dla typów QWDNG, $KI+PVGIGT i %QORNGZ. Tradycyjne rozwiązanie w języku C++ wykorzystujące dziedziczenie i funkcje wirtualne umo li- wia wymienne posługiwanie się obiektami wymienionych klas za pomocą interfejsu klasy 0WODGT. Klasa 0WODGT będzie w nim abstrakcyjną klasą bazową dla pozostałych trzech klas, a instancje klasy 0WODGT nie będą występować. W rozwiązaniu opartym o idiom symboliczny u ytkownik zachowuje mo liwość wymiennego posługiwania się obiektami wymienionych trzech klas za pomocą interfejsu 0WODGT. Jednak klasa 0WODGT słu y równocześnie za kopertę dla obiektów klas listu QWDNG, $KI+PVGIGT i %QORNGZ. Klasy te tworzone są jako klasy pochodne ogólnej klasy bazowej 0WOGTKE4GR charak- teryzującej sygnaturę klas pochodnych. Klasa ta stanowi uogólnienie listu dla aplika- cji numerycznej. Klasa kopertowa 0WODGT zawiera wskaźnik typu 0WOGTKE4GR
  • 25.
    , który dotyczy obiektu listu. Ogólna klasa listu jest z kolei klasą pochodna klasy 6JKPI, a klasa kopertowa (0WODGT) jest pochodną klasy 6QR. Taka struktura tworzy odpowiednie war- stwy pośrednie umo liwiające zaawansowany polimorfizm i aktualizacje podczas wykonania programu. Zwróćmy uwagę, e poniewa list jest klasą pochodną klasy 6JKPI, a koperta klasą pochodna klasy 6QR, to nie mo emy u yć wspólnej klasy bazowej dla listów i kopert, tak jak to bywało w poprzednich przykładach. Symboliczną postać kanoniczną przedstawimy, posługując się ogólnym przykładem, w którym klasa 'PXGNQRG będzie klasą kopertową, a klasa .GVVGT będzie ogólną klasą bazową listów. Kompozyt zło ony z pojedynczego obiektu klasy 'PXGNQRG i pojedyn- czego obiektu klasy jednej z klas pochodnych klasy .GVVGT stanowić będzie pojedynczą abstrakcję wykorzystywaną jako element programu stosującego idiom symboliczny, Ka dy program lub system mo e posiadać wiele klas kopertowych wykorzystujących postać kanoniczna klasy 'PXGNQRG i wiele klas listu utworzonych na wzór klasy .GVVGT i posiadających wyspecjalizowaną semantykę. Na przykład klasa 0WODGT mo e odpo- wiadać klasie 'PXGNQRG, klasa 0WOGTKE4GR klasie .GVVGT, a klasy %QORNGZ i inne będą pochodnymi klasy 0WOGTKE4GR. Ten sam program mo e równie u ywać klasy 5JCRG stworzonej na wzór klasy 'PXGNQRG oraz klasy 5JCRG4GR i jej pochodnych tworzących strukturę drzewiastą wzorowaną na hierarchii dziedziczenia klasy .GVVGT. Chocia klasy 0WODGT i 5JCRG nie mają ze sobą nic wspólnego, to mogą istnieć w jednym programie, a ka da z nich u ywa idiomu symbolicznego we własnym zakresie.
  • 26.
    298 C++. Styl i technika zaawansowanego programowania Klasa 'PXGNQRG (listing 9.2) stanowi uogólnienie klasy zajmującej się obsługą operacji tworzenia i przypisywania obiektów (czyli w praktyce obsługuje wszelkie operacje kopiowania oraz zajmuje się kwestią przydziału pamięci). Często warto zastosować w jej przypadku ortodoksyjną postać kanoniczną (podrozdział 3.1) i uczynić ją w ten sposób konkretnym typem danych. Koperta przypomina nieco etykietę, która mo e być stosowana do ró nych obiektów. Przypisanie oznacza wtedy powiązanie etykiety z obiektem. Usunięcie ostatniej etykiety obiektu jest równowa ne z jego zwróceniem do puli obiektów nieu ywanych. Listing 9.2. Klasa Envelope KPENWFG MJ z poprzedniego listingu KPENWFG J funkcje składowe klasy Envelope GZVGTP 6JKPI
  • 27.
  • 28.
    NGVVGT wskaźnikiprzykładów ENCUU 'PXGNQRG RWDNKE 6QR ] klasa Top zdefiniowana w k.h RWDNKE .GVVGT
  • 29.
    QRGTCVQT EQPUV] przekazuje wszystkie operacje TGVWTP TGR obiektowi rep _ 'PXGNQRG ] TGR NGVVGT OCMG _ 'PXGNQRG .GVVGT `'PXGNQRG ] KH TGR TGR FGTGH FGNGVG TGR _ 'PXGNQRG 'PXGNQRG Z ] TGR ZTGR TGH _ 'PXGNQRG QRGTCVQT 'PXGNQRG Z ] KH TGR ZTGR ] KH TGR TGR FGTGH FGNGVG TGR TGR ZTGR TGH _ TGVWTP
  • 30.
    VJKU _ 6JKPI
  • 31.
    V[RG ] TGVWTPGPXGNQRG _ RTKXCVG UVCVKE XQKF
  • 32.
    QRGTCVQT PGY UKGAV ] 5[UA'TTQT JGCR 'PXGNQRG _ UVCVKE XQKF QRGTCVQT FGNGVG XQKF
  • 33.
    ] _ .GVVGT
  • 34.
    TGR _ Klasa kopertowa zachowuje się jak abstrakcja o luźnym typie, a jej instancje symulują zmienne, które zachowują się jak etykiety, które nie posiadają typu, podobne do sto- sowanych w wielu językach symbolicznych. Na przykład funkcje składowe koperty nie przekazują szczegółowej semantyki obiektu listu, który zawiera koperta. Jednak koperta przyjmuje działanie przechowywanego obiektu klasy listu, posługując się mechanizmem operatora opisanym w podrozdziale 3.5 w taki sam sposób, jak zmienna języka sym- bolicznego przyjmuje zachowanie obiektu, do którego została „przyklejona”. W jaki sposób interfejs koperty przekazuje jednak wiedzę zawieranego obiektu klasy listu?
  • 35.
    Rozdział 9. ♦Emulacja języków symbolicznych w C++ 299 Odpowiedź na to pytanie tkwi w typie zwracanym przez operator . Typem tym jest .GVVGT
  • 36.
    . Zwrócony wskaźnikjest jednym z rzeczywiście niewielu wskaźników wystę- pujących w idiomie symbolicznym. Stanowi on jednak tylko przejściową wartość, która nie jest zwykle przechowywana do dalszego u ytku. Klasa kopertowa posiada konstruktory, ale zasadnicza inicjacja obiektu wykonywana jest przez funkcje wirtualne OCMG klasy listu. Taki sposób działania jest korzystny z punktu widzenia przyrostowości i zostanie omówiony w dalszej części rozdziału. Konstruktory klasy kopertowej słu ą jedynie inicjacji oraz konwersji wykonywanej przez kompilator. Dwa z tych konstruktorów są charakterystyczne dla ortodoksyjnej postaci kanonicznej — konstruktor domyślny (nie posiada parametrów) oraz konstruktor kopiujący. Nie- zbędny jest tak e konstruktor tworzący nową kopertę dla instancji klas listu. Konstruk- tor ten dokonuje konwersji wyników wewnętrznych obliczeń klas listu na obiekty, które u ywane są przez klientów kompozytu zło onego z koperty i listu. Konstruktor kopiujący, operator przypisania i destruktor modyfikują licznik referencji obiektu w sposób opisany w podrozdziale 3.5. Destruktor sprawdza, czy licznik refe- rencji listu jest równy zero. Jeśli tak, to znaczy to, e usuwana jest ostatnia koperta posiadająca referencję tego listu i mo e być on usunięty. Ostatnią z funkcji klasy 'PXGNQRG, ale za to najwa niejszą dla jej działania, jest ope- rator , który automatyzuje przekazywanie wywołań funkcji składowych koperty do obiektu listu. Taki sam efekt uzyskalibyśmy, powielając w interfejsie koperty sygnaturę listu, a ka da z funkcji składowych koperty przekazywałaby swoje działanie odpo- wiadającej jej funkcji listu. Jednak rozwiązanie takie wymaga dodatkowego wysiłku związanego z powieleniem funkcji listu w klasie kopertowej. Klasa .GVVGT (listing 9.3 i 9.4) definiuje interfejs wszystkich klas obsługiwanych za pośrednictwem interfejsu klasy 'PXGNQRG. Klasa .GVVGT sama jest klasą bazową, zwy- kle abstrakcyjną, dla grupy klas, których obiekty obsługiwane są za pośrednictwem interfejsu klasy 'PXGNQRG. Jeden obiekt klasy 'PXGNQRG mo e być wykorzystywany w cyklu swojego ycia jako interfejs dla wielu ró nych obiektów listu. Na przykład obiekt klasy 0WODGT mo e być początkowo interfejsem listu klasy %QORNGZ, jednak w następstwie wykonywanych obliczeń lub operacji przypisania obiekt listu mo e zostać zastąpiony obiektem listu innej klasy. Listing 9.3. Klasa Letter ENCUU .GVVGT RWDNKE 6JKPI ] RWDNKE
  • 37.
  • 38.
    W [VMQYPKMC RQYKPP[PCNG è UKú Y V[O OKGLUEW
  • 39.
    G YINúFW PCCUVQUQYCPKG QRGTCVQTC
  • 40.
    U[IPCVWTC VGL MNCU[PKG OWUK D[è RQYKGNCPC Y MNCUKG 'PXGNQRG
  • 41.
    ,GFPCM PCNG [RCOKúVCè CD[ UM CFQYC TGR MNCU[
  • 42.
    'PXGNQRG D[ CY C EKYGIQ V[RW
  • 43.
    1RGTCVQT RT[RKUCPKC FGHKPKQYCP[LGUV Y MNCUKG 'PXGNQRG
  • 45.
    V[R TGVWTPAV[RG YTCECP[RTG FGMNCTQYCPG VWVCL HWPMELG W [VMQYPKMC RQYKPKGP
  • 46.
    D[è V[RGO RQFUVCYQY[OV[RGO 'PXGNQRG V[RGO 'PXGNQRG
  • 47.
  • 48.
    300 C++. Styl i technika zaawansowanego programowania XKTVWCN XQKF UGPF 5VTKPI PCOG 5VTKPI CFFTGUU XKTVWCN FQWDNG RQUVCIG XKTVWCN TGVWTPAV[RG HWPMELCAW [VMQYPKMC XKTVWCN 'PXGNQRG OCMG konstruktor XKTVWCN 'PXGNQRG OCMG FQWDNG kolejny konstruktor XKTVWCN 'PXGNQRG OCMG KPV FC[U FQWDNG YGKIJV XKTVWCN 6JKPI
  • 49.
  • 50.
    funkcja aktualizacji podczaswykonania .GVVGT ] _ `.GVVGT ] _ 6JKPI
  • 51.
    V[RG RTQVGEVGF HTKGPF ENCUU 'PXGNQRG FQWDNG QWPEGU UVCVKE XQKF
  • 52.
    QRGTCVQT PGY UKGAV N] TGVWTP QRGTCVQT PGY N _ UVCVKE XQKF QRGTCVQT FGNGVG XQKF
  • 53.
    R ] QRGTCVQT FGNGVG R _ 5VTKPI PCOG CFFTGUU RTKXCVG _ Listing 9.4. Funkcje składowe klasy Letter rozwijane w miejscu wywołania
  • 55.
    6WVCL RQYKPP[ QUVCèWOKGUEQPG YU[UVMKG QIÎNPG HWPMELG
  • 56.
    TQYKLCPG Y OKGLUEWY[YQ CPKC ,GUV VQ IQFPG MQPYGPELæ
  • 57.
    RQNGICLæEæ PC WOKGUECPKWFGHKPKELK HWPMELK TQYKLCP[EJ Y OKGLUEW
  • 58.
    Y[YQ CPKC RQCFGMNCTCELæ MNCU[ 4QYKæCPKG VCMKG RQYCNC TÎYPKG
  • 59.
    WPKMPæè E[MNK CNGPQ EK RQOKúF[ HWPMELCOK MNCU 'PXGNQRG K .GVVGT
  • 60.
    KPNKPG FQWDNG .GVVGTRQUVCIG ] KH QWPEGU TGVWTP GNUG TGVWTP QWPEGU
  • 61.
    _ KPNKPG 6JKPI
  • 62.
    .GVVGTV[RG ] GZVGTP 6JKPI
  • 63.
    NGVVGT przykład TGVWTP NGVVGT _ Przyjmuje się, e obiekty nale ące do hierarchii klasy .GVVGT znajdują się zawsze wewnątrz obiektu klasy 'PXGNQRG i tylko ten obiekt widoczny jest dla u ytkownika. Sygnatura klasy nie jest nigdy u ywana bezpośrednio przez u ytkownika. Jednak funkcje składowe klasy .GVVGT wykorzystywane są przez interfejs klasy 'PXGNQRG za pośrednictwem operatora 'PXGNQRGQRGTCVQT . Klasa .GVVGT nie musi być konkretnym typem danych, poniewa obsługiwana jest za pośrednictwem interfejsu klasy 'PXGNQRG.
  • 64.
    Rozdział 9. ♦Emulacja języków symbolicznych w C++ 301 Klasa .GVVGT słu y jako klasa bazowa dla klas aplikacji zarządzanych przez klasę 'PXGNQRG. Klasa 'PXGNQRG zawiera składową TGR wskazującą instancję klasy .GVVGT. Rzeczywiste działanie kompozytu klas 'PXGNQRG i .GVVGT wykonywane jest właśnie przez obiekt jednej z klas pochodnych klasy .GVVGT. Wszystkie funkcje składowe aplikacji zostają wyspecyfikowane w interfejsie klasy listu, zwykle jako funkcje czysto wirtualne. Niektóre z funkcji, wspólne dla wszystkich klas pochodnych klasy .GVVGT mogą zostać zdefiniowane ju w tej klasie. Poniewa pozostałe funkcje zdefiniowane są jako funkcje czysto wirtualne, to mamy gwarancję, e ich implementacji dostarczą klas pochodne. Jednak w dalszej części rozdziału poka- emy, e u ycie funkcji czysto wirtualnych nie jest dopuszczalne w rozszerzonej formie idiomu wykorzystującej obiekty przykładów. Funkcje definiowane przez u ytkownika powinny zwracać obiekty typów wbudowa- nych w język lub konkretnych typów danych (czyli zgodnych z ortodoksyjną postacią kanoniczną) lub typu 'PXGNQRG lub typu referencji 'PXGNQRG. W sygnaturze klasy 'PXGNQRG powinny pojawiać się co najwy ej stałe wskaźniki (EQPUV). Zwracanie zwy- kłych wskaźników do dynamicznie przydzielonych obszarów pamięci mo e bowiem naruszyć zaimplementowany schemat zarządzania pamięcią. Oczywiście funkcje defi- niowane przez u ytkownika mogą być równie typu XQKF. Funkcja OCMG tworzy instancję klasy pochodnej klasy .GVVGT i zwraca wskaźnik typu .GVVGT
  • 65.
    , co opisanezostało w rozdziale 8. W ogólnym przypadku istnieć mo e wiele przecią onych funkcji OCMG, przy czym ka da z nich zajmuje się inicjacją nowego obiektu. adna operacja związana z inicjacją obiektu nie powinna być pozostawiana konstruk- torowi. Na przykład funkcja .GVVGTOCMG mo e inicjować obiekty klas 1XGT0KIJV i (KTUV%NCUU w następujący sposób: 'PXGNQRG .GVVGTOCMG KPV FC[U FQWDNG YGKIJV ] .GVVGT
  • 66.
    TGVXCN KH FC[U YGKIJV ] TGVXCN PGY 1XGT0KIJV _ GNUG ] TGVXCN PGY (KTUV%NCUU _ TGVXCN QWPEGU YGKIJV TGVWTP 'PXGNQRG
  • 67.
    TGVXCN _ Jeśli konstruktor nie zawiera adnej logiki związanej z inicjacją obiektów, to nie wymaga wprowadzania modyfikacji i tym samym ponownej kompilacji. Jest to istotne w środo- wisku przyrostowego ładowania kodu, w którym funkcje wirtualne (na przykład funkcje OCMG) mogą być ładowane przyrostowo, a konstruktory nie. W ogólnym przypadku klasy pochodne klasy .GVVGT nie muszą stosować ortodoksyjnej postaci kanonicznej. Chocia powinny posiadać konstruktor domyślny oraz destruktor, to jednak specyfikacja konstruktora kopiującego oraz operatora przypisania nie jest wymagana.
  • 68.
    302 C++. Styl i technika zaawansowanego programowania Obiekt klasy 'PXGNQRG mo e zawierać obiekt dowolnej klasy pochodnej klasy .GVVGT. Jeśli klasa 'PXGNQRG jest poprawnie zaprojektowana, to dowolny obiekt tej klasy mo e być przypisany dowolnemu innemu obiektowi tej klasy. Listing 9.5 przedstawia przy- kłady prostych klas pochodnych klasy .GVVGT. Ka da z tych klas posiada własny kon- struktor domyślny. Listing 9.5. Przykładowe klasy pochodne klasy Letter ENCUU (KTUV%NCUU RWDNKE .GVVGT ] RWDNKE (KTUV%NCUU `(KTUV%NCUU 'PXGNQRG OCMG 'PXGNQRG OCMG FQWDNG YGKIJV _ ENCUU 1XGT0KIJV RWDNKE .GVVGT ] RWDNKE 1XGT0KIJV `1XGT0KIJV 'PXGNQRG OCMG 'PXGNQRG OCMG FQWDNG YGKIJV FQWDNG RQUVCIG ] TGVWTP _ _ Zgodnie z idiomem przykładu ka da klasa kopertowa posiada pojedynczy, globalnie dostępny obiekt, który wykorzystywany jest jako przykład. Obiekt ten mo e być two- rzony za pomocą specjalnego konstruktora w celu odró nienia go od „zwykłych” obiektów tej klasy. Istnienie przykładów bywa często przydatne w przypadku klas listu tak, by przykład koperty mógł posiadać referencję instancji listu. Przykład listu obsługuje ądania utworzenia obiektów — wszystkie wywołania funkcji OCMG przekazywane są do obiektu listu za pośrednictwem operatora zdefiniowanego w klasie kopertowej. Przykład listu mo e być specjalną instancją ogólnej klasy bazowej listu (klasy .GVVGT), jeśli nie jest ona klasą abstrakcyjną. W przeciwnym wypadku mo emy utworzyć spe- cjalną klasę pochodną, która posiadać będzie domyślne definicje funkcji czysto wirtu- alnych i tworzyć pojedynczy obiekt (singleton) przykładu listu. Klasy posiadające symboliczną postać kanoniczną u ywane są w taki sam sposób jak zliczane wskaźniki i obiekty przykładów — czyli za pomocą operatora zamiast operatora kropkowego. A oto przykład prostej aplikacji ilustrującej sposób posługi- wania się naszymi wzorcowymi klasami 'PXGNQRG i .GVVGT: UVCVKE 'PXGNQRG GPXGNQRG'ZGORNCT nie jest wykorzystywany bezpośrednio 'PXGNQRG
  • 69.
    GPXGNQRG GPXGNQRG'ZGORNCT KPV OCKP ] 'PXGNQRG QXGTPKIJVGT
  • 70.
    GPXGNQRG OCMG QXGTPKIJVGT UGPF #FFKUQP9GUNG[ 4GCFKPI /# 'PXGNQRG CETQUUVQYP
  • 71.
    GPXGNQRG OCMG QXGTPKIJVGT CETQUUVQYP CETQUUVQYP UGPF #PIYCPVKDQ $QUVQP %QOOQP TGVWTP _
  • 72.
    Rozdział 9. ♦Emulacja języków symbolicznych w C++ 303 W ten sposób omówiliśmy podstawowe aspekty symbolicznej postaci kanonicznej. Aby zaakcentować i poszerzyć przedstawione dotąd motywacje, przedstawiamy poni ej zbiór zasad związanych ze stosowaniem tego idiomu: 1. Wszystkie referencje klasy 'PXGNQRG powinny wykorzystywać operator , a nie zapis kropkowy. Operator automatyzuje bowiem przekazywanie operacji do klasy .GVVGT. 2. Funkcje składowe klas listu powinny być wirtualne. Wirtualne funkcje składowe mogą być łatwo ładowane przyrostowo, co zostanie opisane szczegółowo w dalszej części bie ącego rozdziału. 3. Funkcja składowa OCMG wykonuje zadania konstruktora. Sam konstruktor nie wykonuje adnych operacji związanych z inicjacją obiektów. Rozwiązanie takie jest konieczne, jeśli chcemy zachować mo liwość zastępowania kodu inicjacji obiektów podczas wykonania programu, poniewa jedynie funkcje wirtualne mogą być aktualizowane. Konstruktory są nadal obecne w klasach pochodnych klasy 6JKPI (ich istnienie jest konieczne dla działania mechanizmu funkcji wirtualnych), ale nie powinny zawierać adnego kodu definiowanego przez u ytkownika. 4. Ka da klasa powinna posiadać pojedynczy, stały obiekt przykładu. Obiekt przykładu musi być łatwy do zidentyfikowania (na przykład na skutek utworzenia go przez specjalny konstruktor). Dostęp do przykładu nie powinien odbywać się bezpośrednio, a jedynie za pomocą wyznaczonego w tym celu wskaźnika. Przyczyny takiego rozwiązania zostaną omówione w dalszej części rozdziału. 5. Funkcje składowe EWVQXGT 6JKPI
  • 73.
    klas pochodnych klasy6JKPI wykorzystywane są przez mechanizm dynamicznego ładowania kodu podczas wykonywania programu. Parametrem tych funkcji jest wskaźnik obiektu klasy, do której nale ą. Zadanie funkcji EWVQXGT polega na przekształceniu obiektu istniejącej klasy w obiekt nowej klasy ró niącej się formatem, układem składowych i ich typem. Jeśli funkcja EWVQXGT nie potrafi tego dokonać, to mo e wykorzystać pewne sztuczki udostępniane przez środowisko, aby mimo wszystko dokonać konwersji obiektu (symulować jednokierunkową właściwość BECOMES dostępną w języku Smalltalk). Działanie funkcji EWVQXGT zostanie omówione szczegółowo w dalszej części rozdziału. 6. Operator PGY nie mo e być u ywany dla klasy 'PXGNQRG, dlatego te zadeklarowany jest jako prywatny. Próba dynamicznego utworzenia obiektu klasy 'PXGNQRG spowoduje błąd kompilacji. Koperty powinny być deklarowane jedynie jako zmienne automatyczne, składowe innych klas lub, w ostateczności, jako zmienne globalne. Wyeliminowanie wskaźników zwalnia programistę z obowiązku usuwania nieu ywanych obiektów, dzięki czemu nawet elastyczne, polimorficzne typy w rodzaju klasy 0WODGT mogą być u ywane jak konkretne typy danych (czyli tym samym jak typy podstawowe wbudowane w język, np. typ KPV).
  • 74.
    304 C++. Styl i technika zaawansowanego programowania Idiom symboliczny wspomaga in yniera oprogramowania, poniewa zarządzanie pamięcią zostaje zautomatyzowane w znacznym stopniu przez klasę kopertową, która śledzi referencje obiektów. Dostęp do danych i funkcji odbywa się za pośrednictwem dodatkowej warstwy, dzięki czemu ich u ytkownicy są mnie nara eni na wpływ poja- wiających się zmian. Efekt domina wywołany przez modyfikacje klasy zostaje w znacz- nym stopniu ograniczony i tym samym ponowna kompilacja kodu wymagana jest na du o mniejszą skalę. Jeśli dysponujemy odpowiednimi narzędziami konsolidacji i łado- wania kodu, to technika taka mo e nawet zostać u yta do przeprowadzenia przyro- stowych modyfikacji klas działającego programu, wymagając od niego jedynie zmiany konfiguracji uwzględniającej nową klasę. Opisane rozwiązanie po raz kolejny zwiększa stopień polimorfizmu w programach tworzonych w języku C++, udostępniając typy parametryzowane podczas działania programu oraz pewien rodzaj ogólnej klasy. Idiom symboliczny stwarza iluzję, e charakterystyka typów mo e być zmieniana podczas wykonywania programu. Poni ej mo liwość ta zostanie omówiona szczegółowo wraz z odpowiednimi przykładami. 9.3. Przykład — ogólna klasa kolekcji Rozwa my przykład programu, który będzie u ywać zamiennie trzech ró nych rodzajów kontenerów: opartego na tablicy i indeksach w postaci wartości całkowitych, wyko- rzystującego B-drzewa i stosującego tablice mieszające. W tym celu zdefiniujemy klasę %QNNGEVKQP, która zawierać będzie wskaźnik do jednego z wymienionych obiektów wewnętrznych. Aby uzyskać dodatkową elastyczność, klasa %QNNGEVKQP zdefiniowana zostanie jako szablon tak, by jego instancje mogły przechowywać obiekty dowolnego wybranego typu. Deklaracja: %QNNGEVKQP$QQM #WVJQT NKDTCT[ tworzy kolekcję obiektów klasy $QQM indeksowanych za pomocą obiektów klasy #WVJQT. Obiekty listu w naszym przykładzie tworzone będą jako obiekty klas pochodnych klasy %QNNGEVKQP4GR. Klasy te będą charakteryzować warianty (patrz podrozdział 7.7) klasy %QNNGEVKQP4GR. W tym przypadku występować będą trzy warianty klasy %QNNGEVKQP4GR — #TTC[, $VTGG i *CUJ6CDNG — słu ące jako alternatywne rodzaje kontenerów wyko- rzystywane przez klasę %QNNGEVKQP. Większość funkcji składowych klasy %QNNGEVKQP4GR i jej klas pochodnych powinna być wirtualna, dzięki czemu wywołania funkcji skła- dowych przez obiekt klasy %QNNGEVKQP za pośrednictwem wskaźnika klasy %QNNGEVKQP4GR będą powodować wykonanie funkcji składowych odpowiedniej klasy #TTC[, $VTGG lub *CUJ6CDNG. Interfejs klasy %QNNGEVKQP4GR musi zawierać deklaracje wszystkich funkcji składowych wymienionych klas. Klasa %QNNGEVKQP dysponować będzie bowiem wyłącz- nie wiedzą o funkcjach składowych zadeklarowanych przez klasę %QNNGEVKQP4GR. Ponie- wa nie wszystkie klasy pochodne będą dysponować własną implementacją wszyst- kich tych metod, to przydatnym mechanizmem mo e okazać się obsługa wyjątków w sytuacji, gdy wywołana zostanie nieprawidłowa funkcja składowa. Na przykład dostęp do elementów klasy #TTC[ mo e odbywać się jedynie za pomocą indeksu przyj- mującego wartości całkowite; tablice mieszające klasy *CUJ6CDNG mogą wykorzystywać
  • 75.
    Rozdział 9. ♦Emulacja języków symbolicznych w C++ 305 wartości całkowite podczas wyszukiwania indeksowego lub łańcuchy znakowe podczas wyszukiwania asocjacyjnego. Jeśli więc obiekt klasy %QNNGEVKQP wykorzystuje obiekt klasy #TTC[, to wywołanie QRGTCVQT=? 5 powinno spowodować wyrzucenie wyjątku. Jest to cena, jaką musimy zapłacić za elastyczność stylu programowania symbolicznego. Listing 9.6 przedstawia szkielet deklaracji klasy %QNNGEVKQP4GR, która jest klasą bazową dla klas listu w naszym przykładzie. Klasy nale ące do hierarchii dziedziczenia klasy %QNNGEVKQP4GR u ywają dość niezwykłego sposobu implementacji funkcji składowej V[RG. Poniewa klasa %QNNGEVKQP jest klasą parametryczną, to tworzenie na jej podstawie nowej klasy wymagać zawsze własnego przykładu (zazwyczaj administrowanego ręcznie), w skutek czego funkcja składowa V[RG nie mo e zwrócić po prostu globalnej wartości wskaźnika. Zamiast tego operacja OCMG danego przykładu umieszcza adres przykładu wewnątrz ka dego z tworzonych obiektów (składowa GZGORNCT2QKPVGT). I właśnie tę wartość zwraca funkcja składowa V[RG. Na przykład: ENCUU %NCUUGTKXGF(TQO%QNNGEVKQP4GR6 5 RWDNKE %QNNGEVKQP4GR6 5 ] %QNNGEVKQP6 5 OCMG ] %QNNGEVKQP6 5 PGY1DLGEV PGY1DLGEVGZGORNCT2QKPVGT VJKU TGVWTP PGY1DLGEV _ _ 6JKPI
  • 76.
    %QNNGEVKQP4GRV[RG ] TGVWTPGZGORNCT2QKPVGT _ Klasa %QNNGEVKQP4GR sama w sobie stanowi u yteczną abstrakcję i mo e zostać bez- pośrednio wykorzystana przez inny idiom. Logiczna hermetyzacja hierarchii klasy %QNNGEVKQP4GR wewnątrz klasy %QNNGEVKQP ma dwie zalety. Po pierwsze, pozwala klasie %QNNGEVKQP zmieniać sposób reprezentacji kolekcji na ądanie podczas wykonywania programu. Mo liwe jest tak e przypisanie kolekcji jednego typu kolekcji innego typu, dzięki czemu ró ne kolekcje mogą być praktycznie u ywane wymiennie. Kolekcje o znacznych rozmiarach mogą u ywać wartości progowych lub innych kryteriów, decydując się na zmianę swojego typu podczas działania programu w celu poprawy efektywności (na przykład zmieniając klasę #TTC[ na *CUJ6CDNG). Po drugie, klasa 6QR będąca klasą bazową klasy %QNNGEVKQP u ywa atrybutów klasy 6JKPI będącej klasą bazową klasy kopertowej w celu implementacji zarządzania pamięcią w sposób cha- rakterystyczny dla idiomu przykładu. Szkielet klasy %QNNGEVKQP4GR przedstawiony na listingu 9.6 ilustruje omówione kon- cepcje. Dziedziczy on mechanizm zliczania referencji po swojej klasie bazowej 6JKPI. Listing 9.6. Klasa bazowa klas listu wykorzystywanych przez klasę Collection KPENWFG MJ KPENWFG EQNNGEVKQPJ Kolekcja elementów klasy T indeksowanych wartościami klasy S
  • 77.
    306 C++. Styl i technika zaawansowanego programowania VGORNCVGENCUU 6 ENCUU 5 ENCUU %QNNGEVKQP4GR RWDNKE 6JKPI ] RWDNKE XKTVWCN %QNNGEVKQP6 5 OCMG XKTVWCN 6JKPI
  • 78.
  • 79.
    XKTVWCN 6 QRGTCVQT=? KPV XKTVWCN 6 QRGTCVQT=? 5 XKTVWCN XQKF RWV EQPUV 6 %QNNGEVKQP4GR ] _ `%QNNGEVKQP4GR ] _ 6JKPI
  • 80.
    V[RG RTQVGEVGF HTKGPF ENCUU %QNNGEVKQP6 5 UVCVKE XQKF
  • 81.
    QRGTCVQT PGY UKGAV N] TGVWTP QRGTCVQT PGY N _ UVCVKE XQKF QRGTCVQT FGNGVG XQKF
  • 82.
    R ] QRGTCVQT FGNGVG R _ RTKXCVG %QNNGEVKQP4GR6 5
  • 83.
    GZGORNCT2QKPVGT _ Klasa %QNNGEVKQP przedstawiona została na listingu 9.7. Jej zadania sprowadzają się do zarządzania pamięcią w podstawowym zakresie oraz obsługi operacji przypisania, a pozostałe operacje przekazywane są klasie listu. Klasa %QNNGEVKQP mo e tworzyć lub wymieniać obiekty listu dowolnej klasy, opierając się na własnych kryteriach. Progra- mista mo e wyposa yć ją równie w dodatkowe rodzaje konstruktorów umo liwiające u ytkownikowi wybór sposobu reprezentacji tworzonej kolekcji. Listing 9.7. Klasa Collection KPENWFG MJ VGORNCVGENCUU 6 ENCUU 5 ENCUU %QNNGEVKQP4GR VGORNCVGENCUU 6 ENCUU 5 ENCUU %QNNGEVKQP RWDNKE 6QR ] RWDNKE %QNNGEVKQP4GR6 5
  • 84.
    QRGTCVQT EQPUV] TGVWTP TGR _ %QNNGEVKQP %QNNGEVKQP %QNNGEVKQP4GR6 5 `%QNNGEVKQP %QNNGEVKQP %QNNGEVKQP6 5 %QNNGEVKQP QRGTCVQT %QNNGEVKQP6 5 6 QRGTCVQT=? KPV K ] TGVWTP
  • 85.
    TGR=K? _ 6 QRGTCVQT=? 5 U ] TGVWTP
  • 86.
    TGR=U? _ RTKXCVG UVCVKE XQKF
  • 87.
    QRGTCVQT PGY UKGAV ]TGVWTP _ UVCVKE XQKF QRGTCVQT FGNGVG XQKF
  • 88.
    R ] QRGTCVQT FGNGVG R _ %QNNGEVKQP4GR6 5
  • 89.
  • 90.
    Rozdział 9. ♦Emulacja języków symbolicznych w C++ 307 Klasy pochodne klasy %QNNGEVKQP4GR przedstawione zostały na listingu 9.8. Ka dy obiekt klasy %QNNGEVKQP musi zawierać list klasy #TTC[, $VTGG lub *CUJ6CDNG. Ka da z klas pochodnych przesłania te funkcje klasy %QNNGEVKQP4GR, które mają sens w jej przypadku, natomiast pozostałe operacje posiadają domyślną implementację w klasie %QNNGEVKQP4GR sprowadzającą się do wygenerowania wyjątku. Zauwa my, e skoro klasy pochodne wybiórczo przesłaniają jedynie podzbiór funkcji klasy bazowej, to funkcje te nie mogą być zadeklarowane w klasie %QNNGEVKQP4GR jako funkcje czysto wirtualne. Listing 9.8. Klasy implementujące kolekcje VGORNCVGENCUU 6 ENCUU 5 ENCUU #TTC[ RWDNKE %QNNGEVKQP4GR6 5 ] RWDNKE #TTC[ #TTC[ #TTC[6 5 `#TTC[ ENCUU %QNNGEVKQP6 5 OCMG ENCUU %QNNGEVKQP6 5 OCMG KPV UKG 6 QRGTCVQT=? KPV K XQKF RWV EQPUV 6 RTKXCVG 6
  • 91.
    XGE KPV UKG _ VGORNCVGENCUU 6 UVTWEV *CUJ6CDNG'NGOGPV ] *CUJ6CDNG'NGOGPV
  • 92.
  • 93.
    GNGOGPV _ VGORNCVGENCUU 6 ENCUU 5 ENCUU *CUJ6CDNG RWDNKE %QNNGEVKQP4GR6 5 ] RWDNKE *CUJ6CDNG *CUJ6CDNG *CUJ6CDNG6 5 `*CUJ6CDNG ENCUU %QNNGEVKQP6 5 OCMG ENCUU %QNNGEVKQP6 5 OCMG KPV 6 QRGTCVQT=? KPV K 6 QRGTCVQT=? 5 XQKF RWV EQPUV 6 RTKXCVG KPV PDWEMGVU XKTVWCN KPV JCUJ KPV N *CUJ6CDNG'NGOGPV6
  • 94.
    DWEMGVU _ Zarządzanie pamięcią dla obiektów tych klas wykorzystuje zwykle mechanizm zliczania referencji. Tak e i w tym przypadku klasy przedstawione w podrozdziale 3.5 stanowią dobry przykład zastosowania idiomu listu i koperty dla zliczania referencji. W pod- rozdziale tym przedstawione zostały sposoby manipulacji licznikiem referencji będą- cym składową obiektu listu klasy 5VTKPI4GR przez operator przypisania, konstruktor oraz destruktor oraz usuwania obiektu, gdy licznik osiąga wartość 0. Te same zasady
  • 95.
    308 C++. Styl i technika zaawansowanego programowania obowiązują w przypadku klasy %QNNGEVKQP4GR. W podrozdziale 9.5 przedstawimy alternatywne rozwiązania dla mechanizmu zliczania referencji wykorzystywane pod- czas odzyskiwania nieu ytków. Przedstawione rozwiązanie mo e być efektywnie stosowane dla zwiększenia stopnia polimorfizmu — umo liwia na przykład utworzenie sumy dwóch kolekcji o dowolnym sposobie wewnętrznej reprezentacji. Jednak podczas jego stosowania na programistę czyha wiele pułapek. W szczególności, jeśli klasy u ywane w ten sposób zawierają operacje binarne (na przykład sumowanie dwóch kolekcji za pomocą operatora ), to muszą poradzić sobie z sytuacją, w której obiekty przekazane operacji mają ró ne typy (na przykład połączenie obiektów #TTC[ i $VTGG). Zaprojektowanie klas o takich mo - liwościach mo e być pracochłonne i wią e się z dodatkowym narzutem związanym z koniecznością rozpoznawania typu obiektów i wywoływania odpowiednich operacji (patrz przykład klasy 0WODGT w podrozdziale 5.5 na stronie 140 oraz dyskusja w pod- rozdziale 9.7). Teoretycznie moglibyśmy zdefiniować niezbędne konwersje na poziomie klasy 6QR lub 6JKPI, uzyskując w ten sposób pełen polimorfizm. Jednak definicje takich klas stałyby się bardzo skomplikowane i wymagałyby modyfikacji za ka dym razem, gdy dodawana byłaby nowa operacja. Aby zdefiniować więcej ni jeden zewnętrzny interfejs, mo emy u yć tak e dziedziczenia wielokrotnego (na przykład klasa .KUV wykorzystująca reprezentację za pomocą klasy #TTC[ lub .KPMGF.KUV i współdzieląca klasę #TTC[ z klasą %QNNGEVKQP). Rysunek 9.1 przedstawia hierarchię klas dla takiego przypadku. Zwróćmy uwagę, e poziom komplikacji takiego rozwiązania równie wzrasta bardzo szybko. Rysunek 9.1. Hierarchia klas powstała przez zastosowanie dziedziczenia wielokrotnego 9.4. Kod i idiomy obsługujące mechanizm ładowania przyrostowego Jeśli system dysponuje przyrostowym konsolidatorem, to często mo na równie za- implementować program ładujący pozwalający dodawać nowy kod do działającego programu. Listing 9.9 przedstawia prostą funkcję NQCF, której parametrem jest nazwa pliku wynikowego zawierającego relokowalny kod uzyskany w procesie kompilacji. Funkcja ta ładuje ten kod i uruchamia go. Załó my, e plik wynikowy incr.o zawiera tylko jedną funkcję, a jego punkt wejściowy znajduje się na początku sekcji tekstowej. Mo emy wtedy załadować i wykonać kod tej funkcji podczas działania programu za pomocą następującego wywołania: KPV OCKP ] V[RGFGH XQKF
  • 96.
    2( wskaźnik funkcji 2( CPGYHWPE 2( NQCF KPETQ
  • 97.
    CPGYHWPE TGVWTP _
  • 98.
    Rozdział 9. ♦Emulacja języków symbolicznych w C++ 309 Program ten będzie działał na większości platform firmy Sun Microsystems, przy czym musi zostać załadowany z opcją P. Podobny kod mo na napisać dla większości dostęp- nych obecnie systemów operacyjnych. Listing 9.9. Funkcja ładująca plik wynikowy zawierający kod pojedynczej funkcji (platforma Sun) KPENWFG CQWVJ KPENWFG HEPVNJ KPENWFG U[UV[RGUJ ECFFTAV NQCF EQPUV EJCT
  • 99.
    HKNGPCOG ] EJCT DWH=? ECFFTAV QCFZ ECFFTAVUDTM ECFFTAV CFZ EJCT
  • 100.
    QCFZ 2#)5+ NQPIQCFZ 2#)5+ URTKPVH DWH NF 0 6VGZV : # CQWV U Q CQWVPGY CFZ HKNGPCOG U[UVGO DWH KPV HF QRGP HKNGPCOG 1A410.; GZGE 'ZGE TGCF HF EJCT
  • 101.
    'ZGE UKGQH GZGE UDTM 2#)5+ NQPIQCFZ 2#)5+ ECFFTAV NFCFZ ECFFTAVUDTM 'ZGECAVGZV 'ZGECAFCVC 'ZGECADUU TGCF HF NFCFZ 'ZGECAVGZV 'ZGECAFCVC ENQUG HF TGVWTP NFCFZ _ Kod ładowania funkcji mo emy umieścić, na przykład, w interaktywnym programie w języku C++, umo liwiając u ytkownikowi ładowanie nowych funkcji podczas dzia- łania programu. Gdy program znajduje się w stanie oczekiwania, mo emy skompilować kod nowej funkcji, a następnie za ądać od programu załadowania jej relokowalnego kodu. Następnie program musi jeszcze wykonać dodatkowe działania w celu powią- zania funkcji z istniejącym kodem, po czym mo e kontynuować swoje działanie. Kiedy używać tego idiomu? Idiomy przedstawione w tym podrozdziale opisują kod, który może być ręcznie lub półautoma- tycznie generowany przez środowisko umożliwiające przyrostowy rozwój oprogramowania. Styl ten może być wykorzystywany wszędzie tam, gdzie zachodzi potrzeba zmieniania programu podczas jego działania, a więc szczególnie podczas tworzenia prototypów. Idiom ten jest także przydatny do serwisowania skomplikowanych aplikacji pracujących w trybie ciągłym. W kolejnych podrozdziałach przedstawione zostaną idiomy i struktury umo liwiające dynamiczne ładowanie kodu do działającego programu. Problem ten mo na podzielić w istocie na dwa podproblemy — mo liwość ładowania nowych funkcji, która zostanie omówiona jako pierwsza, oraz mo liwość konwersji formatów istniejących obiektów.
  • 102.
    310 C++. Styl i technika zaawansowanego programowania Ładowanie funkcji wirtualnych Jednym z zastosowań kodu ładującego jest ładowanie nowych wersji funkcji do dzia- łającego programu. Jak pokazaliśmy przed chwilą, ładowanie funkcji jest dość łatwe pod warunkiem, e generujemy plik wynikowy zawierający wyłącznie nową funkcję. Aby powiązać istniejące wywołania funkcji z jej nową wersją, potrzebne są jednak pewne dodatkowe działania, które zostaną omówione poni ej. W podrozdziale 9.2 omówiona zostanie tablica wskaźników funkcji wirtualnych, która dołączana jest do ka dej klasy. Co więcej, obiekt przykładu dla dowolnej klasy posiada jako swój pierwszy element wskaźnik tej tablicy (XRVT), co gwarantowane jest przez symboliczną postać kanoniczną. (Szczegóły mogą zale eć od implementacji kompi- latora). Oto dlaczego wszystkie klasy kopertowe tworzone są jako pochodne klasy 6QR — klasa ta zawiera wskaźnik funkcji wirtualnej obiektu, który mo e zostać u yty do zaadresowania XVDN1. W środowisku języka C++ niezbędne jest podjęcie środków administracyjnych słu ą- cych zapewnieniu, e program zawiera dokładnie jedną kopię tablicy funkcji wirtual- nych dla ka dej klasy (przy zało eniu, e klasa ta nie powstała na skutek dziedziczenia wielokrotnego). Ten aspekt jest oczywisty w niektórych środowiskach języka C++, ale w innych wymaga specjalnej konfiguracji. Producent kompilatora powinien dostar- czyć odpowiednich informacji umo liwiających wyjaśnienie tej kwestii. Uchwyt tablicy deskryptorów funkcji wirtualnych to jednak ciągle za mało — musimy jeszcze ustalić, który element tej tablicy odpowiada funkcji, której kod zamierzamy zaktualizować. Nie będzie to specjalnie trudne w przypadku, gdy istnieje tablica opisująca odwzorowanie nazw funkcji na wartości indeksu tablicy XVDN. Niestety, dla większości kompilatorów języka C++ odwzorowanie takie nie istnieje. Dlatego te niezbędne jest wykonanie dodatkowych działań na zewnątrz programu, które przeka ą kodowi ładują- cemu informacje niezbędne do zidentyfikowania właściwego elementu tablicy XVDN. Jeden z mo liwych sposobów polega na napisaniu prostej funkcji pomocniczej pro- gramu ładującego, której jedynym zadaniem jest zwrócić wartość będącą agregatem charakteryzującym funkcję, a w tym jej adres oraz indeks w tablicy deskryptorów funkcji wirtualnych. Funkcja taka mo e być zakodowana ręcznie lub generowana automatycznie podczas przygotowań do aktualizacji funkcji. Proces aktualizacji będzie więc odbywać się w dwóch etapach. Pierwszy polegać będzie na załadowaniu przez kod ładujący funkcji pomocniczej i wywołaniu jej w celu uzyskania indeksu tablicy XVDN dla aktualizowanej funkcji. Drugi etap polegać będzie na załadowaniu nowej wersji funkcji i umieszczeniu jej adresu w odpowiednim miejscu tabeli XVDN. Zanim zajmiemy się samym procesem ładowania, musimy najpierw przyjrzeć się wykorzystywanym przez niego strukturom danych. Najpierw zdefiniujemy typ wskaź- ników funkcji: V[RGFGH KPV
  • 103.
    XRVR 1 Rozwiązanie to działa jedynie dla dziedziczenia pojedynczego. Równie ogólne rozwiązanie dla dziedziczenia wielokrotnego jest trudne do opracowania i nie będzie tutaj omawiane.
  • 104.
    Rozdział 9. ♦Emulacja języków symbolicznych w C++ 311 Struktura ORVT reprezentuje element tablicy funkcji wirtualnych dla większości imple- mentacji języka C++: UVTWEV ORVT ] UJQTV F UJQTV K XRVR H _ Deklaracja taka wykorzystywana jest przez środowiska języka C++ bazujące na kom- pilatorze cfront i jest typowa tak e dla innych systemów. Jeśli jednak kompilator u ywa innej struktury, to kod przedstawiony w tym rozdziale musi zostać do niej dopasowany. Pierwsze dwa pola struktury posiadające typ UJQTV reprezentują wartości przesunięć wykorzystywane podczas dziedziczenia wielokrotnego i nie będziemy ich tutaj omawiać (patrz Ellis Stroustrup [1]). Najbardziej interesuje nas pole H, które wskazuje funkcję dla danego elementu tabeli. Implementacja funkcji pomocniczej jest prosta — jej zadanie polega za zwróceniu adresu bie ącej wersji funkcji, którą będziemy zastępować. Definicja funkcji pomoc- niczej mo e więc wyglądać w następujący sposób: GZVGTP XRVR HWPEVKQP#FFTGUU ] kod zale ny od platformy i kompilatora TGVWTP XRVR #TTC[RWV _ Za ka dym razem, gdy ładować będziemy nową funkcję, ładowana będzie najpierw nowa kopia funkcji pomocniczej HWPEVKQP#FFTGUU, która będzie nadpisywać poprzed- nią swoją wersję lub pozostawiać ją jako nieu ytek w przypadku, gdy odzyskiwanie pamięci nie posiada wysokiego priorytetu. Funkcja pomocnicza HWPEVKQP#FFTGUU potrafi rozwiązać niejednoznaczność związaną z ładowaniem funkcji o przecią anym identyfikatorze, stosując odpowiednie rzutowanie. Załó my na przykład, e klasa #TTC[ posiada wiele funkcji RWV: ENCUU #TTC[ ] RWDNKE XQKF RWV KPV FQWDNG XQKF RWV KPV KPV _ Jeśli po lewej stronie operatora przypisania umieścimy zmienną o odpowiednim typie, to operacja taka pozwoli wybrać wersję funkcji, na przykład o parametrze typu FQWDNG: GZVGTP XRVR HWPEVKQP#FFTGUU ] kod zale ny od platformy i kompilatora V[RGFGH XQKF #TTC[
  • 105.
    6;2' KPV FQWDNG 6;2' TGVXCN #TTC[RWV TGVWTP XRVRTGVXCN _ Następna grupa funkcji dodana do klasy 6QR będzie obsługiwać zadania przyrostowego ładowania i aktualizacji funkcji. Zadaniem pierwszej z tych funkcji, EQORCTG(WPEU jest sprawdzenie, czy dwa deskryptory funkcji opisują tę samą funkcję:
  • 106.
    312 C++. Styl i technika zaawansowanego programowania KPV 6QREQORCTG(WPEU KPV XVDNKPFGZ XRVR XVDN(RVT XRVR HRVT ] kod zale ny od platformy i kompilatora TGVWTP XVDNKPFGZ KPVHRVT _ Pierwsze dwa parametry tej funkcji przekazują informację o pozycji tabeli funkcji wirtualnych. Parametr typu KPV jest jej indeksem, a parametr typu XRVR wartością wskaźnika funkcji znajdującą się na tej pozycji (wartością pola ORVTH). Trzeci z para- metrów reprezentuje adres zastępowanej funkcji. Funkcja EQORCTG(WPEU sprawdza, czy adres funkcji opisywanej przez dwa pierwsze parametry jest równy adresowi przeka- zanemu za pomocą trzeciego parametru. Jeśli tak, to funkcja zwraca wartość ró ną od zera. Sposób wykorzystania parametrów funkcji zale y od konkretnej platformy i kom- pilatora. W naszym przykładzie wiemy, e kompilator zwraca indeks tabeli funkcji wirtualnej, jeśli dokonamy rzutowania adresu funkcji wirtualnej na typ KPV. Wobec tego wystarcza jedynie porównać wartości pierwszego i trzeciego parametru funkcji. Drugą z funkcji jest HKPF8VDN'PVT[: ORVT
  • 107.
    6QRHKPF8VDNU'PVT[ XRVR HWPEVKQP#FFTGUU ] kod zale ny od platformy i kompilatora ORVT
  • 109.
  • 111.
    VJKU TGIKUVGT ORVT
  • 112.
  • 113.
    ORR HQT KPV K XVDN=K?H K ] KH EQORCTG(WPEU K XVDN=K?H HWPEVKQP#FFTGUU ] TGVWTP XVDN K _ _ TGVWTP _ Funkcja ta poszukuje w tablicy funkcji wirtualnych dla danego obiektu (wskazywanej przez pierwsze słowo tego obiektu, które stanowi wartość wskaźnika XRVT) pozycji odpowiadającej funkcji, którą zamierzamy zaktualizować (przekazanej jako parametr funkcji). Funkcja HKPF8VDN'PVT[ zwraca wskaźnik odpowiedniej pozycji tablicy funkcji wirtualnych (ORVT), jeśli została ona znaleziona. W tym momencie pozostaje nam jedynie załadować i dowiązać nową funkcję wirtualną. Zadanie to wykonuje funkcja 6QRWRFCVG: GZVGTP % XRVR NQCF EQPUV EJCT
  • 114.
    XQKF 6QRWRFCVG EQPUV EJCT
  • 115.
  • 116.
    NQCFPCOG ] XRVR HKPFHWPE NQCF RTGRPCOG ORVT
  • 117.
  • 118.
    HKPFHWPE XVDN H NQCF NQCFPCOG _ Parametrami tej funkcji są nazwy plików zawierających funkcję pomocniczą oraz funkcję aktualizowaną. U ywając zdefiniowanej wcześniej funkcji NQCF (której typ został zmieniony z ECFFTAV na XRVR), odnajduje ona odpowiednią pozycję tablicy funkcji
  • 119.
    Rozdział 9. ♦Emulacja języków symbolicznych w C++ 313 wirtualnych i zastępuje znajdującą się tam wartość wskaźnika adresem załadowanej właśnie nowej wersji funkcji. Od tego momentu wszystkie wywołania funkcji dotyczą jej nowej wersji. Trochę więcej wysiłku wymaga przystosowanie zaprezentowanego kodu do rozszerzenia klas o zupełnie nowe funkcje wirtualne (patrz ćwiczenia na końcu tego rozdziału). Roz- wiązanie takie wymaga tak e bardziej zaawansowanych narzędzi zarządzania konfi- guracją zapewniających poprawność semantyczną oraz umo liwiających przyrosto- wość. Dodanie nowej funkcji wirtualnej nie sprowadza się bowiem wyłącznie do roz- szerzenia tablicy XVDN. Problem mo e polegać, na przykład, na dodaniu nowej funkcji wirtualnej, której nazwa przesłaniać będzie istniejącą dotąd funkcję globalną. W jaki sposób nale y zarządzać w takiej sytuacji ponowną kompilacją i ładowaniem kodu? Aktualizacja struktury klasy i funkcja cutover Poszerzenie technik przyrostowej aktualizacji o mo liwości zmiany struktury danych zwiększa w istotny sposób elastyczność środowiska rozwoju lub serwisowania produktu programistycznego. Zagadnienie to jest jednak bardziej skomplikowane ni aktualiza- cja funkcji, która swą prostotę zawdzięcza istnieniu pośredniego poziomu dostępu do takich funkcji, który nie istnieje w przypadku danych. Poziomem takim dysponuje natomiast idiom listu i koperty i w związku z tym mo emy go u yć w celu modyfikacji struktury danych. W bie ącym podrozdziale przedstawione zostanie rozwiązanie umo li- wiające przyrostową zmianę struktury danych w klasach listu. Poniewa większość kodu aplikacji znajduje się właśnie w klasach listu, to przedstawiona tutaj technika znajduje zastosowanie w większości przypadków, które wymagają zmiany danych składowych klasy. Zastanówmy się, co w praktyce oznacza konieczność modyfikacji struktury klasy, załadowania nowej wersji klasy bądź załadowania nowych funkcji składowych. Ze strukturą klasy jako taką nie jest związany aden kod, ale wiedza o strukturze klasy rozproszona jest równie w kodzie operacji tej klasy. W poprzednim podrozdziale pokazaliśmy, w jaki sposób mo na załadować nową wersję funkcji. Jednak załado- wanie nowej wersji klasy nie sprowadza się jedynie do załadowania nowych wersji funkcji składowych, poniewa struktury istniejących obiektów muszą zostać poddane konwersji do nowego formatu. Aby umo liwić aktualizację klas, ka dy obiekt przykładu musi śledzić obiekty, które tworzone są na jego podstawie. Dzięki temu mo e następnie dokonać ich konwersji, gdy struktura klasy listu ulega zmianie. Do śledzenia tworzonych obiektów przez obiekt przykładu wystarcza zwykła klasa .KUV dostępna w bibliotece języka C++. Obiekt klasy .KUV mo e zostać zadeklarowany jako statyczna składowa klasy 'ZGORNCT. U ytkownik musi dostarczyć funkcji składowej EWVQXGT, która potrafi dokonać konwersji istniejących obiektów do nowej postaci, zachowując ich semantykę. Równie ta funkcja mo e być ładowana przyrostowo. Wywołanie funkcji EWVQXGT po załadowaniu nowej klasy nale y do obowiązków aplikacji. Aplikacja powinna wybrać odpowiedni moment wywołania funkcji EWVQXGT, tak aby system znajdował się w określonym stanie — na przykład wtedy, kiedy wiadomo, e aden z rekordów aktywacji aktualizowanych funkcji nie jest otwarty.
  • 120.
    314 C++. Styl i technika zaawansowanego programowania Istnieje wiele sposobów implementacji funkcji EWVQXGT. Zadaniem poni szych przy- kładów jest przede wszystkim ilustracja zadań, które stoją przed taką funkcją. Dodanie nowego pola do klasy Załó my, e nasze zadanie polega na dodaniu nowego pola do klasy *CUJ6CDNG: VGORNCVGENCUU 6 ENCUU 5 ENCUU *CUJ6CDNG RWDNKE %QNNGEVKQP4GR6 5 ] RWDNKE *CUJ6CDNG *CUJ6CDNG *CUJ6CDNG6 5 6JKPI
  • 121.
    EWVQXGT RTKXCVG KPV PDWEMGVU XKTVWCN KPV JCUJ KPV N *CUJ6CDNG'NGOGPV6
  • 122.
    DWEMGVU *CUJ6CDNG'NGOGPV6
  • 123.
    QXGTHNQY nowepole _ Zauwa my, e nowa składowa została dodana na końcu klasy. Zaletą takiego rozwią- zania jest to, e zazwyczaj kod skompilowany dla poprzedniego interfejsu klasy mo e nadal u ywać obiektów posiadających nowy interfejs. Technika taka mo e równie uprościć algorytm funkcji EWVQXGT. Zauwa my przy tym, e nowa klasa *CUJ6CDNG przesłania tak e funkcję EWVQXGT. Jeśli dotychczasowy układ składowych nie został zmieniony przez dodanie nowego pola, to implementacja funkcji EWVQXGT jest oczywista: 6JKPI
  • 124.
  • 125.
  • 126.
  • 127.
    TGVXCN PGY*CUJ6CDNG kopiuje część pochodzącą z klasy bazowej
  • 128.
  • 129.
  • 130.
  • 131.
    VJKU kopiuje składowe tej klasy TGVXCN DWEMGVU QDLGEV DWEMGVU inicjuje nowe pole TGVXCN QXGTHNQY usuwa stare QDLGEV DWEMGVU FGNGVG QDLGEV zwraca nowy obiekt TGVWTP TGVXCN _ Powy sze zmiany dotyczą szablonu *CUJ6CDNG, w związku z czym wymagane jest ponowne utworzenie jego wszystkich instancji występujących w programie. Inaczej mówiąc, ka da instancja szablonu wymaga osobnej aktualizacji, chocia konwersja kodu źródłowego wykonywana jest tylko raz.
  • 132.
    Rozdział 9. ♦Emulacja języków symbolicznych w C++ 315 Zasadnicze zmiany reprezentacji klasy Jeśli wewnętrzna struktury klasy ulega zmianie w zasadniczy sposób, to aktualizacja istniejących instancji wymaga bardziej zaawansowanych środków. Nowe instancje muszą zostać utworzone na podstawie istniejących instancji i w związku z tym funkcja EWVQXGT wymaga dostępu zarówno do starego, jak i nowego interfejsu klasy. Kopię starego interfejsu musimy zachować pod pewną tymczasową nazwą i w ten sam sposób zmienić wszystkie wystąpienia nazwy klasy w programie. Poniewa funkcja EWVQXGT dysponuje starym i nowym interfejsem klasy, to sposób tworzenia nowych obiektów zale y wyłącznie od jej implementacji. Jako przykład rozpatrzymy klasę 2QKPV: ENCUU 2QKPV RWDNKE 5JCRG4GR ] RWDNKE 5JCRG OCMG FQWDNG Z FQWDNG [ XQKF TQVCVG 5JCRG R obrót dookoła punktu RTKXCVG FQWDNG Z [ _ Zadanie nasze polegać będzie na zmianie klasy 2QKPV tak, by wykorzystywała wartości wyra one w radianach u ywane przez pewien akcelerator graficzny. Wymaganie to nie zmienia interfejsu klasy, wystarczy jedynie poddać konwersji istniejące obiekty i system mo e działać dalej. Nowa deklaracja klasy wygląda następująco: ENCUU 2QKPV RWDNKE 5JCRG4GR ] RWDNKE 5JCRG OCMG FQWDNG T FQWDNG VJGVC XQKF TQVCVG 5JCRG R obrót dookoła punktu 6JKPI
  • 133.
    EWVQXGT RTKXCVG FQWDNG TCFKWU #PING VJGVC tworzony na podstawi wartości typu double _ Mo emy dokonać rzutowania zawartości starego pliku nagłówkowego w kategoriach tymczasowej nazwy klasy, uzyskując przedstawiony poni ej interfejs. Zauwa my, e klasa 2QKPV została zadeklarowana jako klasa zaprzyjaźniona, aby funkcja EWVQXGT miała bezpośredni dostęp do jej składowych. W większości przypadków funkcja EWVQXGT mo e uzyskać potrzebne dane, posługując się publiczną sygnaturą starej klasy. Jednak po jej aktualizacji stare funkcje mogą nie być ju dostępne! ENCUU 1.2QKPV RWDNKE 5JCRG4GR ] HTKGPF 2QKPV RWDNKE 5JCRG OCMG FQWDNG Z FQWDNG [ XQKF TQVCVG 5JCRG R obrót dookoła punktu RTKXCVG FQWDNG Z [ _
  • 134.
    316 C++. Styl i technika zaawansowanego programowania Następnie zaimplementujemy funkcję EWVQXGT dla nowe klasy: 6JKPI
  • 135.
  • 136.
  • 137.
    VJKU 2QKPV
  • 138.
    PGY2QKPV PGY2QKPV PGY2QKPV TCFKWU USTV QNF Z
  • 139.
    QNF Z QNF [
  • 140.
    QNF [ KH CDU QNF Z ] PGY2QKPV VJGVC CVCP
  • 141.
    _ GNUG ] PGY2QKPV VJGVC CVCP QNF [ QNF Z _ KH [ PGY2QKPV VJGVC CVCP
  • 142.
    TGVWTP PGY2QKPV _ Koordynacja ładowania funkcji i konwersji obiektów Aplikacja musi sama skoordynować czynności związane z ładowaniem nowego kodu i konwersją istniejących obiektów. Przede wszystkim musi „wiedzieć”, kiedy wykonanie aktualizacji jest bezpieczne (aby uniknąć jej wykonania w trakcie istotnych operacji), ale tak e pobrać od u ytkownika pewne dane, na przykład nazwy plików zawierających nowy kod. Kod koordynujący te czynności mo e zostać wygenerowany automatycznie dla wielu aplikacji. W bie ącym podrozdziale omówione zostaną pewne aspekty zwią- zane z projektowaniem takiego kodu. Aktualizację kodu prześledzimy na przykładzie znanej ju klasy 2QKPV. Po zakończeniu konwersji kodu źródłowego klasy 2QKPV wszystkie funkcje składowe tej klasy wymagają ponownej kompilacji ze względu na nową strukturę klasy. Dla ka dej funkcji wirtualnej klasy 2QKPV musi zostać napisana funkcja pomocnicza pro- gramu ładującego, a istniejący przykład klasy 2QKPV wywołuje funkcję składową 6QRWRFCVG, aby załadować nowe funkcje. Funkcje te ładowane są po kolei, ka da wraz z funkcją pomocniczą odpowiednią dla danego wywołania funkcji 6QRWRFCVG. Załó my, e wszystkie funkcje zostały załadowane. Wśród nich znajduje się funkcja EWVQXGT umo liwiająca konwersję istniejących instancji do nowego formatu. Ponie- wa wszystkie obiekty przykładów dysponują listą istniejących instancji danej klasy, zadanie konwersji istniejących obiektów klas pochodnych klasy listu nie jest skompli- kowane. Pamiętajmy, e klasy listu zagnie d one są na poziomie koncepcji wewnątrz klasy kopertowej, dzięki czemu wiemy, e tylko jedna klasa obiektów kopert mo e odwoływać się za pomocą wskaźnika TGR do dowolnego obiektu pochodzącego z hie- rarchii klasy listu. Przykład tej klasy kopertowej dysponuje listą instancji klasy i mo e sprawdzać po kolei, jaki typ listu one zawierają. Ka dy obiekt listu, dla którego funkcja V[RG zwróci wskaźnik do aktualizowanego przykładu listu, jest równie kandydatem do aktualizacji. Oznacza to, e mo e on z kolei wywołać operację dla ka dego z tych obiek- tów, aktualizując ich pole TGR tak, by wskazywało przekształconą wcześniej instancję. Załó my na przykład, e aktualizujemy klasę 2QKPV, która jest klasą pochodną klasy 5JCRG4GR. Przykład klasy 5JCRG posiada listę wszystkich istniejących obiektów klasy 5JCRG. Dodatkowo wie tak e, e pole TGR ka dego z tych obiektów wskazuje pewien obiekt nale ący do hierarchii 5JCRG4GR. Przeglądając ka dą instancję U klasy 5JCRG,
  • 143.
    Rozdział 9. ♦Emulacja języków symbolicznych w C++ 317 algorytm aktualizacji koncentruje się na instancjach, dla których spełniony jest waru- nek UTGR V[RG RQKPV, gdzie RQKPV jest wskaźnikiem przykładu klasy 2QKPV. Dla takich obiektów U algorytm zastępuję wartość UTGR wartością UTGR EWVQXGT. W ten sposób wszystkie obiekty kopert zostaną zaktualizowane tak, by posiadały refe- rencje obiektów nowej klasy utworzonych na skutek konwersji obiektów starej klasy. W opisanym sposobie działania niezbędna jest pewna korekta. Jeśli wiele kopert współ- dzieli ten sam list, to stara i nowa wersja zostaną pomieszane i konwersja się nie powie- dzie. Wymagana jest bowiem tylko jedna aktualizacja współdzielonego obiektu listu. W tym celu w ka dym obiekcie klasy 6JKPI nale y umieścić licznik. Za ka dym razem, gdy algorytm aktualizacji odwiedza obiekt tej klasy, musi sprawdzić, czy wartość tego licznika równa jest 0 (wartość początkowa licznika). Jeśli tak, to licznik otrzymuje wartość licznika referencji. Następnie wartość licznika zostaje zmniejszona o 1. Jeśli na skutek tej operacji wartość licznika równa jest 0, to wywoływana jest funkcja EWVQXGT. W przeciwnym razie obiekt jest pomijany. W ten sposób ka dy współdzielony obiekt listu zostaje poddany konwersji dopiero podczas ostatnich odwiedzin przez algorytm aktualizacji. Wspomniane operacje mo emy umieścić wewnątrz nowej funkcji składowej FQEWVQXGT: KPV 6JKPIFQEWVQXGT ] KH WRFCVG%QWPV8CN ] WRFCVG%QWPV8CN TGH%QWPV8CN _ TGVWTP WRFCVG%QWPV8CN _ Funkcja składowa 5JCRGFCVC7RFCVG przedstawiona na listingu 9.10 koordynuje aktu- alizację związaną ze zmianą danych w klasach pochodnych klasy bazowej klasy listu 5JCRG4GR. Jej parametrem jest wskaźnik przykładu aktualizowanej klasy oraz wskaź- nik przykładu, który zajmie jego miejsce. Po wykonaniu opisanego wy ej algorytmu aktualizacji konieczna jest jeszcze aktualizacja obiektu przykładu. W ten sposób pro- gram aktualizacja programu zostaje skompletowana i wykorzystuje on nową wersję klasy. Funkcja FCVC7RFCVG zakłada, e funkcje wirtualne nowej wersji klasy zostały załadowane ju wcześniej. Listing 9.10. Przykład kodu nadzorującego pełen cykl aktualizacji klasy V[RGFGH 6JKPI
  • 144.
    6JKPIR XQKF 5JCRGFCVC7RFCVG 6JKPIR QNF'ZGORNCT EQPUV 6JKPIR PGY'ZGORNCT ] 6JKPI
  • 145.
    UCXG4GR 5JCRG
  • 146.
    UR HQT .KUVKVGT5JCRG
  • 147.
    R CNN5JCRGU RPGZV UR R ] KH UR TGR V[RG QNF'ZGORNCT ] KH R TGR FQEWVQXGT ] UCXG4GR UR TGR UR TGR 5JCRG4GR
  • 148.
    UR TGR EWVQXGT FGNGVG UCXG4GR
  • 149.
    318 C++. Styl i technika zaawansowanego programowania _ _ _ UCXG4GR QNF'ZGORNCT QNF'ZGORNCT PGY'ZGORNCT FGNGVG UCXG4GR _ Ładowanie przyrostowe i autonomiczny konstruktor ogólny Ładowanie przyrostowe staje się efektywnym narzędziem, jeśli połączymy je z auto- nomicznymi konstruktorami ogólnymi opisanymi w podrozdziale 8.3. Autonomiczne konstruktory ogólne umo liwiają specjalizowanym obiektom przykładów (na przykład 0WODGT, 0COG, 2WPEV) rejestrować się w bardziej ogólnym przykładzie (#VQO), zwanym autonomicznym przykładem ogólnym. Specjalizowane przykłady są zwykle pochodnymi klasy autonomicznego przykładu ogólnego. Autonomiczny przykład ogólny działa jak agent wszystkich zarejestrowanych w nim przykładów. Zbiór tych przykładów mo e zmieniać się podczas działania programu. Posługując się ładowaniem przyrostowym, mo emy załadować nową klasę pochodną podczas działania programu, utworzyć jej przykład i zarejestrować go w przykładzie klasy bazowej. Na przykład mo emy dodać klasy $KPCT[1R i 7PCT[1R do parsera działa- jącego systemu, utworzyć ich przykłady za pomocą odpowiednich konstruktorów i za- rejestrować je w przykładzie klasy #VQO. Od tego momentu system będzie umiał parsować wyra enia zawierające operatory unarne i binarne wszędzie tam, gdzie przykład klasy #VQO będzie u ywany do analizy składni. 9.5. Odzyskiwanie nieużytków W większości przypadków języki programowania symbolicznego zwalniają programistę z obowiązku zarządzania pamięcią. Jeśli w programie przestają istnieć wszystkie refe- rencje pewnego obiektu, to zajmowana przez niego pamięć zostaje automatycznie odzy- skana podczas działania programu, z pomocą systemu operacyjnego i (lub) platformy sprzętowej. Mechanizm ten nosi nazwę odzyskiwania nieu ytków. Odzyskiwanie nie- u ytków stwarza iluzję dysponowania nieskończenie pojemną pamięcią wobec czego programiści nie muszą pamiętać o usuwaniu obiektów, które nie są ju potrzebne. Jeśli system wykryje, e dany obiekt nie jest ju w aden sposób wykorzystywany, to zajmo- wana przez niego pamięć zostaje odzyskana i mo e być przydzielona innym obiektom. Działanie takiego mechanizmu jest niewidoczne dla u ytkownika programu. Idiomy zliczania referencji przedstawione w podrozdziale 3.5 stanowią słabą formę mechanizmu odzyskiwania nieu ytków. W szczególności idiom zliczanych wskaźni- ków (strona 73) oferuje przezroczystość zarządzania pamięcią charakterystyczną dla środowisk programowania symbolicznego. Jednak algorytmy odzysku pamięci bazu- jące na zliczaniu referencji nie potrafią odzyskiwać pamięci w przypadku występo- wania cyklicznie referencyjnych struktur danych, chyba e wykorzystują kosztowne
  • 150.
    Rozdział 9. ♦Emulacja języków symbolicznych w C++ 319 techniki skanowania rekurencyjnego. Mechanizmy odzyskiwania nieu ytków wykorzy- stywane w środowiskach programowania w językach wysokiego poziomu nie posia- dają takiego ograniczenia i rzadko u ywają zliczania referencji. Idiom odzyskiwania nieu ytków przedstawiony w tym podrozdziale stanowi alternatywę dla technik zliczania referencji. Techniki, które nie wykorzystują zliczania referencji, posiadają równie zalety z punktu widzenia zastosowań we wbudowanych systemach czasu rzeczywistego. W systemach tych wyjątkowe zdarzenia takie jak awaria procesora mogą spowodować przejście aplikacji w stan regeneracji, w którym odzyskanie pamięci mo e nie być łatwe. Jeśli proces działa błędnie i musi zostać usunięty, to mo e on nie zdołać wywołać odpo- wiednich destruktorów. Jeśli proces ten współdzieli obiekty, dla których zliczane są referencje z innymi procesami, to liczniki referencji tych obiektów nigdy nie osiągną wartości zero i zajmowana przez nie pamięć nie zostanie odzyskana. W takiej sytuacji wymiatanie okazuje się bardziej elastyczną techniką ni zliczanie referencji, poniewa potrafi odzyskać zdecydowanie więcej nieu ywanych zasobów. Większość środowisk programowania symbolicznego ukrywa szczegóły mechanizmu odzysku nieu ytków w implementacji kompilatora oraz środowiska wykonywania programów. Chocia niektóre wczesne środowiska programowania w języku Smalltalk wykorzystują technikę zliczania referencji, to jednak programista aplikacji nie musi pisać w tym celu adnego kodu. Stanowi to oczywisty kontrast z sytuacją w języku C++ przedstawioną w podrozdziale 3.5, gdzie zarządzanie pamięcią zostało zaimplemen- towane jako idiom języka C++ i nie jest wobec tego ukryte wewnątrz implementacji kompilatora i środowiska wykonania programu. Niektóre schematy odzyskiwania u ytków wykorzystują specjalizowane mo liwości sprzętu polegające na mo liwości uzyskania informacji, czy dane słowo pamięci reprezentuje aktywny wskaźnik obiektu czy po prostu fragment danych. Niezale nie od tego, czy wykorzystywana jest imple- mentacja kompilatora, mo liwości systemu operacyjnego lub wyspecjalizowanego sprzętu, zarządzanie pamięcią w językach symbolicznych odbywa się na poziomie ni szym od kodu źródłowego tworzonego przez programistę aplikacji. Taka przezro- czystość jest właściwie domniemana zawsze, gdy pojawia się termin odzyskiwanie nieu ytków. Poniewa język C++ sam w sobie działa na dość niskim poziomie, to nie jest mo liwe zupełne ukrycie w nim działania mechanizmu odzyskiwania nieu ytków. Jednak stosując pewne konwencje oraz dostarczając kod, który tworzy środowisko odzyskiwania nieu ytków, programy w języku C++ mogą osiągnąć wysoki stopień przezroczystości mechanizmu odzyskiwania nieu ytków dla wybranych klas. Technika, która zostanie tutaj przedstawiona, przypomina pod względem stopnia prze- zroczystości mechanizm zliczania referencji omówiony w podrozdziale 3.5, ale oddziela proces odzyskiwania pamięci obiektu od jego „odłączenia” od ostatniej referencji. Technika ta nie pozwala odzyskiwać pamięci w przypadku istnienia cyklicznych refe- rencji. Jej efektywność jest nieco gorsza ni w przypadku zliczania referencji, ale jej zło oność zale y w du ym stopniu od sposobu u ycia oraz szczegółów implementacji. Odmiany prezentowanej techniki umo liwiają przyrostowe odzyskiwanie nieu ytków, dzięki czemu zasadnicze przetwarzanie nie wymaga zawieszania na zbyt długie okresy czasu we celu odzyskiwania pamięci. W przeciwieństwie do tradycyjnych rozwiązań
  • 151.
    320 C++. Styl i technika zaawansowanego programowania odzyskiwania nieu ytków omawiana technika pozwala programiście powiązać z odzy- skiwaniem zasobów obiektu pewne dodatkowe czynności — mechanizm odzyskiwania nieu ytków mo e wywoływać destruktory odzyskiwanych obiektów. Dotychczas opracowano szereg algorytmów odzyskiwania nieu ytków. Jednym z pierw- szych był algorytm „znacz i zamiataj” [2]. Jego działanie polegało na analizie wszystkich istniejących obiektów w celu wykrycia zawieranych przez nie referencji innych obiektów. W procesie tym wszystkie obiekty, do których istniały referencje, zostawały odpowied- nio oznaczone. W kolejnym przebiegu usuwane były wszystkie obiekty, które nie zostały oznaczone w pierwszym przebiegu. Kopiowanie półprzestrzeni jest techniką zapewniającą całkowite odzyskanie zasobów zajmowanych przez obiekty, do których nie istnieją ju adne referencje. Implementacją tej techniki jest algorytm Bakera [3]. Pozwala on uniknąć opóźnienia charakterystycznego dla metody „znacz i zamiataj” za cenę większych wymagań odnośnie pamięci. Algorytm Bakera dzieli pamięć na dwie połowy: A i B. Przestrzeń A zwana jest zwykle docelową, a B przestrzenią źródłową z powodów, które wyjaśnią się poni ej. Nowe obiekty two- rzone są w przestrzeni A. W pewnym momencie (gdy przestrzeń A zapełni się obiektami lub system nie jest obcią ony) wszystkie osiągalne obiekty przestrzeni A umieszczane są w sposób ciągły w przestrzeni B, przy czym wskaźniki tych obiektów są aktuali- zowane. Od tego momentu przestrzeń A nie zawiera ju adnych wykorzystywanych obiektów — jej zawartość stanowią same nieu ytki. W ten sposób role przestrzeni A i B ulegają zamianie z ka dym cyklem algorytmu. Jednak algorytm Bakera (i większość technik odzyskiwania nieu ytków, które nie wyko- rzystują zliczania referencji) wymaga rozró nienia, czy dany obszar pamięci reprezentuje pewne dane czy te referencję (wskaźnik) obiektu. W języku C++ nie jest mo liwe ustalenie, czy dane słowo pamięci jest wartością typu int czy wskaźnikiem. Ostatnio opublikowano nowe techniki odzyskiwania nieu ytków bazujące na algorytmie „znacz i zamiataj”, które umo liwiają prawie zupełne odzyskiwanie pamięci. Jedyny problem w ich przypadku stanowią przypadkowe synonimy wskaźników czyli wartości, których reprezentacja przypadkowo równowa na jest reprezentacji adresu pewnego obiektu, chocia w rzeczywistości reprezentują pewną wartość całkowitą lub jeszcze coś innego. Jednak algorytmy te nie pozostawiają „wiszących” referencji pod warunkiem, e progra- mista przestrzega pewnych zasad. Przykład takiego algorytmu przedstawia Caplinger [4]. Jednak poruszając się w świecie symbolicznych kopert i listów, mo emy zidentyfikować wszystkie obiekty danej klasy kopertowej (wykorzystując listę tworzoną przez przy- kład tej klasy), co pozwoli odnaleźć wszystkie referencje dowolnej klasy listu. Jeśli zmienimy algorytm przydziału pamięci dla klasy listu tak, by u ywał stałej puli (pod- rozdział 3.6), to wtedy typy wszystkich obiektów w puli będą znane (są tego samego typu). Znamy równie wszystkie adresy, pod którymi mogą znajdować się obiekty klasy listu, istnieje więc nadzieja, e mo emy zaznaczyć wszystkie u ywane obiekty, a pozostałe usunąć. Poniewa wszystkie obiekty istniejące w puli są takich samych rozmiarów, to nie musimy martwić się fragmentacją pamięci, natomiast obszar pamięci u ywany przez obiekty ka dej z klas, dla której działa mechanizm odzyskiwania nie- u ytków, musi zostać określony przez programistę.
  • 152.
    Rozdział 9. ♦Emulacja języków symbolicznych w C++ 321 Kiedy używać tego idiomu? Zastosowanie odzyskiwania nieużytków pozwala uwolnić użytkowników od problemu zarządza- nia pamięcią — na przykład podczas szybkiego tworzenia prototypów. Odzyskiwanie nieużytków może być także stosowane jako technika audytu zdarzeń w systemach czasu rzeczywistego. Pomaga ona zagwarantować, że pamięć zostanie odzyskana nawet w przypadku zaistnienia sytuacji wyjątkowej. Rozwiązanie odzyskiwania nieu ytków dla idiomu listu i koperty przedstawimy na przykładzie klasy 6TKCPING. W tym celu bazowa klasa listu musi zostać uzupełniona o znacznik, który będzie kasowany, jeśli obiekt jest „osiągalny” podczas wykonywania algorytmu odzyskiwania. Obiekty tej klasy muszą tak e dysponować znacznikiem informującym, czy obiekt jest u ywany. Bit ten będzie u ywany przez operator PGY w celu znalezienia obszaru, który mo na przydzielić nowemu obiektowi. Początkowo wszystkie wspomniane znaczniki będą skasowane. Podczas tworzenia obiektu usta- wiony zostanie znacznik u ycia. Oprócz tych znaczników ka dy obiekt będzie posia- dać równie znaczniki A i B odpowiadające sytuacji, w której obiekt znajduje się w przestrzeniach A i B algorytmu Bakera. Przykład — figury geometryczne i odzyskiwanie nieużytków W dodatku E przedstawiony został przykład pakietu umo liwiającego rysowanie figur geometrycznych, który u ywa idiomu symbolicznego przedstawionego w tym rozdziale. Przykład ten demonstruje mo liwości ładowania przyrostowego omówione we wcze- śniejszej części rozdziału oraz u ywa zaawansowanej formy odzyskiwania nieu ytków. Technika odzyskiwania nieu ytków nie wykorzystuje w tym przypadku zliczania refe- rencji, lecz utrzymana jest w duchu algorytmu „znacz i zamiataj” oraz algorytmu Bakera. Algorytm odzyskiwania nieu ytków sprawdza dla obiektów dostępnych na liście przykładu klasy 5JCRG, czy ich składowa TGR wskazuje obiekt klasy 6TKCPING (przez zasto- sowanie funkcji składowej V[RG i porównanie zwróconej przez nią wartości a adresem przykładu klasy 6TKCPING). Dla ka dego obiektu, który spełnia ten warunek, algorytm ustawia znacznik. Po zakończeniu tego przebiegu algorytm przegląda wektor o stałym rozmiarze (z którego przydzielane są obiekty klasy 6TKCPING w poszukiwaniu obiektów, które nie posiadają ustawionego znacznika. Jeśli obiekt taki posiada dodatkowo ska- sowany znacznik wykorzystania, to wywoływany jest jego destruktor i kasowany jest znacznik wykorzystania. Na końcu kasowany jest te znacznik u ywany przez algorytm odzyskiwania nieu ytków. Algorytm ten będzie wykonywany cyklicznie. Najpierw przetwarzać będzie wszystkie obiekty klasy 6TKCPING, następnie obiekty klasy .KPG, potem obiekty klasy %KTENG i tak dalej dla wszystkich klas pochodnych klasy 5JCRG4GR, po czym znowu wystartuje dla klasy 6TKCPING. Na wy szym poziomie algorytm mo e być stosowany w ten sposób dla ró nych aplikacji (hierarchii klas 5JCRG, %QNNGEVKQP etc.). Porządek wykonywania cykli określony jest zwykle przez środowisko wykonywania programu i mo e wymagać strojenia dla konkretnej aplikacji. Zastosowanie algorytmu Bakera umo liwia nawet przyrostowe odzyskiwanie nieu ytków (patrz ćwiczenia na końcu tego rozdziału).
  • 153.
    322 C++. Styl i technika zaawansowanego programowania Przyjrzyjmy się szczegółom implementacji algorytmu zamieszczonej w dodatku E. Struktura klasy została ju opisana powy ej, a hierarchia dziedziczenia przedstawiona jest na rysunku 9.2. Szczegóły implementacji znajdują się w klasach pochodnych klasy 5JCRG4GR, których obiekty u ytkownik u ywa jako instancje klasy 5JCRG. Rysunek 9.2. Struktura klas Shape w idiomie symbolicznym Funkcja składowa 5JCRGKPKV inicjuje globalne struktury danych. Inicjacja tych struktur odbywa się w jawny sposób w kodzie, a nie za pomocą domyślnych mechanizmów dostępnych w środowisku języka C++, co ma na celu lepszą kontrolę porządku inicjacji. Funkcja składowa 5JCRGKPKV inicjuje najpierw dwie listy. Pierwsza z nich, CNN5JCRGU, śledzi tworzenie instancji klasy 5JCRG, a druga, CNN5JCRG'ZGORNCTU, spełnia to samo zadanie dla obiektów klas pochodnych klasy 5JCRG4GR. Funkcja 5JCRGKPKV wywołuje następnie operację KPKV dla ka dej z klas pochodnych klasy 5JCRG4GR, które tworzą własne obiekty przykładów. Ka dy przykład rejestruje się w klasie 5JCRG za pomocą funkcji 5JCRGTGIKUVGT, która umieszcza wskaźnik przykładu na liście CNN5JCRG'ZGORNCTU. Właśnie dlatego istotne jest, aby listy klasy 5JCRG zostały zainicjowane, zanim nastąpi inicjacja przykładów. Wadą takiego sche- matu inicjacji jest to, e podczas kompilacji klasy 5JCRG muszą być znane wszystkie klasy pochodne klasy 5JCRG4GR. Po wykonaniu inicjacji u ytkownik mo e za ądać utworzenia obiektu klasy 5JCRG, posługując się idiomem autonomicznego konstruktora ogólnego: 5JCRG QDLGEV
  • 154.
    UJCRG OCMG R RR Operacja OCMG wywoływana jest dla przykładu klasy 5JCRG i zwraca obiekt utworzony na podstawie parametrów podanych przez u ytkownika. U ytkownik przekazuje funkcji OCMG zbiór punktów definiujących figurę geometryczną. Mo e równie przekazać wskaź- nik przykładu, jeśli sama liczba punktów nie wystarcza do określenia rodzaju figury. Para punktów mo e bowiem definiować prostokąt lub odcinek i w takiej sytuacji wskaź- nik przykładu jest niezbędny, aby uniknąć niejednoznaczności. Domyślnym rodzajem figury tworzonym na podstawie trzech punktów jest trójkąt. Zauwa my, e powy sze wywołanie funkcji OCMG mo emy równie wyrazić w nastę- pujący sposób: UJCRG QRGTCVQT OCMG R R R
  • 155.
    Rozdział 9. ♦Emulacja języków symbolicznych w C++ 323 Przykład klasy 5JCRG zwraca wskaźnik swojej wewnętrznej reprezentacji, który wska- zuje instancję klasy 5JCRG4GR jako obiekt, dla którego wywoływana jest metoda OCMG. W przykładach idiomu listu i klasy przedstawionych w podrozdziale 5.5 zarządzanie pamięcią, w tym operacje OCMG, obsługiwane było przez klasę kopertową. W obecnym przykładzie zarządzania pamięcią odbywa się z wnętrza listu. Poniewa funkcja OCMG jest funkcją wirtualną klasy listu, to nowe jej wersje mogą być ładowane przyrostowo w sposób opisany we wcześniejszej części tego rozdziału. Powy sze wywołanie oznacza w efekcie wywołanie funkcji 5JCRG4GROCMG. Funkcja 5JCRG4GROCMG jest funkcją przecią oną i posiada dwie wersje. Parametrami pierwszej z nich są współrzędne punktów reprezentujących figurę oraz wskaźnik przykładu dla danego typu obiektu. Druga wersja wykorzystuje jedynie współrzędne wierzchołków i na podstawie ich liczby zakłada określony typ figury. Implementacja tej wersji spro- wadza się w rzeczywistości do wywołania pierwszej wersji ze wskaźnikiem przykładu odpowiedniego typu. W przypadku wywołania przedstawionego powy ej u yta zostanie druga z wymienionych wersji: 5JCRG 5JCRG4GROCMG %QQTFKPCVG RR %QQTFKPCVG RR %QQTFKPCVG RR ] TGVWTP OCMG RR RR RR VTKCPING _ Wersja ta wywołuje z kolei pierwszą wersję: 5JCRG 5JCRG4GROCMG %QQTFKPCVG RR %QQTFKPCVG RR %QQTFKPCVG RR 6JKPIR V[RG ] TGVWTP 5JCRG4GR
  • 156.
    V[RG OCMG RR RRRR _ Wersja ta wywołuje następnie funkcję OCMG klasy 6TKCPING z parametrami opisującymi wierzchołki trójkąta: 5JCRG 6TKCPINGOCMG %QQTFKPCVG RR %QQTFKPCVG RR %QQTFKPCVG RR ] 6TKCPING
  • 157.
    TGVXCN PGY6TKCPING TGVXCN R RR TGVXCN R RR TGVXCN R RR TGVXCN GZGORNCT2QKPVGT VJKU TGVWTP
  • 158.
    TGVXCN _ Ta funkcja OCMG tworzy najpierw nowy obiekt klasy 6TKCPING za pomocą operatora PGY. Następnie składowe tego obiektu inicjowane są danymi opisującymi wierzchołki trójkąta. Składowa GZGORNCT2QKPVGT inicjowana jest wartością VJKU, która wskazuje w tym przypadku przykład klasy 6TKCPING (VJKU ma w tym kontekście taką samą war- tość jak zmienna VTKCPING, czyli globalny wskaźnik przykładu trójkąta). Kończąc swoje działanie, funkcja zwraca nowo utworzony obiekt. Instrukcja TGVWTP powoduje wywołanie konstruktora 5JCRG 5JCRG4GR w celu konwersji zwracanego obiektu do właściwego typu. Gdy funkcja 6TKCPINGOCMG wywołuje operator PGY w celu utworzenia nowego obiektu klasy 6TKCPING, to wywoływany jest własny operator PGY klasy 6TKCPING:
  • 159.
    324 C++. Styl i technika zaawansowanego programowania XQKF
  • 160.
    6TKCPINGQRGTCVQT PGY UKGAV PD[VGU] KH RQQN+PKVKCNKGF PD[VGU ] IE%QOOQP PD[VGU RQQN+PKVKCNKGF 2QQN5KG JGCR RQQN+PKVKCNKGF PD[VGU _ 6TKCPING
  • 161.
  • 162.
    JGCR YJKNG VR KP7UG ] VR 6TKCPING
  • 163.
  • 164.
    VR 4QWPF PD[VGU _ VR IEOCTM VR KP7UG TGVWTP XQKF
  • 165.
    VR _ Funkcja 6TKCPINGQRGTCVQT PGY zwraca wskaźnik niewykorzystywanego obiektu z puli obiektów klasy 6TKCPING. Podczas pierwszego wywołania operatora zmienna RQQN+PKVKCNKGF posiada wartość 0 nadaną jej podczas kompilacji. W związku z tym porównanie jej ze zmienną PD[VGU, która ma wartość UKGQH 6TKCPING, spowoduje wywołanie funkcji 5JCRG4GRIE%QOOQP. Funkcja ta u ywana jest do odzyskiwania nie- u ytków oraz w celu inicjacji pul obiektów podczas uruchamiania programu lub kon- wersji klasy. Funkcja ta między innymi nadaje wartości początkowe znacznikom obiektów znajdujących się w puli. Po wykonaniu funkcji IE%QOOQP operator PGY prze- gląda pule obiektów klasy 6TKCPING (wskazywaną przez składową 6TKCPINGJGCR), poszukując obiektu posiadającego skasowany znacznik KP7UG. Obsługę sytuacji, w której w puli nie ma ju takich obiektów, pozostawiamy do wykonania Czytelnikowi jako ćwiczenie. Tworzenie wszystkich obiektów klasy 5JCRG przebiega według tego samego wzorca. Inicjacja nowego obiektu klasy 5JCRG na podstawie istniejącego obiektu lub przypisanie jednego obiektu innemu obiektowi powodują wywołanie odpowiedniej wersji kon- struktora lub operatora przypisania: 5JCRG5JCRG 5JCRG Z ] 6JKPI VR VJKU CNN5JCRGU RWV VR TGR ZTGR _ 5JCRG 5JCRGQRGTCVQT 5JCRG Z ] TGR ZTGR TGVWTP
  • 166.
    VJKU _ W ten sposób wiele obiektów klasy 5JCRG mo e dysponować wskaźnikiem tego samego, wspólnego obiektu jednej z klas pochodnych klasy 5JCRG4GR. Zgodnie z idiomem wskaźników zliczanych (strona 73) obiekty klasy 5JCRG nie są tworzone na stercie. Dzięki temu nie musimy martwić się o zwalnianie zajmowanej przez nie pamięci, poniewa ka dy obiekt zadeklarowany jako zmienna automatyczna zwalnia swoją pamięć, gdy przestaje być dostępny. Podobnie ka dy obiekt klasy 5JCRG, który jest składową innego obiektu, zostaje automatycznie zwolniony podczas zwalnia- nia zawierającego go obiektu. Globalne obiekty klasy 5JCRG zostają usunięte podczas kończenia pracy programu, natomiast odzyskiwania nieu ytków wymagają obiekty klas pochodnych klasy 5JCRG4GR.
  • 167.
    Rozdział 9. ♦Emulacja języków symbolicznych w C++ 325 Odzyskiwanie nieu ytków mo e zostać uruchomione w dowolnym momencie. W przy- kładzie zamieszczonym w Dodatku E wywoływane jest „ręcznie” w kilku punktach programu. Bardziej przezroczyste, choć zarazem bardziej kosztowne, rozwiązanie polegałoby na wywoływaniu odzysku nieu ytków za ka dym razem, gdy tworzony jest nowy obiekt. Jeszcze inna strategia mo e wywoływać odzyskiwanie nieu ytków tylko w sytuacji, gdy operator PGY nie mo e znaleźć nieu ywanych obiektów w puli. Poszczególne fazy odzyskiwania nieu ytków mogą nawet zostać rozdzielone w czasie. Dotyczy to, na przykład, stosowania tego mechanizmu w celu przyrostowego odzy- skiwania pamięci w systemach czasu rzeczywistego, w których zbyt długie przerwy związane z odzyskiwaniem nieu ytków nie są wskazane (patrz ćwiczenia na końcu tego rozdziału). Odzyskiwanie nieu ytków zarządzane jest przez funkcję 5JCRGIE, która sama wykonuje fazę oznaczenia obiektów, a fazę usuwania przekazuje poszcze- gólnym obiektom listu: XQKF 5JCRGIE ] .KUVKVGT6QRR UJCRG+VGT
  • 168.
    CNN5JCRGU UJCRG+VGTTGUGV HQT 6QRR VR UJCRG+VGTPGZV VR ] 5JCRG
  • 169.
    VR TGR OCTM _ .KUVKVGT6JKPIR UJCRG'ZGORNCT+VGT
  • 170.
    CNN5JCRG'ZGORNCTU UJCRG'ZGORNCT+VGTTGUGV HQT 6JKPIR CP'ZGORNCT UJCRG'ZGORNCT+VGTPGZV CP'ZGORNCT ] 5JCRG4GR
  • 171.
  • 172.
    CP'ZGORNCT VJKU'ZGORNCT IE _ _ W pierwszej pętli funkcja IE przegląda wszystkie istniejące obiekty klasy 5JCRG i ustawia ich znaczniki za pomocą funkcji OCTM. W ten sposób zaznaczone zostają wszystkie obiekty klas pochodnych klasy 5JCRG4GR, dla których istnieją obiekty klasy 5JCRG posiadające ich referencje. Druga z pętli odwiedza wszystkie przykłady klas listu — dla ka dej klasy pochodnej klasy 5JCRG4GR istnieje jeden przykład — i pozwala im wykonać fazę usuwania algorytmu odzyskiwania nieu ytków. Poniewa ka dy przykład klasy pochodnej klasy 5JCRG4GR zarządza pulą obiektów, to zlokalizowanie i analiza istniejących instancji własnej klasy nie stanowi dla niego problemu. W przypadku klasy 5JCRG pula jest ciągłym obszarem pamięci właśnie po to, by przykład tej klasy mógł łatwiej zarządzać jej instancjami. Usuwanie obiektów wykonywane jest przez statyczną, wspólną operację klasy 5JCRG4GR o nazwie IE%QOOQP (listing 9.11). Funkcji tej przekazywana jest liczba bajtów przypa- dająca na obiekt danej klasy (PD[VGU), liczba bajtów zajmowanych przez ka dy obiekt podczas poprzedniego przebiegu algorytmu odzyskiwania nieu ytków (RQQN+PKVKCNKGF), liczba obiektów w puli (2QQN5KG) oraz wskaźnik puli (JGCR). Wartości tych parametrów mo emy uzyskać, wywołując operacje IE dla klasy pochodnej. Wywołanie to mo e być wykonane przez dowolną operacje klasy 5JCRG i nie wymaga adnych parametrów.
  • 173.
    326 C++. Styl i technika zaawansowanego programowania Listing 9.11. Faza usuwania obiektów określonego typu XQKF 5JCRG4GRIE%QOOQP UKGAV PD[VGU EQPUV UKGAV RQQN+PKVKCNKGF EQPUV KPV 2QQN5KG %JCTAR JGCR ] UKGAV U PD[VGU! PD[VGU RQQN+PKVKCNKGF UKGAV 5KGQH 4QWPF U 5JCRG4GR
  • 174.
  • 175.
    JGCR HQT KPV K K 2QQN5KG K ] UYKVEJ PD[VGU ] ECUG zwykły przypadek odzyskiwania nieu ytków KH VR KP7UG ] KH VR IEOCTM ^^ VR URCEG (TQO5RCEG ] nie usuwa VR URCEG 6Q5RCEG _ GNUG KH VR VR V[RG ] pamięć obiektu wymaga odzyskania VR 5JCRG4GR`5JCRG4GR VR KP7UG RTKPVH 5JCRG4GRIE%QOOQP RTKPVH 1F[UMCP[ QDKGMV MNCU[ 6TKCPING EP # EJCT
  • 176.
  • 177.
    JGCR5KGQH _ _ DTGCM FGHCWNV inicjacja VR KP7UG DTGCM _ VR IEOCTM VR 5JCRG4GR
  • 178.
    %JCTAR VR 5KGQH _ _ Jak wspomnieliśmy ju wcześniej, funkcja 5JCRG4GR wykonuje zarówno zadanie ini- cjacji puli pamięci, jak i odzyskiwania pamięci. Logika mechanizmu odzyskiwania nieu ytków umieszczona została w gałęzi instrukcji wyboru UYKVEJ opatrzonej etykietą 0. Zachowuje ona zaznaczone obiekty lub obiekty znajdujące się ju w docelowej prze- strzeni w rozumieniu algorytmu Bakera. Sprawdzenie przynale ności obiektu do okre- ślonej przestrzeni nie jest w tym wypadku zresztą istotne, ale staje się wa ne w momen- cie, gdy odzyskiwanie pamięci działa w sposób przyrostowy. Pamięć obiektów, które nie są zaznaczone i znajdują się w przestrzeni źródłowej, jest odzyskiwana — znacz- niki wykorzystania obiektów są kasowane, co przywraca je do puli u ywanej przez operator PGY. Zauwa my, e w ten sposób powiązaliśmy zwalnianie zasobów obiektów (przez wy- wołanie destruktora) z odzyskiwaniem nieu ytków. Zwalnianie zasobów obiektów jest działaniem, którego semantyka zale y od aplikacji. Natomiast odzyskiwanie nieu yt- ków koncentruje się wyłącznie na jednym zasobie niezwykle istotnym dla większości aplikacji — pamięci. Rozdzielenie obu tych operacji jest mo liwe (patrz podrozdział 3.7), ale oznacza obcią enie programistów koniecznością ręcznego usuwania niepotrzebnych obiektów, zanim zajmowana przez nie pamięć zostanie odzyskana jako nieu ytek. Na przykład, jeśli obiekt przechowuje deskryptor otwartego pliku w systemie UNIX, to
  • 179.
    Rozdział 9. ♦Emulacja języków symbolicznych w C++ 327 samo odzyskanie pamięci zajmowanej przez taki obiekt nie wyczerpuje zagadnienia odzyskania zajmowanych przez niego zasobów. Dlatego te w naszym przykładzie obie operacje zostały połączone. Z przedstawionym rozwiązaniem związane jest zało enie, e u ytkownik mo e pozwolić sobie na odroczenie odzyskania pamięci na dowolnie długi okres czasu. Aby zaktywi- zować proces odzyskiwania pamięci, u ytkownik powinien wywołać funkcję odzyski- wania nieu ytków natychmiast po usunięciu ostatniej referencji obiektu. 9.6. Hermetyzacja typów podstawowych W większości środowisk programowania symbolicznego instancje wszystkich typów są obiektami, w przeciwieństwie do języka C++, gdzie pewne typy posiadające repre- zentację zale ną od typu maszyny — na przykład KPV, EJCT, NQPI, UJQTV i tak dalej — są typami podstawowymi wbudowanymi w język, a nie pełnoprawnymi klasami. Czasami przydatna byłaby mo liwość posługiwania się wartościami tych typów jak obiektami — na przykład w celu stosowania dla nich mechanizmu automatycznego odzyskiwania nieu ytków. To samo dotyczy tak e konkretnych typów danych czyli klas, które posiadają elastyczność u ycia podobną do typów wbudowanych w język, ale nie posiadają właściwości typów symbolicznych. Rozwiązanie tego problemu polega na obudowaniu ka dego z typów podstawowych, którego zamierzamy u ywać w środowisku symbolicznym, za pomocą nowej klasy i tym samym utworzeniu biblioteki klas stanowiących odpowiedniki typów podsta- wowych. Chocia rozwiązanie to wymaga pewnego nakładu pracy, to pod względem koncepcyjnym jest stosunkowo łatwe. Przykładem mo e być kolekcja klas pochodnych klasy 0WODGT przedstawiona w podrozdziale 3.5. Wprowadzając w tym przykładzie niewielkie zmiany związane z zastosowaniem idiomu listu i koperty — polegające głównie na utworzeniu wspomnianych klas jako pochodnych klas 6QR i 6JKPI — uzy- skamy w pełni polimorficzne klasy numeryczne, dla których mo e stosowany być mechanizm odzyskiwania nieu ytków i których obiekty mogą być u ywane zamiast wartości typów podstawowych w przypadku bardziej tradycyjnych idiomów. Główny problem związany z włączeniem typów podstawowych do idiomu symbolicz- nego polega na znalezieniu dla nich odpowiedniego miejsca w hierarchii dziedziczenia. Przykład klasy 0WODGT pomyślany został tak, by posiadał funkcjonalność wszystkich typów numerycznych, ale za cenę nierozró niania wartości typu FQWDNG od wartości typu HNQCV, wartości typu EJCT od wartości typu KPV i tak dalej. Zastanówmy się na przykład, czy 5VTKPI powinien być samodzielnym typem symbolicznym, tak jak zało- yliśmy to w rozdziale 3., czy raczej powinien zostać, podobnie jak w języku Smalltalk, umieszczony w hierarchii pod klasami #TTC[GF%QNNGEVKQP, 5GSWGPEGCDNG%QNNGEVKQP i %QNNGEVKQP? W rozdziale 6. podkreśliliśmy, e tego rodzaju decyzje nale y podejmo- wać wyłącznie na podstawie gruntownej analizy dziedziny aplikacji.
  • 180.
    328 C++. Styl i technika zaawansowanego programowania 9.7. Wielometody i idiom symboliczny Zastanówmy się przez chwilę, w jaki sposób działa operacja dodawania dwóch obiektów numerycznych, zwłaszcza w przypadku, gdy są one ró nych typów. Tradycyjny spo- sób obsługi takiej sytuacji polega na zaaran owaniu przez system typów kompilatora odpowiedniej konwersji typu. Rozwiązanie takie staje się nieaktualne, gdy typy obiektów powstają w czasie działanie programu. Dlatego te w przypadku programowania sym- bolicznego niezbędne jest inne rozwiązanie. Załó my, e ka dy obiekt numeryczny dostępny jest dla programisty jako obiekt klasy 0WODGT, a klasy listu takie jak na przy- kład %QORNGZ czy $KI+PVGIGT są niewidoczne. Obiekty klasy 0WODGT zmieniają swoją charakterystykę podczas działania programu i wobec tego kompilator nie dysponuje wystarczającą informacją, aby wygenerować kod konwersji. Konwersja taka, związana równie z dodaniem odpowiednich operacji, musi zostać przeprowadzona podczas działania programu. W jaki sposób dokonamy wyboru algorytmu dodawania dwóch wartości numerycz- nych? Jedno z rozwiązań polega na przekazaniu wykonania tej operacji pierwszemu jej argumentowi. Inaczej mówiąc, operator zostanie wtedy wybrany na podstawie typu pierwszego argumentu. Sytuacja taka zachodzi zawsze w przypadku stosowania funkcji wirtualnych. Rozwiązanie takie obcią a jednak ka dy typ wiedzą o wszystkich typach, których wartości mogą brać udział w dodawaniu. W ten sposób typy numeryczne tracą na swojej autonomii, zwartości i niezale ności. Zdecydowanie lepszym rozwiązaniem byłby wybór algorytmu dodawania w oparciu o typ obu argumentów. Funkcje wirtualne języka C++ nie dysponują taką mo liwością, ale posiadają ją niektóre języki programowania symbolicznego. Funkcje lub operatory, których wybór odbywa się na podstawie typu wielu argumentów podczas działania pro- gramu nazywane są wielometodami w językach obiektowych bazujących na języku Lisp. Symulacja wielometod w języku C++ wymaga stosowania idiomów wykorzystują- cych pole informujące o typie obiektu omówionych w rozdziale 4. W ogólnym przy- padku stosowanie takich pól nie jest zalecane, poniewa funkcje wirtualne stanowią preferowany mechanizm umo liwiający wybór wariantów implementacji dostępnych w klasach pochodnych. Jednak w tym przypadku funkcje wirtualne nie są odpowiednim rozwiązaniem, co zostanie pokazane poni ej. W podrozdziale 5.6 (strona 169) przedstawiona została następująca definicja operatora klasy .2( reprezentującej filtry dolnoprzepustowe: 8CNWG
  • 181.
  • 182.
    H ] UYKVEJ H V[RG ] ECUG 6ACVC O[6[RG 6ACVC ECEJGF+PRWV H TGVWTP GXCNWCVG ECUG 6A.2( KH H H HC TGVWTP VJKU GNUG TGVWTP H
  • 183.
    Rozdział 9. ♦Emulacja języków symbolicznych w C++ 329 ECUG 6A*2( KH H H H TGVWTP PGY 0QVEJ H H H GNUG TGVWTP PGY $2( H H H ECUG 6A$2( KH (KNVGT
  • 184.
    H H H TGVWTP H GNUG TGVWTP PGY $2( H H H ECUG 6A0QVEJ ECEJGF+PRWV H TGVWTP VJKU _ _ Analizując implementację tego operatora, stwierdziliśmy wtedy, e mógłby on zostać zastąpiony kolekcją funkcji wirtualnych. Jednak liczba takich funkcji wzrastałaby szybko wraz z pojawianiem się nowych typów filtrów i w związku z tym uproszczenie kodu byłoby jedynie pozorne. Semantyka funkcji (KNVGTQRGTCVQT 8CNWG
  • 185.
    wymaga zwrócenia przezjej imple- mentację wartości lub obiektu reprezentującego jeden z wielu typów filtrów. Typ ten zale y od typu filtra, dla którego została wywołana ta funkcja, i od typu obiektu prze- kazanego jej jako parametr. Innymi słowy, o wyborze algorytmu i typie zwróconym jako wynik decyduje typ filtra wejściowego i typ filtra wyjściowego. W przykładzie przedstawionym w podrozdziale 5.6 logika tej operacji została rozproszona pomiędzy klasy pochodne klasy (KNVGT. Jak pokazuje to powy szy fragment, kodu klasy .2( dotyczy klas reprezentujących filtry wyjściowe. Gdyby w przykładzie tym zastosować mecha- nizm funkcji wirtualnych, to funkcja (KNVGTQRGTCVQT 8CNWG
  • 186.
    KPRWV wywoły- wałaby po prostu funkcję składową filtra wejściowego i w związku z tym logika operacji nale ałaby do filtrów wejściowych. adne z wymienionych rozwiązań nie jest w pełni satysfakcjonujące. Dlatego te za- stosujemy rozwiązanie polegające na u yciu hybrydowych operacji, które nie nale ą do adnej klasy. Rozwiązanie takie bazować będzie na funkcjach zaprzyjaźnionych i przecią aniu globalnym omówionym w podrozdziale 3.3. Stare rozwiązanie: Nowe rozwiązanie: ENCUU %QORNGZ ] %QORNGZ QRGTCVQT %QORNGZ E RWDNKE +OCIKPCT[ K ] _ %QORNGZ QRGTCVQT +OCIKPCT[ ENCUU %QORNGZ ] QRGTCVQT FQWDNG HTKGPF %QORNGZ QRGTCVQT %QORNGZ +OCIKPCT[ RWDNKE _ QRGTCVQT FQWDNG _ W przykładzie tym operator zostaje wybrany podczas kompilacji programu na pod- stawie zadeklarowanych typów argumentów. Nie istnieje bowiem bezpośredni sposób wyboru funkcji na podstawie rzeczywistych typów argumentów podczas wykonania programu. W tym celu musimy właśnie u yć wielometod.
  • 187.
    330 C++. Styl i technika zaawansowanego programowania Przyjrzymy się im najpierw na prostym przykładzie klasy 0WODGT dla operatora doda- wania. Implementując wielometodę, QRGTCVQT wykorzystamy przemienność operacji dodawania zmniejszając w ten sposób o połowę liczbę przypadków, które nale y roz- patrzyć. Do rozpoznawania typu argumentów u yjemy funkcji wirtualnej 6QRV[RG. Chocia jest to funkcja wirtualna, to jej zastosowanie odpowiada rozwiązaniu pole- gającemu na zastosowaniu pola informującego o typie obiektu przyjmującego wartości typu wyliczeniowego. Pierwszą czynnością będzie dodanie nowej funkcji składowej do klasy 0WODGT i jej klas pochodnych: KU# EQPUV 6QR
  • 188.
    EQPUV. Funkcja tabędzie zwracać wartość logiczną VTWG, jeśli wywołana zostanie dla obiektu klasy # z parametrem wskazującym obiekt klasy $ i zachodzi związek $ IS-A #. Zadaniem tej funkcji jest więc ustalenie, który z argumentów operatora dodawania jest bardziej ogólny i powinien przejąć kontrolę nad wykonaniem operacji. Operatory konwersji występujące we wcześniejszych przykładach zastąpimy pojedynczą funkcją składową RTQOQVG. Funkcja ta wykonywać będzie konwersje dowolnego obiektu typu 0WODGT do typu obiektu, dla którego została wywołana. Przy zało eniu, e funk- cja RTQOQVG będzie wywoływana dla wszystkich niezbędnych konwersji, nie jest ju konieczne przecią anie operacji dodawania dla ka dej klasy kopertowej. Ka da klasa posiadać będzie jedną wersję operacji dodawania. (Nazwa operacji została zmieniona z QRGTCVQT na CFF, aby uniknąć niejednoznaczności związanej z wprowadzeniem operatora przedstawionego poni ej). Poniewa ka da koperta mo e przyjąć, e jej operacja CFF wywoływana jest zawsze z parametrem tej samej klasy, to funkcja CFF nie musi ju korzystać z instrukcji wyboru UYKVEJ: ENCUU 0WODGT ] RWDNKE XKTVWCN 0WODGT
  • 189.
    V[RG EQPUV XKTVWCN KPV KU# EQPUV 0WODGT
  • 190.
    EQPUV EQPUV XKTVWCN 0WODGT RTQOQVG EQPUV 0WODGT EQPUV XKTVWCN 0WODGT CFF EQPUV 0WODGT EQPUV HTKGPF 0WODGT QRGTCVQT EQPUV 0WODGT EQPUV 0WODGT EQPUV _ ENCUU %QORNGZ RWDNKE 0WODGT ] RWDNKE KPV KU# EQPUV 0WODGT
  • 191.
    EQPUV EQPUV ] TGVWTP P V[RG EQORNGZ'ZGORNCT _ 0WODGT RTQOQVG EQPUV 0WODGT P EQPUV ] zawsze zwraca obiekt typu Complex KH PV[RG KOCIKPCT['ZGORNCT ] TGVWTP 0WODGT POCIPKVWFG _ GNUG KH PV[RG KPVGIGT'ZGORNCT ] TGVWTP 0WODGT POCIPKVWFG _ GNUG _ poni sza operacja wykonywana jest jedynie dla obiektów typu Complex
  • 192.
    Rozdział 9. ♦Emulacja języków symbolicznych w C++ 331 0WODGT CFF EQPUV 0WODGT EQPUV _ ENCUU +OCIKPCT[ RWDNKE %QORNGZ ] RWDNKE KPV KU# EQPUV 0WODGT
  • 193.
    EQPUV EQPUV ] TGVWTP P V[RG KOCIKPCT['ZGORNCT ^^ %QORNGZKU# P _ brak operacji promote — adne obiekty nie wymagają promowania do klasy Imaginary poni sza operacja wykonywana jest jedynie dla obiektów typu Imaginary 0WODGT CFF EQPUV 0WODGT EQPUV _ Efekt odpowiadający wielometodom uzyskujemy, wprowadzając globalnie przecią ony QRGTCVQT , który kompilator stosuje dla dowolnych argumentów klasy 0WODGT: 0WODGT QRGTCVQT EQPUV 0WODGT P EQPUV 0WODGT P VJTQY 0WODGT6[RG'TTQT ] KH PKU# P ] 0WODGT VGORQTCT[ PRTQOQVG P TGVWTP PCFF VGORQTCT[ _ GNUG KH PKU# P ] 0WODGT VGORQTCT[ PRTQOQVG P TGVWTP PCFF VGORQTCT[ _ GNUG ] VJTQY 0WODGT6[RG'TTQT _ _ Zauwa my, e rozwiązanie takie posiada szereg zalet w stosunku do podejścia przed- stawionego w podrozdziale 5.5 na stronie 140. Funkcje składowe są bardziej spójne — promocje do danego typu odbywają się lokalnie w danej klasie, a wszystkie ope- ratory binarne wykonywane są dla argumentów o homogenicznym typie (w związku z tym nie musimy przecią ać operatorów). Definicje funkcji składowych nie zawierają ju instrukcji wyboru UYKVEJ. Przedstawione rozwiązanie przybli a sposób działania języków symbolicznych. W nie- których implementacjach języka Smalltalk klasy numeryczne bezpośrednio obsługują operacje arytmetyczne, jeśli oba argumenty są tego samego typu. Jeśli typy nie są zgodne, to wysyłany jest komunikat do klasy, aby obsłu yła wszelkie niezbędne promocje i kon- wersje, a następnie wywołała metodę odpowiedniej klasy pochodnej. Rozwiązanie w języku Smalltalk odpowiada więc sytuacji, w której nasz globalnie przecią ony operator postaci: QRGTCVQT EQPUV 0WODGT EQPUV 0WODGT byłby zadeklarowany jako: 0WODGTQRGTCVQT EQPUV 0WODGT
  • 194.
    332 C++. Styl i technika zaawansowanego programowania Niektóre środowiska programowania w języku Lisp implementują wielometody jako kaskady wywołań funkcji wirtualnych dynamicznie wyszukujące ście kę do metody określonej przez typy wielu argumentów. Ćwiczenia 1. Zmodyfikuj funkcję 6QRWRFCVG (oraz inne fragmenty kodu, jeśli to konieczne) tak, by mo liwe było dodawanie nowych funkcji wirtualnych do klasy. Pamiętaj, e oryginalna tablica XVDN dla danej klasy jest osadzona statycznie w pamięci. Załó , e kompilator umieszcza elementy tablicy odpowiadające nowym funkcjom wirtualnym na końcu tablicy XVDN. Zastanów się, jakie ograniczenia posiada takie rozwiązanie w przypadku dziedziczenia po klasie listu oraz w jaki sposób mo na dodać funkcje wirtualne w klasach pochodnych. 2. Dodaj funkcję składową 6JKPIDCEMQWV 6JKPI
  • 195.
    , która umoliwi powrót struktury klasy i funkcji wirtualnych do wersji poprzedzającej ostatnią aktualizację. 3. Zmodyfikuj program zamieszczony w dodatku E tak, by odzyskiwanie nieu ytków wykonywane było za ka dym razem, gdy tworzony jest nowy obiekt. 4. Aby rozło yć działanie mechanizmu odzyskiwania nieu ytków w czasie, zmodyfikuj algorytm odzyskiwania nieu ytków przedstawiony w dodatku E tak, by ka de wywołanie funkcji 5JCRGIE powodowało jedynie odzyskiwanie nieu ytków pojedynczej klasy pochodnej klasy 5JCRG4GR. 5. Aby sposób działania mechanizmu odzyskiwania nieu ytków był jeszcze bardziej stopniowy, zaimplementuj metodę 5JCRGIE tak, by w trakcie wykonywania algorytmu oddawała sterowanie do wywołującego ją kodu, a kolejne jej wywołania podejmowały wykonanie algorytmu w tym miejscu, gdzie zakończyły je poprzednie. Wskazówka: faza zaznaczania obiektów powinna być niepodzielna, natomiast wykonanie fazy zamiatania mo e zostać podzielone pomiędzy szereg wywołań. Kolejne wywołanie podejmować będzie zamiatanie począwszy od punktu, w którym zakończyło je poprzednie wywołanie. Zamiana roli przestrzeni A i B powinna nastąpić dopiero po zakończeniu całej fazy zamiatania. Zastanów się, jaka kombinacja wartości pól KP7UG, IEOCTM i URCEG powinna powodować odzyskanie obiektu. 6. Powtórz poprzednie ćwiczenie, tym razem zakładając jednak niepodzielność fazy zamiatania. 7. Połącz dwa poprzednie ćwiczenia w jedno. 8. Napisz system przydziału pamięci, w którym klasy posiadające obiekty o tym samym rozmiarze współdzielą pule pamięci i zarządzane są przez ten sam mechanizm odzyskiwania nieu ytków. Przyporządkowanie klas do puli pamięci powinno odbywać się podczas działania programu.
  • 196.
    Rozdział 9. ♦Emulacja języków symbolicznych w C++ 333 9. Wymiatanie generacji [5] jest techniką odzyskiwania nieu ytków, która wykorzystuje osobne pule pamięci dla obiektów w ró nym wieku. Pule zawierające młodsze obiekty wymiatane są częściej, w oparciu o obserwację, e prawdopodobieństwo niewykorzystywania jest wy sze dla obiektów młodszych ni dla starszych. Zaimplementuj algorytm wymiatania generacji posługujący się trzema pulami generacji dla jednej klasy. Określ wiek obiektów mierzony za pomocą cykli zaznaczania i wymiatania. 10. Przepisz klasy 0WODGT przedstawione w podrozdziale 5.5, posługując się idiomem symbolicznym. 11. Zaimplementuj symboliczną klasę 5VTKPI, bazując na przykładzie tej klasy zamieszczonym w podrozdziale 3.5. Bibliografia [1] Ellis Margaret A., i Stroustrup B., The Annotated C++ Reference Manual. Reading Mass.: Addison-Wesley, 1990, podrozdział 10.5. [2] McCarthy J., „Recursive Functions of Symbolic Expressions and Their Computation by Machine”, Communications of the ACM 3 (1960), 184. [3] Baker H. G., „List Processing in Real Time on a Serial Computer”, A.I. Working Paper 139, MIT-AI Lab, Boston (kwiecień 1977). [4] Caplinger Michael, „A Memory Allocator with Garbage Collection for C”. USENIX Association Winter Conference, (luty 1988), 325 – 30 [5] Ungar David, Generation Scavenging: A Non-Disruptive High Performance Storage Reclamation Algorithm”, SIGPLAN Notices 19, 5 (maj 1984).