Zaawansowane CPP/Wykład 6: Funkcje typów i inne sztuczki

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania

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) )};
};

(Źródło: Isclass.cpp)

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 }; };

(Źródło: Convertible.cpp)

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 }; 
};

(Źródło: Convertible.cpp)

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; };

(Źródło: Strip.cpp)

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; }

(Źródło: Promote.cpp)

teraz polecenia

std::vector<double> x(10);
std::vector<double> y(10);
x+y;

(Źródło: Promote.cpp)

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;
};

(Źródło: Promote.cpp)

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;
};

(Źródło: Promote.cpp)

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)

(Źródło: Promote.cpp)

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);

6.3.5 Generowanie klas

6.4 Podsumowanie