Tworzenie kopii obiektów. Wzorzec prototypu
Lipiec 24th, 2010 | Published in Bez kategorii
Kopiowanie obiektów, czyli tworzenie duplikatów, przechowujących te same informacje bez niszczenia oryginału, jest jedną z głównych operacji, które wykorzystujemy w programowaniu. Artykuł opisuje tę czynność, analizując techniki wspierające proces budowy kopii w języku C++.
Autor: Robert Nowak
Źródło: Software Developer’s Journal 03/2010 (183) sdjournal.org
Kopiowanie obiektów jest operacją wykonywaną zazwyczaj: przekazując argumenty przez wartość, zwracając wyniki obliczeń, przechowując detale w kontenerach i w wielu innych sytuacjach są tworzone kopie. Jeżeli obiekt jest dostępny pośrednio, na przykład przez wskaźnik, to można stworzyć kopię wskaźnika (albo innego uchwytu) albo całego obiektu. W związku z tym możemy wyróżnić trzy rodzaje kopiowania: kopiowanie płytkie, gdy kopiujemy uchwyty (wskaźniki), kopiowanie głębokie, gdy wytwarzamy kopię obiektu, , a oprócz tego kopiowanie leniwe, które łączy kopiowanie płytkie i głębokie. Do demonstracji tych technik będziemy używali klasy Foo pokazanej na Listingu 1.
Kopią płytką nazywa się kopiowanie jedynie obiektów pośredniczących, wskaźników, referencji, uchwytów i tym podobne. Kopia taka jest tworzona prędko, ponieważ wskaźnik albo inny obiekt pośredniczący jest w wielu wypadkach małym obiektem. Po zrobieniu płytkiej kopii ten sam obiekt jest dostępny z wielu miejsc, obiekt wskazywany nie jest kopiowany, zmiana jego stanu będzie widoczna we wszystkich kopiach. Głęboka kopia znaczy rzeczywiste kopiowanie obiektów wskazywanych. Powstawanie takiej kopii zajmuje więcej czasu i zasobów, ale obiekt i kopia są od siebie niezależne. Zmiany obiektu nie posiadają wpływu na kopię.
Kopiowanie opóźnione
Kopiowanie opóźnione lub leniwe używa obie strategie kopiowania opisane wcześniej. Na początku robimy kopię płytką, która jest przeprowadzana szybko i umożliwia poprawne odczytywanie informacji przechowywanych w zarządzanym obiekcie. Przy próbie konwersji obiektu badamy, czy obiekt jest wskazywany przez jeden, czy przez kilka wskaźników. Jeśli istnieje tylko jeden wskaźnik, to modyfikacja odbywa się na zarządzanym obiekcie, jednakże jeżeli już wskaźników jest więcej, projektuje się głęboką kopię wskazywanego obiektu i modyfikuje się tę kopię. Leniwe kopiowanie umożliwia zatem optymalne połączenie obu strategii, a ceną jest konieczność przechowywania dodatkowej składowej, która umożliwia rozstrzygnąć, czy powinno się robić głęboką kopię. Składową tą jest licznik odniesień lub flaga. Można pozbyć się tej składowej, tworząc głęboką kopię obiektu w każdej sytuacji, gdy wołana jest operacja modyfikująca, niemniej jednak wówczas wiele kopii jest zbędnych.
Przykład leniwego kopiowania został pokazany na Listingu 2. Przedstawiona tam klasa używa sprytne wskaźniki boost::shared_ptr, które zostały omówione w SDJ 11/2009. Sprytne wskaźniki to szablony stron internetowych, które pozwalają samoczynnie usuwać obiekt stworzony na stercie, przechowują one i updatują licznik odniesień do wskazywanego obiektu. Szablony stron www te wspierają tworzenie leniwej kopii, dostarczają metodę unique, która prezentuje, czy zarządzany obiekt jest wskazywany przez jeden, czy więcej wskaźników. Metoda ta jest wykorzystana w klasie LazyFoo do rozstrzygania, czy można przemieniać bieżący obiekt, czy raczej powinno się zrobić kopię.
Kreując kopię głęboką obiektu tymczasowego, który będzie usunięty po zakończeniu operacji kopiowania, można utworzyć kopię płytką i nie usuwać tego obiektu, co przypomina przeniesienie właściciela obiektu. Taki mechanizm dla wskaźników dostarcza std::auto_ptr (SDJ 11/2009), w ogólnym przypadku wymaga on wsparcia w języku. Takie wsparcie będzie dostarczone w nowym standardzie C++200x przez referencję do r-wartości, co pozwoli na implementację różnych konstruktorów kopiujących. Korzystając z konstruktora kopiującego do r-wartości, będzie można przenieść zawartość obiektu, unikniemy wówczas zbędnej kopii.
Całkiem szybkie kopiowanie głębokie
Dla pewnych typów obiektów kopia głęboka ma możliwość być przygotowana bez użycia konstruktora kopiującego za pomocą operacji kopiujących fragmenty pamięci. Obiekty, które będą w ten sposób kopiowane, nie mogą posiadać składowych, które są wskaźnikami, bo wskazywane poprzez te składowe obiekty również będą musiały być kopiowane przy projektowaniu kopii głębokiej. Treści o tym, czy obiekt może być kopiowany za radą kopiowania bajtów, dostarcza klasa cech (trejt) has_trivial_copy , który jest dostarczany przez bibliotekę boost::traits. Funkcja fastDeepCopy, pokazana na Listingu 3, stosuje dodatkowy argument, który jest tworzony w czasie kompilacji na bazie treści o typie. Jego wartość nie jest istotna, natomiast typ pozwala wybrać odpowiednią funkcję kopiującą. Jeżeli obiekty mogą być kopiowane za poradą procedur kopiującej fragmenty pamięci, to jest ona wołana, w przeciwnym wypadku woła się konstruktor kopiujący. Technologia trejtów została opisana w SDJ 11/2009.
Wzorzec prototypu
Jeżeli posługujemy się wskaźnikiem lub referencją do klasy bazowej, to możemy wyprodukować jedynie płytką kopię. Kopia głęboka jest niedostępna, ponieważ przy wytwarzaniu obiektu powinno się podać konkretny typ (patrz SDJ 2/2010), a my dysponujemy jedynie typem interfejsu. Rzeczywisty typ obiektu może być inny. Wzorzec prototypu, nazywany również wirtualnym konstruktorem, prezentowany w książce ,Wzorce projektowe” poprzez „bandę czworga” (Gamma, Helm, Johnson, Vlissides), pozwala na powstawanie głębokiej kopii w takich przypadkach. Pomysł polega na przeniesieniu odpowiedzialności za powstawanie obiektów do klas konkretnych, stosując mechanizm zadań wirtualnych. Klasa bazowa dostarcza metody czysto wirtualnej, która jest nadpisywana w klasach konkretnych (gdzie znany jest typ), zatem można wytworzyć głęboką kopię obiektu. Przykład pokazano na Listingu 4, klasa bazowa Figure dostarcza metody czysto wirtualnej clone. Metoda ta jest nadpisywana w klasach konkretnych, o ile ją będziemy wołali, to będzie tworzona głęboka kopia obiektu o odpowiednim typie.
Jeżeli jest dostępny wirtualny konstruktor, możemy wygenerować głęboką kopię, używając interfejs klasy bazowej, wołając metodę clone(). Listing 4 zawiera przykład, który wytwarza głęboką kopię kolekcji figur i stosuje przedstawioną technikę.
Fabryka prototypów
Wzorzec prototypu możemy wykorzystać w fabryce, która będzie dostarczała obiektów danego typu, nazywanej fabryką prototypów. Fabryki są to klasy pośredniczące w wytwarzaniu nowych obiektów, jeden z rodzajów fabryk został omówiony w SDJ 2/2010. Fabryka prototypów przechowuje obiekty wzorcowe, które będą kopiowane, o ile użytkownik zleci utworzenie nowego obiektu. Fabryka taka umożliwia wyprodukować obiektów różnorakich typów na podstawie identyfikatora, oprócz tego możemy nadać różnorakie identyfikatory obiektom tego samego stylu różniącym się stanem. Fabryki prototypów w wielu wypadkach zużywają więcej zasobów niż fabryki obiektów, konieczne jest przechowywanie obiektów wzorcowych, na podstawie których będą projektowane kopie. Przykład takiej fabryki pokazano na Listingu 5.
Fabryki prototypów pozwalają wygodnie wyprodukować obiekty z danej hierarchii klas, wymagają, aby w tej hierarchii był implementowany wzorzec prototypu. Dodatkowym kosztem tego typu fabryki jest wykorzystywanie mechanizmu późnego wiązania (funkcje wirtualne), więc obiekty muszą zawierać wskaźnik na tablicę zadań wirtualnych, wołanie metody clone() odbywa się pośrednio.
Podsumowanie
Przedstawione techniki powiązane z tworzeniem kopii są powszechnie stosowane w różnorakich językach kodowania programowego. Ich znajomość umożliwia na tworzenie płytkiej lub głębokiej kopii w zależności od wymagań.