2010.03_Tworzenie kopii obiektów_[Programowanie C C++].pdf

(362 KB) Pobierz
441718510 UNPDF
Programowanie C++
Tworzenie kopii obiektów
Wzorzec prototypu
Kopiowanie obiektów, czyli tworzenie duplikatów, przechowujących te
same informacje bez niszczenia oryginału, jest jedną z podstawowych
operacji, które wykorzystujemy w programowaniu. Artykuł opisuje
tę czynność, analizując techniki wspierające proces tworzenia kopii
w języku C++.
Dowiesz się:
• Co to jest leniwe kopiowanie;
• Co to jest wzorzec prototypu;
• Jak stworzyć fabrykę prototypów.
Powinieneś wiedzieć:
• Jak pisać proste programy w C++;
• Co to jest dziedziczenie i funkcje
wirtualne;
• Co to są szablony.
ne powyżej. Na początku wykonujemy ko-
pię płytką, która jest przeprowadzana szyb-
ko i umożliwia poprawne odczytywanie in-
formacji przechowywanych w zarządzanym
obiekcie. Przy próbie modyfikacji obiektu
badamy, czy obiekt jest wskazywany przez
jeden, czy przez kilka wskaźników. Jeże-
li istnieje tylko jeden wskaźnik, to mody-
fikacja odbywa się na zarządzanym obiek-
cie, natomiast jeżeli wskaźników jest wię-
cej, wykonuje się głęboką kopię wskazywa-
nego obiektu i modyfikuje się tę kopię. Leni-
we kopiowanie umożliwia więc optymalne
połączenie obu strategii, a ceną jest koniecz-
ność przechowywania dodatkowej składo-
wej, która pozwala rozstrzygnąć, czy nale-
ży robić głęboką kopię. Składową tą jest licz-
nik odniesień lub flaga. Można pozbyć się
tej składowej, tworząc głęboką kopię obiek-
tu za każdym razem, gdy wołana jest opera-
Poziom
trudności
kopiowanie obiektów wskazywanych. Two-
rzenie takiej kopii zajmuje więcej czasu i zaso-
bów, ale obiekt i kopia są od siebie niezależne.
Zmiany obiektu nie mają wpływu na kopię.
Na Rysunku 1 pokazano zawartość wskaźni-
ków po wykonaniu płytkiej i głębokiej kopii,
przykład kodu zawiera Listing 1.
K opiowanie obiektów jest operacją
wykonywaną bardzo często: prze-
kazując argumenty przez wartość,
zwracając wyniki obliczeń, przechowując
elementy 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 wykonać kopię wskaźni-
ka (lub innego uchwytu) albo całego obiek-
tu. W związku z tym możemy wyróżnić trzy
rodzaje kopiowania: kopiowanie płytkie, gdy
kopiujemy uchwyty (wskaźniki), kopiowa-
nie głębokie, gdy tworzymy kopię obiektu,
oraz kopiowanie leniwe, które łączy kopiowa-
nie płytkie i głębokie. Do demonstracji tych
technik będziemy używali klasy Foo pokaza-
nej na Listingu 1.
Kopią płytką nazywa się kopiowanie jedy-
nie obiektów pośredniczących, wskaźników,
referencji, uchwytów itp. Kopia taka jest
tworzona szybko, ponieważ wskaźnik lub in-
ny obiekt pośredniczący jest zazwyczaj ma-
łym obiektem. Po wykonaniu płytkiej kopii
ten sam obiekt jest dostępny z wielu miejsc,
obiekt wskazywany nie jest kopiowany, zmia-
na jego stanu będzie widoczna we wszystkich
kopiach. Głęboka kopia oznacza rzeczywiste
Kopiowanie opóźnione
Kopiowanie opóźnione lub leniwe wyko-
rzystuje obie strategie kopiowania opisa-
Listing 1. Tworzenie kopii płytkiej i głębokiej
class Foo { //klasa pomocnicza
public :
Foo () : i_ ( 0 ) {}
int get () const { return i_ ; }
void set ( int i ) { i_ = i ; }
} ;
Foo * shellCopy ( Foo * f ) { //płytka kopia
return f ; //wskaźniki pokazują na ten sam obiekt
}
Foo * deepCopy ( Foo * f ) { //głęboka kopia
return new Foo (* f ); //wskaźniki pokazują na różne obiekty
}
Foo * p1 = new Foo ();
Foo * p2 = shellCopy ( p1 ); //płytka kopia
Foo * p3 = deepCopy ( p1 ); //głęboka kopia
p1 -> set ( 2 ); //zmiana obiektu wskazywanego przez p1
assert ( p2 -> get () == 2 ); //obiekt wskazywany przez p2 został zmieniony
assert ( p3 -> get () == 1 ); // obiekt wskazywany przez p3 nie zosta ł zmieniony
12
3/2010
441718510.024.png 441718510.025.png 441718510.026.png 441718510.027.png 441718510.001.png 441718510.002.png 441718510.003.png 441718510.004.png 441718510.005.png 441718510.006.png 441718510.007.png
Wzorzec prototypu
cja modyfikująca, ale wtedy wiele kopii jest
zbędnych.
Przykład leniwego kopiowania został
pokazany na Listingu 2. Przedstawiona
tam klasa wykorzystuje sprytne wskaźni-
ki boost::shared_ptr , które zostały omó-
wione w SDJ 11/2009. Sprytne wskaźniki
to szablony, które pozwalają automatycznie
usuwać obiekt utworzony na stercie, prze-
chowują one i aktualizują licznik odnie-
sień do wskazywanego obiektu. Szablony
te wspierają tworzenie leniwej kopii, dostar-
czają metodę unique , która pokazuje, czy
zarządzany obiekt jest wskazywany przez
jeden, czy więcej wskaźników. Metoda ta
jest wykorzystana w klasie LazyFoo do roz-
strzygania, czy można modyfikować bieżący
obiekt, czy raczej należy zrobić kopię.
Tworząc kopię głęboką obiektu tymcza-
sowego, który będzie usunięty po zakoń-
czeniu operacji kopiowania, można wyko-
nać kopię płytką i nie usuwać tego obiek-
tu, 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 wspar-
cia w języku. Takie wsparcie będzie dostar-
czone w nowym standardzie C++200x po-
przez referencję do r-wartości, co pozwoli
na implementację różnych konstruktorów
kopiujących. Używając konstruktora ko-
piującego do r-wartości, będzie można prze-
nieść zawartość obiektu, unikniemy wtedy
zbędnej kopii.
cja fastDeepCopy , pokazana na Listingu 3,
wykorzystuje dodatkowy argument, który
jest tworzony w czasie kompilacji na podsta-
wie informacji o typie. Jego wartość nie jest
istotna, natomiast typ pozwala wybrać od-
powiednią funkcję kopiującą. Jeżeli obiekty
mogą być kopiowane za pomocą funkcji ko-
piującej fragmenty pamięci, to jest ona wo-
łana, w przeciwnym wypadku woła się kon-
struktor kopiujący. Technika trejtów została
opisana w SDJ 11/2009.
SDJ 2/2010), a my dysponujemy tylko ty-
pem interfejsu. Rzeczywisty typ obiektu
może być inny.
Wzorzec prototypu, nazywany też wir-
tualnym konstruktorem, opisany w książce
,,Wzorce projektowe'' przez „bandę czworga''
(Gamma, Helm, Johnson, Vlissides), pozwa-
la na tworzenie głębokiej kopii w takich przy-
padkach. Pomysł polega na przeniesieniu od-
powiedzialności za tworzenie obiektów do
klas konkretnych, wykorzystując mechanizm
funkcji wirtualnych. Klasa bazowa dostarcza
metody czysto wirtualnej, która jest nadpi-
sywana w klasach konkretnych (gdzie znany
jest typ), więc można utworzyć głęboką kopię
obiektu. Przykład pokazano na Listingu 4,
klasa bazowa Figure dostarcza metody czy-
sto wirtualnej clone . Metoda ta jest nadpisy-
Wzorzec prototypu
Jeżeli posługujemy się wskaźnikiem lub re-
ferencją do klasy bazowej, to możemy wy-
konać jedynie płytką kopię. Kopia głęboka
jest niedostępna, ponieważ przy tworzeniu
obiektu należy podać konkretny typ (patrz
Listing 2. Leniwa kopia z użyciem boost::shared_ptr
class LazyFoo { //przechowuje leniwą kopię obiektu typu Foo (Listing 1)
public :
LazyFoo ( int i ) : ptr_ ( new Foo ( i ) ) {}
LazyFoo ( const LazyFoo & l ) : ptr_ ( l . ptr_ ) {}
int get () const { return ptr_ -> get (); }
void set ( int i ) { //metoda zmienia stan obiektu
if ( ptr_ . unique () ) { ptr_ -> set ( i ); } //bada czy istnieje konieczność
tworzenia kopii
else { ptr_ = PFoo ( new Foo ( i ) ); }
}
private :
typedef shared_ptr < Foo > PFoo ;
PFoo ptr_ ;
} ;
Szybkie kopiowanie głębokie
Dla pewnych typów obiektów kopia głęboka
może być wykonana bez użycia konstrukto-
ra kopiującego za pomocą operacji kopiują-
cych fragmenty pamięci. Obiekty, które bę-
dą w ten sposób kopiowane, nie mogą mieć
składowych, które są wskaźnikami, bo wska-
zywane przez te składowe obiekty także bę-
dą musiały być kopiowane przy tworze-
niu kopii głębokiej. Informacji o tym, czy
obiekt może być kopiowany za pomocą ko-
piowania bajtów, dostarcza klasa cech ( trejt )
has_trivial_copy , który jest dostarcza-
ny przez bibliotekę boost::traits . Funk-
Listing 3. Wykorzystanie klasy cech do wyboru algorytmu kopiowania
template < typename T > //kopiowanie za pomocą memcpy
T * doFastDeepCopy ( const T * element , true_type ) {
char * mem = new char [ sizeof ( T )]; //przydziela pamięć
memcpy ( mem , element , sizeof ( T ));
return reinterpret_cast < T *>( mem ) ;//zwraca obiekt odpowiedniego typu
}
template < typename T > //woła konstruktor kopiujący
T * doFastDeepCopy ( const T * element , false_type ) {
return new T (* element );
}
template < class T > //algorytm tworzenia kopii wykorzystuje trejty
T * fastDeepCopy ( const T * element ) {
return doFastDeepCopy ( element , has_trivial_copy < T >() ); //tworzy dodatkowy
argument
Szybki start
Aby uruchomić przedstawione przykła-
dy, należy mieć dostęp do kompilatora
C++ oraz edytora tekstu. Niektóre przy-
kłady korzystają z udogodnień dostar-
czanych przez biblioteki boost, warun-
kiem ich uruchomienia jest instalacja bi-
bliotek boost (w wersji 1.36 lub nowszej).
Na wydrukach pominięto dołączanie od-
powiednich nagłówków oraz udostęp-
nianie przestrzeni nazw, pełne źródła do-
łączono jako materiały pomocnicze.
}
Rysunek 1. Kopia płytka i głęboka dla obiektów dostępnych pośrednio
www.sdjournal.org
13
441718510.008.png 441718510.009.png 441718510.010.png 441718510.011.png 441718510.012.png 441718510.013.png 441718510.014.png 441718510.015.png 441718510.016.png 441718510.017.png 441718510.018.png 441718510.019.png
Programowanie C++
Listing 4. Wzorzec prototypu
wana w klasach konkretnych, jeżeli ją będzie-
my wołali, to będzie tworzona głęboka kopia
obiektu o odpowiednim typie.
Jeżeli jest dostępny wirtualny konstruktor,
możemy tworzyć głęboką kopię, wykorzy-
stując interfejs klasy bazowej, wołając meto-
clone() . Listing 4 zawiera przykład, któ-
ry tworzy głęboką kopię kolekcji figur i wyko-
rzystuje przedstawioną technikę.
class Figure {//klasa bazowa
public :
virtual Figure * clone () const = 0 ;//wirtualny konstruktor
virtual ~ Figure () {}
} ;
class Square : public Figure {//klasa konkretna
public :
Square ( int size ) : size_ ( size ) {}
Square ( const Square & sq ) : size_ ( sq . size_ ) {}
Figure * clone () const { return new Square (* this ); } //tworzy głęboką kopię
private :
int size_ ;
} ;
class Circle : public Figure {//klasa konkretna
public :
Circle ( int r ) : r_ ( r ) {}
Circle ( const Circle & c ) : r_ ( c . r_ ) {}
Figure * clone () const { return new Circle (* this ); }//tworzy głęboką kopię
private :
int r_ ;
} ;
Fabryka prototypów
Wzorzec prototypu możemy wykorzystać
w fabryce, która będzie dostarczała obiek-
tów danego typu, nazywanej fabryką pro-
totypów. Fabryki są to klasy pośredniczą-
ce w tworzeniu nowych obiektów, jeden
z rodzajów fabryk został omówiony w SDJ
2/2010. Fabryka prototypów przechowu-
je obiekty wzorcowe, które będą kopiowa-
ne, jeżeli użytkownik zleci utworzenie no-
wego obiektu. Fabryka taka pozwala two-
rzyć obiektów różnych typów na podstawie
identyfikatora, ponadto możemy nadać róż-
ne identyfikatory obiektom tego samego ty-
pu różniącym się stanem. Fabryki prototy-
pów zazwyczaj zużywają więcej zasobów niż
fabryki obiektów, konieczne jest przechowy-
wanie obiektów wzorcowych, na podstawie
których będą tworzone kopie. Przykład ta-
kiej fabryki pokazano na Listingu 5.
Fabryki prototypów pozwalają wygodnie
tworzyć obiekty z danej hierarchii klas, wy-
magają, aby w tej hierarchii był implemento-
wany wzorzec prototypu. Dodatkowym kosz-
tem tego rodzaju fabryki jest używanie me-
chanizmu późnego wiązania (funkcje wirtu-
alne), więc obiekty muszą zawierać wskaźnik
na tablicę funkcji wirtualnych, wołanie meto-
dy clone() odbywa się pośrednio.
//przykład użycia wirtualnego konstruktora do tworzenia głębokiej kopii obiektów
typedef vector < Figure *> Figures ;
Figures igures ;//kolekcja igur, która będzie kopiowana
igures . push_back ( new Square ( 2 ) );
igures . push_back ( new Circle ( 3 ) );
igures . push_back ( new Circle ( 1 ) );
Figures copy ; //głęboka kopia kolekcji igur
for ( Figures :: const_iterator ii = igures . begin (); ii != igures . end (); ++ ii )
copy . push_back ( (* ii )-> clone () ); // wykorzystuje wzorzec prototypu
Listing 5. Fabryka prototypów
class FigCloneFactory { //fabryka prototypów dla hierarchii igur
public :
int registerFig ( Figure * prototype , int id ) { //rejestruje nowy obiekt oraz jego
identyikator
prototypes_ . insert ( make_pair ( id , prototype ) );
}
Figure * create ( int id ) {//tworzy obiekt danego typu i w danym stanie
map < int , Figure *>:: const_iterator i = prototypes_ . ind ( id );
if ( i ! = prototypes_ . end () ) //jeżeli znalazł odpowiedni wpis
return prototypes_ . ind ( id )-> second -> clone (); //wzorzec prototypu
return 0L ; //zwraca nullptr jeżeli nie znalazł prototypu
}
private :
Podsumowanie
Przedstawione techniki związane z tworze-
niem kopii są powszechnie stosowane w róż-
nych językach programowania. Ich znajo-
mość pozwala na tworzenie płytkiej lub głę-
bokiej kopii w zależności od potrzeb.
W Sieci
map < int , Figure *> prototypes_ ;//przechowuje obiekty wzorcowe
} ;
http://www.boost.org – dokumentacja
bibliotek boost;
http://www.open-std.org – dokumenty
opisujące nowy standard C++.
Więcej w książce
Zagadnienia dotyczące współcześnie stosowanych technik w języku C++, wzorce projekto-
we, programowanie generyczne, prawidłowe zarządzanie zasobami przy stosowaniu wy-
jątków, programowanie wielowątkowe, ilustrowane przykładami stosowanymi w bibliotece
standardowej i bibliotekach boost, zostały opisane w książce ,,Średnio zaawansowane pro-
gramowanie w C++'', która ukaże się niebawem.
ROBERT NOWAK
Adiunkt w Zakładzie Sztucznej Inteligencji Insty-
tutu Systemów Elektronicznych Politechniki War-
szawskiej, zainteresowany tworzeniem aplikacji
bioinformatycznych oraz zarządzania ryzykiem.
Programuje w C++ od ponad 10 lat.
Kontakt z autorem:rno@o2.pl
14
3/2010
441718510.020.png 441718510.021.png 441718510.022.png 441718510.023.png
 
Zgłoś jeśli naruszono regulamin