Zaawansowane CPP/Wykład 6: Funkcje typów i inne sztuczki
6.1 Wprowadzenie
Funkcja jest podstawowym pojęciem w większości języków programowania, z którym wszyscy jesteśmy dobrze obeznani. Funkcje przyjmują zestaw argumentów i zwracają jakąś wartość. Szablony dają ciekawą możliwość interpretowania ich jako funkcji typów: funkcje których argumentem są typy, a wartością zwracaną typ lub jakaś wartość. Weźmy dla przykładu szablon sum_traits z poprzedniego modułu. Można interpretować go jako dwie funkcje przyjmujące typ poprzez parametr szablonu i zwracające albo wartość
sum_traits<int>::zero();
albo typ
sum_traits<int>::sum_type
Takie funkcje możemy definiować poprzez wypisanie wszystkich specjalizacji pełnych jak w przypadku sum_traits, albo przez wykorzystanie specjalizacji częściowych jak np. dla iterator_traits. Najciekawsza możliwość to jednak pisanie "obliczalnego kodu" takich funkcji, przy czym "obliczenia" dokonują się w czasie kompilacji. Przykłady takich funkcji zostaną podane na tym wykładzie. W realnych zastosowaniach wykorzystuję się kombinację wszystkich powyższych możliwości.
Korzystając z szablonów można również uogólnić pojęcie listy, i definiować listy typów. Umożliwiają one między innymi, indeksowany dostęp do swoich składowych, przekazywanie zmiennej liczby parametrów typu do szablonów i automatyczną generację hierarchi klas opartych na tych typach. Listy typów omówię w drugiej części tego wykładu.
6.2 Funkcje typów
6.2.1 If-Then-Else
Zacznę od przydatnej konstrukcji implementującej możliwość wyboru jednego z dwu typów na podstawie stałej logicznej (typu bool). Dokonujemy tego za pomocą szablonu podstawowego
template<bool flag,typename T1,typename T2> struct If_then_else { typedef T1 Result; };
który będzie podstawiany dla wartości flag=true i jego specjalizacji dla wartości flag=false:
template<typename T1,typename T2> struct If_then_else<false,T1,T2> { typedef T2 Result; };
Teraz możemy go np. wykorzystać do wybrania większego z dwu typów:
template<typename T1,typenameT2> struct Greater_then { typedef typename If_then_else<sizeof(T1)>sizeof(T2),T1,T2>::result result; };
6.2.2 Sprawdzanie czy dany typ jest klasą
Następny przykład to szablon służący do sprawdzania czy dany typ jest klasą. Wykorzystamy w tym celu operator sizeof() i przeciążane szablony funkcji razem z zasadą "nieudane podstawienie nie jest błędem" (zob. 3.5). Potrzebujemy więc wyrażenia które nie ma sensu dla dla typów nie będacych klasami. Takim wyrażeniem będzie int C::* oznaczające wskaźnik do pola składowego klasy C typu int. Cały szablon wygląda następująco.
template<typename T> class Is_class { najpierw definiujemy dwa typy różniące sie rozmiarem typedef char one; typedef struct {char c[2];} two; teraz potrzebne bedą dwa przeładowane szablony template<typename U> static one test(int U::*); template<typename U> static two test(...); to który szablon został wybrany sprawdzamy poprzez sprawdzenie rozmiaru zwracanego typu enum {yes = (sizeof(test<T>(0))==sizeof(one) )}; };
Operator sizeof(test<T>(0)) musi rozpoznać typ zwracany przez funkcję test<T>(0). W tym celu uruchamiany jest mechanizm rozstrzygania przeciążenia. Jeśli typ T nie jest klasa to próba podstawienia pierwszej funkcji spowoduje powstanie niepoprawnej konstrukcji T::* i podstawienie się nie powiedzie. Druga funkcja ma argumenty pasujące do czegokolwiek więc jej podstawienie zawsze się powiedzie. Druga funkcja zwraca typ o rozmiarze większym od rozmiaru typu one więc stała yes otrzyma wartość false.
Jeśli typ T jest klasą to int T::* jest poprawne (na tym etapie nie jest istotne czy klasa w ogóle posiada taką składową) i mechanizm rozstrzygania będzie pracował dalej sprawdzając czy argument wywołania jest zgodny z int T::*. W tym przypaku jest to zero które może być użyte jako wskaźnik więc podstawienie się powiedzie. Oczywiście drugie podstawienie też by się powiodło ale jest mniej specjalizowane. W wyniku zmienna yes otrzyma wartość true. Warto zauważyć że gdybyśmy zamiast zera podstawili jako argument funkcji test<T>(int T::*) jakąś inna liczbę całkowitą to podstawienie się nie powiedzie i zawsze otrzymamy fałsz dla zmiennej yes.
6.2.3 Sprawdzanie możliwości rzutowania
Kolejny przykład wykorzystuje tą samą technikę w celu stwierdzenia czy jeden z typów można rzutować na drugi.
template<typename T,typename U> class Is_convertible { typedef char one; typedef struct {char c[2];} two; tym razem korzystamy ze zwykłych przeciążonych funkcji static one test(U) ; static two test(...); static T makeT();
public: enum {yes = sizeof(test(makeT()) )==sizeof(one), same_type=false }; };
Teraz sprawdzane są dwie przeciążone funkcje, makeT() zwraca obiekt typu T więc jeśli typ T może być rzutowany na U to wybrane zostanie dopasowanie pierwszej funkcji jako bardziej wyspecjalizowanej. Funkcja makeT() została użyta zamiast np. T() bo konstruktor defaultowy może dla tego typu nie istnieć. Dodatkowa specjalizacja
template<typename T> class Is_convertible<T,T> { public: enum {yes = true, same_type=true }; };
pozwala nam użyć tej klasy do identyfikacji identycznych typów.
6.2.4 Zdejmowanie kwalifikatorów
Każdy typ w C++ może być opatrzony dodatkowymi kwalifikatorami takimi jak const czy & (referencja). Mając dany typ T dodanie do niego kwalifikatora jest proste. Z pozoru jednak może się to wiązać z możliwością wygenerowania niepoprawnych podwójnych kwalifikatorów np. const const T. Okazuje się jednak że o ile
const const int i =0;
jest konstrukcją nie prawdłową to w przypadku argumentów szablonu nadmiarowe kwalifikatory zostaną zignorowane:
template<typename T> struct const_const { const T t = T(); }; const_const<const int> a;
jest poprawne i pole t będzie typu const int. To samo tyczy się referencji. Pomimo tych udogodnień może być konieczne operacją usunięcia jednego lub obydwu kwalifikatorów i uzyskanie gołego typu podstawowego. W tym celu możemy zdefiniować szablon (zob. D. Vandervoorde, N. Josuttis: "C++ Szablony, Vademecum profesjonalisty", rozd. 17)
template<typename T> struct Strip { typedef T arg_t; typedef T striped_t; typedef T base_t; typedef const T const_type;
typedef T& ref_type; typedef T& ref_base_type; typedef const T & const_ref_type;
};
i jego specjalizację dla typów z kwalifikatorem const
template<typename T> struct Strip< T const> { typedef const T arg_t; typedef T striped_t; typedef typename Strip<T>::base_t base_t; typedef T const const_type;
typedef T const & ref_type; typedef T & ref_base_type; typedef T const & const_ref_type; };
i dla referencji
template<typename T> struct Strip<T&> {
typedef T& arg_t; typedef T striped_t; typedef typename Strip<T>::base_t base_t; typedef base_t const const_type;
typedef T ref_type; typedef base_t & ref_base_type; typedef base_t const & const_ref_type; };
Proszę zwrócić uwagę na konstrukcję
typedef typename Strip<T>::base_t base_t;
Ponieważ typ typ T może się oznacząc typ kwalifikowany za pomocą const wykorzystujemy rekurencyjne odwołanie aby uzyskać typo podstawowy. Jest to przykład techniki metaprogramowania które bardziej szczegółówobędzie omówione w rozdziale 8.
6.2.5 Cechy typów
6.2.6 Cechy promocji
Załóżmy, że postanowiliśmy zaimplementować możliwość dodawania wektorów:
template<typename T> std::vector<T> operator+(const std::vector<T> &a, const std::vector<T> &b) {
assert(a.size()==b.size());
std::vector<T> res(a); for(size_t i=0;i<a.size();++i) res[i]+=b[i];
return res; }
teraz polecenia
std::vector<double> x(10); std::vector<double> y(10);
x+y;
skompilują się, ale
std::vector<int> l(10);
x+l;
już nie. Ponieważ dodawanie double do int jest dozwolone to rozsądne by było aby nasz implemenatacje też na to zezwalała. Przerabiamy więc nasz operator+() (lub dodajemy przeciążenie):
template<typename T,typename U> std::vector<T> operator+(const std::vector<T> &a, const std::vector<U> &b) {
assert(a.size()==b.size());
std::vector<T> res(a); for(size_t i=0;i<a.size();++i) res[i]+=b[i];
return res; }
Kłopot z tą definicją to typ zwracany, który zależy teraz od kolejności składników dodawania a nie od ich typu. Abu rozwiązać ten problem zdefiniujemy sobie klasę cech która będzie określała typ wyniku na podstawie typu składników. Wybierzemy następującą strategię jeśli typy mają różny rozmiar to wybieramy typ większy. Jeżeli mają ten sam rozmiar to liczymy na specjalizacje:
template<typename T1,typename T2> struct Promote { typedef typename If_then_else<(sizeof(T1) > sizeof(T2)), T1, typename If_then_else< (sizeof(T1)< sizeof(T2)), T2, void>::Result >::Result Result; };
Dla identycznych typów wynik jest jasny (choć jak przekonuje nas przykład 5.1 typ sumy nie musi być typem składników), ale niemniej musimy go zdefiniować:
template<typename T> struct Promote<T,T> { typedef T Result; };
Resztę musimy definiować ręcznie korzystając ze specjalizacji pełnych. Można to sobie uprościć definiując makro
#define MK_PROMOTE(T1,T2,Tr) \ template<> class Promotion<T1, T2> { \ public: \ typedef Tr Result; \ }; \ \ template<> class Promotion<T2, T1> { \ public: \ typedef Tr Result; \ };
MK_PROMOTE(bool, char, int) MK_PROMOTE(bool, unsigned char, int) MK_PROMOTE(bool, signed char, int)
6.3 Listy typów
6.3.1 Definicja
Listy typów są ciekawą konstrukcją zaproponowaną przez Alexandrescu. Ich szczegółowy opis i zastosowania można znaleźć w Alexandrescu: "Nowoczesne projektowanie". Definicja listy typów opiera się na rekurencyjnej implementacji listy znanej miedzy innymi z Lisp-a: lista składa się z pierwszego elementu (głowy) i reszty (ogona), co możemy zapisać jako:
template<typename H,typename T> struct Type_list { typedef H head; typedef T tail; }
Do tego potrzebna nam jest jeszcze definicja pustego typu
class Null_type {};
jako znacznika końca listy. I tak
Type_list<int,Null_type>
oznacza jednoelementową listę (int)
Type_list<double,Type_list<int,Null_type> >
listę dwuelemntową (double,int) i tak dalej.
6.3.2 Length
Ta prosta struktura daje zadziwiająco wiele możliwości. Na początek napiszemy funkcję zwracającą długość listy. W tym celu skorzystamy z ekurencyjnej definicji: długość listy to jeden plus długość ogona, długość listy pustej wynosi zero. Tą definicję implementujemy następująco:
template<typename T> struct Length; template<> struct Length<Null_type> { enum {value = 0}; };
template<typename H,typename T> struct Length<Type_list<H,T> > { enum {value = 1 + Length<T>::value}; };
6.3.3 Indeksowanie
Tą samą techniką możemy zaimplementować indeksowany dostęp do elementów listy. Znów korzystamy z rekurencji: element o indeksie i to albo głowa (jeśli i==0, albo element o indeksie i-1 w jej ogonie.
template<typename T,size_t I> struct At;
template<typename T,typename H> struct At<T,0> { typedef T result; };
template<typename T,typename H> struct At<T,I> { typedef typename At<H,I-1>::result result; };
6.3.4 Generowanie switch-a
W bibliotece boost jest zaimplementowana klasa any której obiekty mogą reprezentować dowolną wartość {BOOST_HOME/doc/html/any.html}. Żeby taką wartość odczytać musimy użyć odpowiedniej konkretyzacji szablonu funkcji any_cast:
boost::any val; cout<<any_cast<int>(val)<<endl;
Jeśli w szablonie podstawimy nie ten typ co trzeba, to otrzymamy wyjątek. Chcemy teraz napisać funkcję która drukuje wartości typu any przy czym wiemy że przechowywane są w nich wartości tylko kilku wybranych typów (np.int, double, string). Ponieważ any udostępnia informację o typie przechowywanej w niej wartości moglibyśmy taką funkcję print_any() zaimplementować następująco:
print_any(std::ostream &of,boost::any val) { if(val.type()==typeid(int) ) of<<any_cast<int>(val)<<std::endl; else if val.type()==typeid(double) ) of<<any_cast<double>(val)<<std::endl; else if val.type()==typeid(string) ) of<<any_cast<string>(val)<<std::endl; else of<<"unsuported type"<<std::endl; }
Sprobujemy teraz zaimplementować to samo za pomocą list typów, dodatkowo zażądamy aby móc drukować również wartości typu vector<T> gdzie T jest typem z listy.
Jak zwykle musimy sformułować problem rekurencyjnie: Sprawdzamy czy typ val jest typem głowy listy, jeśli tak to drukujemy jeśli nie to próbujemy drukować val używając ogona listy
template<typename T> void print_val(std:ostream &of,boost::any val) { typedef typename T::Head Head; typedef typename T::Tail Tail;
if(val.type()==typeid() ) { of<<any_cast<Head>(val)<<std::endl; } else if (val.type()==typeif(std::vector<Head>)) { of<<any_cast<std::vector<Head> >(val)<<std::endl; } else print_val<Tail>(of,val); }
Potrzebujemy jeszcze warunku kończącego rekurencję
template<> void print_val<Null_type>(std::ostream &of,boost::any) { of<<"don't know how to print this"<<std::endl; }
i możemy już używać naszego szablonu:
typedef TypeList<double, Type_list<int, Type_list<string, Null_type> > > my_list;
print_val<my_list>(val);