Zaawansowane CPP/Wykład 7: Klasy wytycznych: Różnice pomiędzy wersjami
Matiunreal (dyskusja | edycje) Nie podano opisu zmian |
|||
(Nie pokazano 50 wersji utworzonych przez 5 użytkowników) | |||
Linia 2: | Linia 2: | ||
Klasy wytycznych, nazywane również klasami reguł (policy classes) | Klasy wytycznych, nazywane również klasami reguł (policy classes) | ||
służą do parametryzowania zachowania innych klas. Rozważmy przykład | |||
funkcji | funkcji <tt>accumulate</tt>. Posiada ona również przeciążoną wersję | ||
umożliwiającą postawienie dowolnej operacji zamiast dodawania: | umożliwiającą postawienie dowolnej operacji zamiast dodawania: | ||
template <class InputIterator, class T, class BinaryFunction> | template <class InputIterator, class T, class BinaryFunction> | ||
T accumulate(InputIterator first, InputIterator last, T init, | T accumulate(InputIterator first, InputIterator last, T init, | ||
BinaryFunction binary_op); | BinaryFunction binary_op); | ||
Jedyna zmiana w implementacji klasy w stosunku do [http://osilek.mimuw.edu.pl/index.php?title=Zaawansowane_CPP/Wyk%C5%82ad_5:_Klasy_cech#prz.5.2 przykładu 5.2] to zmiania operacji sumowania na: | |||
[ | |||
init <nowiki>=</nowiki> binary_op(init, *first); | init <nowiki>=</nowiki> binary_op(init, *first); | ||
Pomimo że pojawił się dodatkowy szablon klasy, to nie jest to typowa | Pomimo, że pojawił się dodatkowy szablon klasy, to nie jest to typowa | ||
klasa wytycznych. Zachowanie jest określone nie tyle przez ten | klasa wytycznych. Zachowanie jest określone nie tyle przez ten | ||
parametr co przez funktor przekazany jako argument wywołania. | parametr, co przez funktor przekazany jako argument wywołania. | ||
Możemy jednak zmienić trochę implementację: | Możemy jednak zmienić trochę implementację: | ||
template <class Operation, class InputIterator, class T > | template <class Operation, class InputIterator, class T > | ||
T accumulate(InputIterator first, InputIterator last, T init) { | T accumulate(InputIterator first, InputIterator last, T init) { | ||
for (; first !<nowiki>=</nowiki> last; ++first) | for (; first !<nowiki>=</nowiki> last; ++first) | ||
init <nowiki>=</nowiki> Operation::op(init,*first) | init <nowiki>=</nowiki> Operation::op(init,*first) | ||
return init; | return init; | ||
} | } | ||
Takiego szablonu możemy używać następująco: | Takiego szablonu możemy używać następująco: | ||
template<typename T> Sumation { | template<typename T> Sumation { | ||
static op(const T &a,const T &b) { | static op(const T &a,const T &b) { | ||
return a+b; | return a+b; | ||
} | } | ||
}; | };<br> | ||
accumulate<Summation<double> >accumulate(first,last,0.0); | |||
Klasa (szablon) <tt>Summation</tt> jest właśnie klasą wytycznych. W | |||
zasadzie nie ma powodów aby implementować funkcję <tt>accumulate</tt> za | |||
Klasa (szablon) | |||
zasadzie nie ma powodów aby implementować funkcję | |||
pomocą klas wytycznych, poza być może nadzieją na trochę bardziej | pomocą klas wytycznych, poza być może nadzieją na trochę bardziej | ||
efektywny kod. | efektywny kod. | ||
W następnej | W następnej części przedstawię bardziej realistyczny, ale i bardziej | ||
skomplikowany przykład. | skomplikowany przykład. | ||
==Projektowanie za pomocą klas wytycznych | ==Projektowanie za pomocą klas wytycznych== | ||
Problemem w uniwersalnych bibliotekach jest duża ilość możliwych | Problemem w uniwersalnych bibliotekach jest duża ilość możliwych | ||
Linia 54: | Linia 52: | ||
alokacji pamięci i różne strategie obsługi błędów. Te zagadanienia są | alokacji pamięci i różne strategie obsługi błędów. Te zagadanienia są | ||
w dużej mierze ortogonalne do siebie. Jeśli więc mamy trzy strategie | w dużej mierze ortogonalne do siebie. Jeśli więc mamy trzy strategie | ||
przydziału pamięci | przydziału pamięci i trzy strategie obsługi błędu, to w sumie | ||
dostajemy dziewięć możliwych kombinacji. Decyzja o możliwości pracy w | dostajemy dziewięć możliwych kombinacji. Decyzja o możliwości pracy w | ||
środowisku wielowątkowym zwiększą | środowisku wielowątkowym zwiększą tę liczę dwukrotnie. | ||
Klasy wytycznych mogą pomóc opanować ten kombinatoryczny wzrost ilości | Klasy wytycznych mogą pomóc opanować ten kombinatoryczny wzrost ilości | ||
możliwości. Idea polega na tym aby za każdą decyzję odpwiedzialną | możliwości. Idea polega na tym, aby za każdą decyzję odpwiedzialną | ||
zrobić jedną klasę wytyczną, przekazywaną jako parametr szablonu. W | zrobić jedną klasę wytyczną, przekazywaną jako parametr szablonu. W | ||
przytoczonym przykładzie szablon kontenera mógłby posiadać dwa | przytoczonym przykładzie szablon kontenera mógłby posiadać dwa | ||
parametry wytycznych | parametry wytycznych | ||
template<typename T, | template<typename T, | ||
typename Allocator_policy, | typename Allocator_policy, | ||
typename Checking_policy> | typename Checking_policy> | ||
Kontener; | Kontener; | ||
i potrzebowalibyśmy 6 (3 | i potrzebowalibyśmy 6 (3 <tt>Allocator_policy</tt> i 3 <tt>Checking_policy</tt>) różnych | ||
klas wytycznych. Sześć | klas wytycznych. Sześć może się wydawać niewiele mniejsze od | ||
dziewięciu, ale takie podejście skaluje się liniowo z liczbą | dziewięciu, ale takie podejście skaluje się liniowo z liczbą | ||
wytycznych: dodanie nowej | wytycznych: dodanie nowej strategii alokacji pamięci wymaga jednej | ||
dodatkowej klasy a liczba kombinacji zwieksza się do 12. | dodatkowej klasy, a liczba kombinacji zwieksza się do 12. | ||
W praktyce wszystkie wytyczne miałyby wartości domyślne. | W praktyce wszystkie wytyczne miałyby wartości domyślne. | ||
==Stack== | ==Stack== | ||
Pokażę teraz jak to działa w praktyce na przykładzie znanego już nam | |||
szablonu klasy | szablonu klasy <tt>Stack</tt>, w którym na początek dokonam drobnych zmian: | ||
template<typename T <nowiki>=</nowiki> int , size_t N <nowiki>=</nowiki> 100> class Stack { | template<typename T <nowiki>=</nowiki> int , size_t N <nowiki>=</nowiki> 100> class Stack { | ||
private: | private: | ||
T rep[N]; | T rep[N]; | ||
size_t _top; | size_t _top; | ||
public: | public: | ||
Stack():_top(0) {} | Stack():_top(0) {} | ||
void push(const T &val) {_rep[_top++]<nowiki>=</nowiki>val;} | void push(const T &val) {_rep[_top++]<nowiki>=</nowiki>val;} | ||
void pop() {--_top;} | void pop() {--_top;} | ||
const T& top() const {return _rep[top-1];} | const T& top() const {return _rep[top-1];} | ||
bool is_empty {return !_top;} | bool is_empty {return !_top;} | ||
} | } | ||
Zmiany polegają na rozdzieniu operacji odczytywania wierzchołka stosu | Zmiany polegają na rozdzieniu operacji odczytywania wierzchołka stosu | ||
i zdejmowania elementu | i zdejmowania elementu ze stosu. Umożliwia to między innymi | ||
przekazywanie wartości zwracanej ze stosu przez referencje, poza tym | przekazywanie wartości zwracanej ze stosu przez referencje, poza tym | ||
jest to bardziej bezpieczne. | jest to bardziej bezpieczne. | ||
Ten kod jest delikatnie rzecz ujmując bardzo prościutki. Możemy | Ten kod jest, delikatnie rzecz ujmując, bardzo prościutki. Możemy | ||
rozbudowywać go w co najmniej dwu kierunkach. Po pierwsze można użyć | rozbudowywać go w co najmniej dwu kierunkach. Po pierwsze można użyć | ||
dynamicznej alokacji pamięci, po drugie możemy zaimplementować | dynamicznej alokacji pamięci, po drugie możemy zaimplementować | ||
Linia 106: | Linia 104: | ||
Żeby zaimplementować sprawdzanie zakresu dodajemy nowy parametr do | Żeby zaimplementować sprawdzanie zakresu dodajemy nowy parametr do | ||
szablonu | szablonu <tt>Stack</tt>, który będzie określał klasę wytyczną dla tej strategii: | ||
}; | template<typename T <nowiki>=</nowiki> int , size_t N <nowiki>=</nowiki> 100, | ||
typename Checking_policy <nowiki>=</nowiki> No_checking_policy > | |||
class Stack { | |||
private: | |||
T _rep[N]; | |||
size_t _top; | |||
public: | |||
Stack():_top(0) {};<br> | |||
void push(const T &val) {<br> | |||
Checking_policy::check_push(_top,N); | |||
_rep[_top++]<nowiki>=</nowiki>val; | |||
}<br> | |||
void pop() { | |||
Checking_policy::check_pop(_top); | |||
--_top; | |||
}<br> | |||
const T& top() const { | |||
Checking_policy::check_top(_top); | |||
return _rep[top-1]; | |||
}<br> | |||
bool is_empty() { | |||
return !_top; | |||
}<br> | |||
}; | |||
([[media:Stack1.h | Źródło: stack1.h]]) | |||
Klasa | Klasa | ||
struct No_checking_policy { | struct No_checking_policy { | ||
static void check_push(size_t,size_t) {}; | static void check_push(size_t,size_t) {}; | ||
static void check_pop(size_t) {}; | static void check_pop(size_t) {}; | ||
static void check_top(size_t) {}; | static void check_top(size_t) {}; | ||
}; | }; | ||
([[media:Checking_policy.h | Źródło: checking_policy.h]]) | |||
implementuje najprostszą strategię sprawdzania zakresu: brak | implementuje najprostszą strategię sprawdzania zakresu: brak | ||
sprawdzania. | sprawdzania. Proszę zauważyć, że w tym wypadku najprawdopodobniej | ||
żaden kod nie zostanie dodany: kompilator "wyoptymalizuje" puste | żaden kod nie zostanie dodany: kompilator "wyoptymalizuje" puste | ||
funckje. | funckje. | ||
Linia 154: | Linia 148: | ||
Inne możliwe strategie to np. | Inne możliwe strategie to np. | ||
class Abort_on_error_policy { | class Abort_on_error_policy { | ||
public: | public: | ||
static void check_push(size_t top,size_t size) { | static void check_push(size_t top,size_t size) {<br> | ||
if(top ><nowiki>=</nowiki> size) { | |||
if(top ><nowiki>=</nowiki> size) { | std::cerr<<"trying to push elemnt on full stack: aborting"<<std::endl; | ||
std::cerr<<"trying to push elemnt on full stack: aborting"<<std::endl; | abort(); | ||
abort(); | } | ||
} | };<br> | ||
}; | <i>i podobnie dla pozostałych funkcji sprawdzających</i> | ||
}; | |||
([[media:Checking_policy.h | Źródło: checking_policy.h]]) | |||
}; | |||
Programując w C++ wstyd by było nie użyć wyjątków: | Programując w C++ wstyd by było nie użyć wyjątków: | ||
struct Std_exception_on_error_policy { | struct Std_exception_on_error_policy {<br> | ||
static void check_push(size_t top,size_t size) {<br> | |||
static void check_push(size_t top,size_t size) { | if(top ><nowiki>=</nowiki> size) { | ||
throw std::range_error("over the top"); | |||
} | |||
};<br> | |||
<i>i podobnie dla pozostałych funkcji sprawdzających</i><br> | |||
}; | |||
([[media:Checking_policy.h | Źródło: checking_policy.h]]) | |||
Teraz możemy prosto konfigurować szablon <tt>Stack</tt> podając mu | |||
Teraz możemy prosto konfigurować szablon | |||
odpowiednie argumenty: | odpowiednie argumenty: | ||
Stack<int,10> s_no_check; | Stack<int,10> s_no_check; | ||
Stack<double ,100,Abort_on_error_policy> s_abort; | Stack<double ,100,Abort_on_error_policy> s_abort; | ||
Stack<int *,25,Std_exception_on_error_policy> s_except; | Stack<int *,25,Std_exception_on_error_policy> s_except; | ||
([[media:Policy1.cpp | Źródło: policy1.cpp]]) | |||
W celu zaimplementowania różnych strategii | W celu zaimplementowania różnych strategii przydziału pamięci dodajemy | ||
dodatkowy parametr szablonu który sam będzie szablonem: | dodatkowy parametr szablonu, który sam będzie szablonem: | ||
template<typename T <nowiki>=</nowiki> int , size_t N <nowiki>=</nowiki> 100, | template<typename T <nowiki>=</nowiki> int , size_t N <nowiki>=</nowiki> 100, | ||
typename Checking_policy <nowiki>=</nowiki> No_checking_policy, | |||
template<typename U,size_t M> class Allocator_policy | template<typename U,size_t M> class Allocator_policy | ||
<nowiki>=</nowiki> Static_table_allocator > class Stack; | |||
Szablon | Szablon typu <tt>Allocator_policy</tt> posiada jeden typ stowarzyszony i | ||
szereg funkcji: | szereg funkcji: | ||
template<typename T,size_t N <nowiki>=</nowiki> 0> struct Static_table_allocator { | template<typename T,size_t N <nowiki>=</nowiki> 0> struct Static_table_allocator { | ||
typedef T rep_type[N]; | typedef T rep_type[N]; | ||
void init(rep_type &,size_t) {}; | void init(rep_type &,size_t) {}; | ||
void expand_if_needed(rep_type &,size_t) {}; | void expand_if_needed(rep_type &,size_t) {}; | ||
void shrink_if_needed(rep_type &size_t) {}; | void shrink_if_needed(rep_type &size_t) {}; | ||
void dealocate(rep_type &){}; | void dealocate(rep_type &){};<br> | ||
static size_t size() {return N;};<br> | |||
}; | |||
([[media:Allocator2.h | Źródło: allocator2.h]]) | |||
Szablon <tt>Stack</tt> implementujemy teraz następująco: | |||
template<...> class Stack {<br> | |||
typedef typename Allocator_policy<T,N>::rep_type rep_type; | |||
rep_type _rep; | |||
size_t _top; | |||
template<...> class Stack { | Allocator_policy<T,N> alloc; | ||
public: | |||
typedef typename Allocator_policy<T,N>::rep_type rep_type; | Stack(size_t n <nowiki>=</nowiki> N):_top(0) { | ||
rep_type _rep; | alloc.init(_rep,n); | ||
size_t _top; | };<br> | ||
Allocator_policy<T,N> alloc; | void push(const T &val) { | ||
public: | alloc.expand_if_needed(_rep,_top); | ||
Stack(size_t n <nowiki>=</nowiki> N):_top(0) { | Checking_policy::check_push(_top,alloc.size()); | ||
alloc.init(_rep,n); | _rep[_top++]<nowiki>=</nowiki>val; | ||
}; | }<br> | ||
void pop() { | |||
void push(const T &val) { | Checking_policy::check_pop(_top); | ||
alloc.expand_if_needed(_rep,_top); | --_top; | ||
Checking_policy::check_push(_top,alloc.size()); | alloc.shrink_if_needed(_rep,_top); | ||
_rep[_top++]<nowiki>=</nowiki>val; | }<br> | ||
} | const T& top() const { | ||
Checking_policy::check_top(_top); | |||
void pop() { | return _rep[top-1]; | ||
Checking_policy::check_pop(_top); | }<br> | ||
--_top; | bool is_empty() { | ||
alloc.shrink_if_needed(_rep,_top); | return !_top; | ||
} | }<br> | ||
~Stack() {alloc.dealocate(_rep);}<br> | |||
const T& top() const { | }; | ||
Checking_policy::check_top(_top); | ([[media:Stack2.h | Źródło: stack2.h]]) | ||
return _rep[top-1]; | |||
} | |||
bool is_empty() { | |||
return !_top; | |||
} | |||
}; | |||
Szablon | Szablon | ||
template<typename T,size_t N > struct Expandable_new_allocator { | template<typename T,size_t N > struct Expandable_new_allocator { | ||
typedef T * rep_type; | typedef T * rep_type; | ||
size_t _size; | size_t _size; | ||
void init(rep_type &rep,size_t n) {_size<nowiki>=</nowiki>n;rep <nowiki>=</nowiki> new T[_size];}; | void init(rep_type &rep,size_t n) {_size<nowiki>=</nowiki>n;rep <nowiki>=</nowiki> new T [_size];}; | ||
void expand_if_needed(rep_type & rep,size_t top) { | void expand_if_needed(rep_type & rep,size_t top) { | ||
if(top <nowiki>=</nowiki><nowiki>=</nowiki> _size) { | if(top <nowiki>=</nowiki><nowiki>=</nowiki> _size) { | ||
_size<nowiki>=</nowiki>2*_size; | _size<nowiki>=</nowiki>2*_size; | ||
T *tmp<nowiki>=</nowiki> new T[_size]; | T *tmp<nowiki>=</nowiki> new T[_size]; | ||
std::copy(rep,&rep[top],tmp); | std::copy(rep,&rep[top],tmp); | ||
delete [] rep; | delete [] rep; | ||
rep <nowiki>=</nowiki> tmp; | rep <nowiki>=</nowiki> tmp; | ||
} | } | ||
}; | }; | ||
void shrink_if_needed(rep_type &size_t) { | void shrink_if_needed(rep_type &size_t) { | ||
}; | }; | ||
void dealocate(rep_type &rep){delete [] rep;}; | void dealocate(rep_type &rep){delete [] rep;};<br> | ||
size_t size() const {return _size;}; | |||
}; | |||
([[media:Allocator2.h | Źródło: allocator2.h]]) | |||
definiuje strategię dynamicznego przydziału pamięci "na żądanie". | definiuje strategię dynamicznego przydziału pamięci "na żądanie". | ||
Możemy teraz dowolnie składać nasze strategie: | Możemy teraz dowolnie składać nasze strategie: | ||
int n<nowiki>=</nowiki>10; | int n<nowiki>=</nowiki>10; | ||
Stack<int,0,Std_exception_on_error_policy,Expandable_new_allocator > | Stack<int,0,Std_exception_on_error_policy,Expandable_new_allocator > s1(n); | ||
Stack<int,10,Abort_on_error_policy,Static_table_allocator > | Stack<int,10,Abort_on_error_policy,Static_table_allocator > s2(n); | ||
([[media:Policy2.cpp | Źródło: policy2.cpp]]) | |||
Widać że takie podejście jest bardzo elastyczne, użytkownik może | Widać, że takie podejście jest bardzo elastyczne, użytkownik może | ||
praktycznie dowolnie konfigurować sobie zachowanie klasy | praktycznie dowolnie konfigurować sobie zachowanie klasy <tt>Stack</tt>, | ||
zwłaszcza że ma możliwość | zwłaszcza, że ma możliwość tworzenia własnych klas wytycznych. | ||
Oczywiście powyższy przykład nie jest do końca dopracowany. Przede | Oczywiście powyższy przykład nie jest do końca dopracowany. Przede | ||
wszystkim strategie przydziału pamięci i strategie sprawdzenia zakresu | wszystkim strategie przydziału pamięci i strategie sprawdzenia zakresu | ||
nie są całkowicie niezależne. Np. w funkcji | nie są całkowicie niezależne. Np. w funkcji <tt>push</tt> jeśli powiedzie | ||
się wywołanie funkcji | się wywołanie funkcji <tt>expand_if_needed()</tt> to nie ma potrzeby | ||
wywoływania funkcji | wywoływania funkcji <tt>check_push()</tt>. Po drugie - całkowicie | ||
pominęliśmy kwestię diagnostyki funkcji alokujących pamięć. Możliwe | pominęliśmy kwestię diagnostyki funkcji alokujących pamięć. Możliwe | ||
rozwiązanie to przekazanie <tt>Checkin_policy</tt> jako argumentu szablonu | |||
do | do <tt>allocator_policy</tt>. Można też rozważyć posiadanie dwu różnych | ||
klas wytycznych, jednej dla obsługi | klas wytycznych, jednej dla obsługi błędów przekroczenia zakresu, | ||
drugiej do obsługi błędów przydziału pamięci. | drugiej do obsługi błędów przydziału pamięci. | ||
Stosowanie wytycznej | ==Dziedziczenie wytycznych== | ||
funkcji statycznych. W przypadku wytycznej | |||
musieliśmy utworzyć obiekt tej klasy ponieważ niektóre implementacje | Stosowanie wytycznej <tt>Checking_policy</tt> sprowadzało się do używania | ||
funkcji statycznych. W przypadku wytycznej <tt>Allocator_policy</tt> | |||
musieliśmy utworzyć obiekt tej klasy, ponieważ niektóre implementacje | |||
tej wytycznej posiadają stan (w tym przypadku jest to zmienna | tej wytycznej posiadają stan (w tym przypadku jest to zmienna | ||
<tt>_size</tt>). Alternatywnym sposobem użycia takiej wytycznej | |||
jest wykorzystanie | jest wykorzystanie dziedziczenia: | ||
template<typename T <nowiki>=</nowiki> int , size_t N <nowiki>=</nowiki> 100, | template<typename T <nowiki>=</nowiki> int , size_t N <nowiki>=</nowiki> 100, | ||
typename Checking_policy <nowiki>=</nowiki> No_checking_policy, | |||
template<typename U,size_t M> class Allocator_policy | template<typename U,size_t M> class Allocator_policy | ||
<nowiki>=</nowiki> Static_table_allocator > | |||
class Stack: private Checking_policy, private Allocator_policy<T,N> { | class Stack: private Checking_policy, private Allocator_policy<T,N> {<br> | ||
typedef typename Allocator_policy<T,N>::rep_type rep_type; | |||
rep_type _rep; | |||
size_t _top; | |||
public: | |||
Stack(size_t n <nowiki>=</nowiki> N):_top(0) { | |||
init(_rep,n); | |||
}; | |||
void push(const T &val) { | |||
expand_if_needed(_rep,_top); | |||
Checking_policy::check_push(_top,this->size()); | |||
_rep[_top++]<nowiki>=</nowiki>val; | |||
} | |||
void pop() { | |||
Checking_policy::check_pop(_top); | |||
--_top; | |||
this->shrink_if_needed(_rep,_top); | |||
} | |||
const T& top() const { | |||
Checking_policy::check_top(_top); | |||
return _rep[top-1]; | |||
} | |||
bool is_empty() { | |||
return !_top; | |||
} | |||
~Stack() {this->dealocate(_rep);} | |||
}; | |||
([[media:Stack3.h | Źródło: stack3.h]]) | |||
Główna zmiana to konieczność kwalifikowania nazw niektórych funkcji | Główna zmiana to konieczność kwalifikowania nazw niektórych funkcji | ||
przez | przez <tt>this-></tt> tak, aby stały się nazwami zależnymi (zob. [http://osilek.mimuw.edu.pl/index.php?title=Zaawansowane_CPP/Wyk%C5%82ad_3:_Szablony_II#Zale.C5.BCne_klasy_bazowe wykład 3.7.1]). | ||
Skorzystałem z dziedziczenia prywatnego aby zaznaczyć, że dziedziczę | |||
Skorzystałem z | implementację a nie interfejs (<tt>Stack</tt> '''nie jest''' | ||
implementację a nie interfejs ( | <tt>Allocator_policy</tt>). Jednak odziedziczenie również interfejsu | ||
klasy <tt>Allocator_policy</tt> poprzez dziedziczenie publiczne może być użyteczne. Aby się o tym przekonać | |||
rozważymy kolejną modyfikację naszego przykładu: przeniesiemy zmienną | |||
rozważymy kolejną modyfikację naszego przykładu: przeniesiemy | <tt>_rep</tt> z klasy <tt>Stack</tt> do klasy wytycznej np. | ||
size_t | template<typename T,size_t N > class Dynamic_table_allocator { | ||
protected: | |||
typedef T * rep_type; | |||
rep_type _rep; | |||
size_t _size; | |||
void init(size_t n) {_size<nowiki>=</nowiki>n;_rep <nowiki>=</nowiki> new T[_size];}; | |||
void expand_if_needed(size_t) {}; | |||
void shrink_if_needed(size_t) {}; | |||
void dealocate(){delete [] _rep;};<br> | |||
size_t size() const {return _size;}; | |||
public: | |||
void resize(size_t n) { | |||
T *tmp<nowiki>=</nowiki> new T[n]; | |||
std::copy(_rep,&_rep[(_size<n)?_size:n],tmp); | |||
delete [] _rep; | |||
_rep <nowiki>=</nowiki> tmp; | |||
_size<nowiki>=</nowiki>n; | |||
} | |||
}; | |||
([[media:Allocator3_1.h | Źródło: allocator3_1.h]]) | |||
Pociąga to za sobą zmiany w klasie <tt>Stack</tt>: | |||
template<typename T <nowiki>=</nowiki> int , size_t N <nowiki>=</nowiki> 100, | |||
typename Checking_policy <nowiki>=</nowiki> No_checking_policy, | |||
/ | template<typename U,size_t M> class Allocator_policy | ||
<nowiki>=</nowiki> Static_table_allocator > | |||
class Stack: private Checking_policy, public Allocator_policy<T,N> {<br> | |||
size_t _top;<br> | |||
public: Stack(size_t n <nowiki>=</nowiki> N):_top(0) { Stack::init(n); }; void | |||
push(const T &val) { | |||
Stack::expand_if_needed(_top); | |||
Checking_policy::check_push(_top,this->size()); | |||
Stack::_rep[_top++]<nowiki>=</nowiki>val; | |||
} | |||
void pop() { | |||
Checking_policy::check_pop(_top); --_top; | |||
Stack::shrink_if_needed(_top); | |||
} | |||
const T& top() const { | |||
Checking_policy::check_top(_top); | |||
return Stack::_rep[top-1]; } | |||
bool is_empty() { return !_top; } Stack() { | |||
Stack::dealocate();} | |||
}; | |||
([[media:Stack3_1.h | Źródło: stack3_1.h]]) | |||
Zmieniłem również sposób uzależniania nazw niezależnych na | |||
kwalifikację ich nazwą klasy <tt>Stack</tt>. Teraz możemy korzystać z | |||
interfejsu klasu <tt>Dynamic_table_allocator</tt> w klasie <tt>Stack</tt>. | |||
Stack<int,n,Std_exception_on_error_policy,Dynamic_table_allocator > s(n); | |||
< | s.resize(20); | ||
([[media:Policy3_1.cpp | Źródło: policy3_1.cpp]]) |
Aktualna wersja na dzień 11:41, 22 lis 2006
Wprowadzenie
Klasy wytycznych, nazywane również klasami reguł (policy classes) służą do parametryzowania zachowania innych klas. Rozważmy przykład funkcji accumulate. Posiada ona również przeciążoną wersję umożliwiającą postawienie dowolnej operacji zamiast dodawania:
template <class InputIterator, class T, class BinaryFunction> T accumulate(InputIterator first, InputIterator last, T init, BinaryFunction binary_op);
Jedyna zmiana w implementacji klasy w stosunku do przykładu 5.2 to zmiania operacji sumowania na:
init = binary_op(init, *first);
Pomimo, że pojawił się dodatkowy szablon klasy, to nie jest to typowa klasa wytycznych. Zachowanie jest określone nie tyle przez ten parametr, co przez funktor przekazany jako argument wywołania.
Możemy jednak zmienić trochę implementację:
template <class Operation, class InputIterator, class T > T accumulate(InputIterator first, InputIterator last, T init) { for (; first != last; ++first) init = Operation::op(init,*first) return init; }
Takiego szablonu możemy używać następująco:
template<typename T> Sumation { static op(const T &a,const T &b) { return a+b; } };
accumulate<Summation<double> >accumulate(first,last,0.0);
Klasa (szablon) Summation jest właśnie klasą wytycznych. W zasadzie nie ma powodów aby implementować funkcję accumulate za pomocą klas wytycznych, poza być może nadzieją na trochę bardziej efektywny kod.
W następnej części przedstawię bardziej realistyczny, ale i bardziej skomplikowany przykład.
Projektowanie za pomocą klas wytycznych
Problemem w uniwersalnych bibliotekach jest duża ilość możliwych implementacji pojedynczego komponentu. Dzieje się tak kiedy implementując komponent możemy podjąć kilka prawie niezależnych od siebie decyzji. Projektując kontener mamy np. do wyboru różne sposoby alokacji pamięci i różne strategie obsługi błędów. Te zagadanienia są w dużej mierze ortogonalne do siebie. Jeśli więc mamy trzy strategie przydziału pamięci i trzy strategie obsługi błędu, to w sumie dostajemy dziewięć możliwych kombinacji. Decyzja o możliwości pracy w środowisku wielowątkowym zwiększą tę liczę dwukrotnie.
Klasy wytycznych mogą pomóc opanować ten kombinatoryczny wzrost ilości możliwości. Idea polega na tym, aby za każdą decyzję odpwiedzialną zrobić jedną klasę wytyczną, przekazywaną jako parametr szablonu. W przytoczonym przykładzie szablon kontenera mógłby posiadać dwa parametry wytycznych
template<typename T, typename Allocator_policy, typename Checking_policy> Kontener;
i potrzebowalibyśmy 6 (3 Allocator_policy i 3 Checking_policy) różnych klas wytycznych. Sześć może się wydawać niewiele mniejsze od dziewięciu, ale takie podejście skaluje się liniowo z liczbą wytycznych: dodanie nowej strategii alokacji pamięci wymaga jednej dodatkowej klasy, a liczba kombinacji zwieksza się do 12. W praktyce wszystkie wytyczne miałyby wartości domyślne.
Stack
Pokażę teraz jak to działa w praktyce na przykładzie znanego już nam szablonu klasy Stack, w którym na początek dokonam drobnych zmian:
template<typename T = int , size_t N = 100> class Stack { private: T rep[N]; size_t _top; public: Stack():_top(0) {} void push(const T &val) {_rep[_top++]=val;} void pop() {--_top;} const T& top() const {return _rep[top-1];} bool is_empty {return !_top;} }
Zmiany polegają na rozdzieniu operacji odczytywania wierzchołka stosu i zdejmowania elementu ze stosu. Umożliwia to między innymi przekazywanie wartości zwracanej ze stosu przez referencje, poza tym jest to bardziej bezpieczne.
Ten kod jest, delikatnie rzecz ujmując, bardzo prościutki. Możemy rozbudowywać go w co najmniej dwu kierunkach. Po pierwsze można użyć dynamicznej alokacji pamięci, po drugie możemy zaimplementować sprawdzanie zakresu aby wykryć próbę włożenia elementu na pełny stos, lub zdjęcia/odczytania elementu ze stosu pustego. W tym przypadku mamy różne możliwości reakcji na te błędy.
Żeby zaimplementować sprawdzanie zakresu dodajemy nowy parametr do szablonu Stack, który będzie określał klasę wytyczną dla tej strategii:
template<typename T = int , size_t N = 100, typename Checking_policy = No_checking_policy > class Stack { private: T _rep[N]; size_t _top; public: Stack():_top(0) {};
void push(const T &val) {
Checking_policy::check_push(_top,N); _rep[_top++]=val; }
void pop() { Checking_policy::check_pop(_top); --_top; }
const T& top() const { Checking_policy::check_top(_top); return _rep[top-1]; }
bool is_empty() { return !_top; }
};
Klasa
struct No_checking_policy { static void check_push(size_t,size_t) {}; static void check_pop(size_t) {}; static void check_top(size_t) {}; };
implementuje najprostszą strategię sprawdzania zakresu: brak sprawdzania. Proszę zauważyć, że w tym wypadku najprawdopodobniej żaden kod nie zostanie dodany: kompilator "wyoptymalizuje" puste funckje.
Inne możliwe strategie to np.
class Abort_on_error_policy { public: static void check_push(size_t top,size_t size) {
if(top >= size) { std::cerr<<"trying to push elemnt on full stack: aborting"<<std::endl; abort(); } };
i podobnie dla pozostałych funkcji sprawdzających };
Programując w C++ wstyd by było nie użyć wyjątków:
struct Std_exception_on_error_policy {
static void check_push(size_t top,size_t size) {
if(top >= size) { throw std::range_error("over the top"); } };
i podobnie dla pozostałych funkcji sprawdzających
};
Teraz możemy prosto konfigurować szablon Stack podając mu odpowiednie argumenty:
Stack<int,10> s_no_check; Stack<double ,100,Abort_on_error_policy> s_abort; Stack<int *,25,Std_exception_on_error_policy> s_except;
W celu zaimplementowania różnych strategii przydziału pamięci dodajemy dodatkowy parametr szablonu, który sam będzie szablonem:
template<typename T = int , size_t N = 100, typename Checking_policy = No_checking_policy, template<typename U,size_t M> class Allocator_policy = Static_table_allocator > class Stack;
Szablon typu Allocator_policy posiada jeden typ stowarzyszony i szereg funkcji:
template<typename T,size_t N = 0> struct Static_table_allocator { typedef T rep_type[N]; void init(rep_type &,size_t) {}; void expand_if_needed(rep_type &,size_t) {}; void shrink_if_needed(rep_type &size_t) {}; void dealocate(rep_type &){};
static size_t size() {return N;};
};
Szablon Stack implementujemy teraz następująco:
template<...> class Stack {
typedef typename Allocator_policy<T,N>::rep_type rep_type; rep_type _rep; size_t _top; Allocator_policy<T,N> alloc; public: Stack(size_t n = N):_top(0) { alloc.init(_rep,n); };
void push(const T &val) { alloc.expand_if_needed(_rep,_top); Checking_policy::check_push(_top,alloc.size()); _rep[_top++]=val; }
void pop() { Checking_policy::check_pop(_top); --_top; alloc.shrink_if_needed(_rep,_top); }
const T& top() const { Checking_policy::check_top(_top); return _rep[top-1]; }
bool is_empty() { return !_top; }
~Stack() {alloc.dealocate(_rep);}
};
Szablon
template<typename T,size_t N > struct Expandable_new_allocator { typedef T * rep_type; size_t _size; void init(rep_type &rep,size_t n) {_size=n;rep = new T [_size];}; void expand_if_needed(rep_type & rep,size_t top) { if(top == _size) { _size=2*_size; T *tmp= new T[_size]; std::copy(rep,&rep[top],tmp); delete [] rep; rep = tmp; } }; void shrink_if_needed(rep_type &size_t) { }; void dealocate(rep_type &rep){delete [] rep;};
size_t size() const {return _size;}; };
definiuje strategię dynamicznego przydziału pamięci "na żądanie". Możemy teraz dowolnie składać nasze strategie:
int n=10; Stack<int,0,Std_exception_on_error_policy,Expandable_new_allocator > s1(n); Stack<int,10,Abort_on_error_policy,Static_table_allocator > s2(n);
Widać, że takie podejście jest bardzo elastyczne, użytkownik może praktycznie dowolnie konfigurować sobie zachowanie klasy Stack, zwłaszcza, że ma możliwość tworzenia własnych klas wytycznych.
Oczywiście powyższy przykład nie jest do końca dopracowany. Przede wszystkim strategie przydziału pamięci i strategie sprawdzenia zakresu nie są całkowicie niezależne. Np. w funkcji push jeśli powiedzie się wywołanie funkcji expand_if_needed() to nie ma potrzeby wywoływania funkcji check_push(). Po drugie - całkowicie pominęliśmy kwestię diagnostyki funkcji alokujących pamięć. Możliwe rozwiązanie to przekazanie Checkin_policy jako argumentu szablonu do allocator_policy. Można też rozważyć posiadanie dwu różnych klas wytycznych, jednej dla obsługi błędów przekroczenia zakresu, drugiej do obsługi błędów przydziału pamięci.
Dziedziczenie wytycznych
Stosowanie wytycznej Checking_policy sprowadzało się do używania funkcji statycznych. W przypadku wytycznej Allocator_policy musieliśmy utworzyć obiekt tej klasy, ponieważ niektóre implementacje tej wytycznej posiadają stan (w tym przypadku jest to zmienna _size). Alternatywnym sposobem użycia takiej wytycznej jest wykorzystanie dziedziczenia:
template<typename T = int , size_t N = 100, typename Checking_policy = No_checking_policy, template<typename U,size_t M> class Allocator_policy = Static_table_allocator > class Stack: private Checking_policy, private Allocator_policy<T,N> {
typedef typename Allocator_policy<T,N>::rep_type rep_type; rep_type _rep; size_t _top; public: Stack(size_t n = N):_top(0) { init(_rep,n); }; void push(const T &val) { expand_if_needed(_rep,_top); Checking_policy::check_push(_top,this->size()); _rep[_top++]=val; } void pop() { Checking_policy::check_pop(_top); --_top; this->shrink_if_needed(_rep,_top); } const T& top() const { Checking_policy::check_top(_top); return _rep[top-1]; } bool is_empty() { return !_top; } ~Stack() {this->dealocate(_rep);} };
Główna zmiana to konieczność kwalifikowania nazw niektórych funkcji przez this-> tak, aby stały się nazwami zależnymi (zob. wykład 3.7.1). Skorzystałem z dziedziczenia prywatnego aby zaznaczyć, że dziedziczę implementację a nie interfejs (Stack nie jest Allocator_policy). Jednak odziedziczenie również interfejsu klasy Allocator_policy poprzez dziedziczenie publiczne może być użyteczne. Aby się o tym przekonać rozważymy kolejną modyfikację naszego przykładu: przeniesiemy zmienną _rep z klasy Stack do klasy wytycznej np.
template<typename T,size_t N > class Dynamic_table_allocator { protected: typedef T * rep_type; rep_type _rep; size_t _size; void init(size_t n) {_size=n;_rep = new T[_size];}; void expand_if_needed(size_t) {}; void shrink_if_needed(size_t) {}; void dealocate(){delete [] _rep;};
size_t size() const {return _size;}; public: void resize(size_t n) { T *tmp= new T[n]; std::copy(_rep,&_rep[(_size<n)?_size:n],tmp); delete [] _rep; _rep = tmp; _size=n; } };
Pociąga to za sobą zmiany w klasie Stack:
template<typename T = int , size_t N = 100, typename Checking_policy = No_checking_policy, template<typename U,size_t M> class Allocator_policy = Static_table_allocator > class Stack: private Checking_policy, public Allocator_policy<T,N> {
size_t _top;
public: Stack(size_t n = N):_top(0) { Stack::init(n); }; void push(const T &val) { Stack::expand_if_needed(_top); Checking_policy::check_push(_top,this->size()); Stack::_rep[_top++]=val; } void pop() { Checking_policy::check_pop(_top); --_top; Stack::shrink_if_needed(_top); } const T& top() const { Checking_policy::check_top(_top); return Stack::_rep[top-1]; } bool is_empty() { return !_top; } Stack() { Stack::dealocate();} };
Zmieniłem również sposób uzależniania nazw niezależnych na kwalifikację ich nazwą klasy Stack. Teraz możemy korzystać z interfejsu klasu Dynamic_table_allocator w klasie Stack.
Stack<int,n,Std_exception_on_error_policy,Dynamic_table_allocator > s(n); s.resize(20);