Definicija predloška klase

Predložak klase (class template) je parametrizirana klasa koja služi prevodiocu za generiranje klase kada se specificiraju parametri predloška. Kao i funkcija, klasa može biti parametrizirana tipom ili konstantnim izrazom.

template <typename T>
class Vect
{
public:
    typedef int index;

    Vect() : size(0), data(0) {}
    explicit Vect(index n, T v=0);
    Vect(const Vect& v);                            // CCtor
    // ....
   private:
    index   size;
    T *data;
};

Instancijacija predloška klase

Vect<int>    vi;    // Vektor cijelih brojeva
Vect<double> vc;    // Vektor brojeva dvostruke preciznosti
Vect<float>  vf;    // Vektor brojeva jednostruke preciznosti

Kada prevodilac naiđe na jednu od ovih deklaracija on generira klasu polazeći od predloška Vect<T>, supstituirajući za T odgovarajući parametar, i kompilira generiranu klasu.

Parametar koji smo supstituirali mora biti kompatibilan s predloškom klase (sve operacije nad supstituiranim tipom moraju biti dozvoljene).

Klase Vect<int>, Vect<double> i Vect<float> sa stanovišta prevodioca su posve nezavisne jedna od druge i objekt jednog tipa ne može se tretitrati kao objekt drugog tipa.

Implementacija metoda članica

Primjer implementacije: konstruktor i operator pridruživanja.

// Konstruktor
template <typename T>
Vect<T>::Vect(index n, T v) : size(n), data(new T[n]) {
    for(index i=0; i < size; ++i) data[i]=v;
}

Ime funkcije (ovdje konstruktora Vect) je kvalificirano punim imenom klase (Vect<T>) i nakon toga na ime klase referiramo bez parametra (kao Vect, a ne Vect<T>).

template <typename T>
Vect<T>& Vect<T>::operator=(T x) {
    for(index i=0; i < size; ++i) data[i]=x;
    return *this;
}

Napomene

  • Ime template parametra u definiciji predloška klase je proizvoljno kao i ime formalnog parametra funkcije.
  • Normalna pravila skrivanja imena iz vanjskog bloka deklaracijama u unutarnjem bloku vrijede i za parametar klase.
  • Funkcije članice parametrizirane klase i same su parametrizirane funkcije, ali se kod njih određivanje template parametra ne radi na osnovu argumenta funkcije nego na osnovu tipa objekta na kojem je funkcija pozvana.
  • Funkcija članica parametrizirane klase instancira samo onda kada se koristi. Funkcija članica koja se nikad ne poziva u programu neće biti instancirana.

Tip parametra

Predložak klase može imati tri tipa parametara:

Parametar koji nije tip

Parametar koji nije tip može biti:

  • Cjelobrojnog ili enumeracijskog tipa;
  • Referenca ili pokazivač na objekt ili na funkciju

Takav parametar mora biti konstanta poznata za vrijeme kompilacije.

Primjer:

Vektora s duljinom fiksiranom za vrijeme kompilacije:

template <typename T, int dim>
class FixedVect
{
    public:
        typedef int index;
        explicit FixedVect(T v=0);

        T& operator[](index i) {return data[i]; }
        const T&  operator[](index i) const {return data[i]; }

        FixedVect& operator=(T x);
        // ...
        index  n() const {return dim; }
    private:
        T data[dim];
};

Implementacija

// Konstruktor
template <typename T, int dim>
FixedVect<T,dim>::FixedVect(T v)
{
    for(index i=0; i < dim; ++i) data[i]=v;
}

class i typename

Umjesto ključne riječi typename za deklaraciju parametra u predlošku klase ili funkcije koji je tip možemo koristiti class:

template <class T>
class Vect{
....
};

typename u deklaraciji varijabli

Kad su tipovi definirani unutar predloška klase, kao što je to index u Vect<T>, prevodilac ne može, uvijek, razlikovati radi li se radi o tipu ili vrijednosti. Kada mu to nije moguće on pretpostavlja da se radi o vrijednosti. Ako se pak radi o tipu moramo mu to naglasiti s typename ispred tipa.

template <typename T, int dim>
void print(const std::string& str, FixedVect<T,dim>& v)
{
    FixedVect<T,dim>::index * pi;// Množenje, a ne deklaracija pokazivača
// ....
}

Time dobivamo grešku pri kompilaciji. Da bismo to ispravili treba koristiti typename, koji će reći prevodiocu da je prvi argument tip:

template <typename T, int dim>
void print(const std::string& str, FixedVect<T,dim>& v)
{
    typename FixedVect<T,dim>::index * pi;// Deklaracija pokazivača
// ....
}

Instancijacija predloška

Vect<int>    vi;     // Instancira klasu i kreira objekt vi

Ako koristimo samo pokazivač ili referencu na instancu klase neće doći do instanciranja klase:

Vect<double> * vc;    // Ne povlači instanciranje klase

Ista ekonomičnost se primijenjuje i na funkcije članice predloška klase. Ako se neka funkcija članica ne koristi, ona neće biti niti instancirana.

Organizacija koda

Kada prevodilac naiđe na mjesto na kome se prvi puta koristi neka instanca predloška klase ili funkcije on treba kreirati i kompilirati instancu. Da bi to mogao učiniti nije mu dovoljna samo deklaracija predloška.

Negativna posljedica takve organizacije:

C++ standard predviđa klasičnu organizaciju koda i za predloške (tzv. separate compilation model), ali ona za sada nije široko podržana od strane prevodilaca.

Kompilacijski polimorfizam

Primjer: Dinamički polimorfizam

dynamic.png

Dinamički polimorfizam: Klase

// Javno sučelje za sve figure
class Figura{
    public:
        virtual void crtaj() const = 0;
        virtual double centar() const = 0;
};

class Krug : public Figura{
    public:
        virtual void crtaj() const;
        virtual double centar() const;
        // ...
};

class Kvadrat : public Figura{
    public:
        virtual void crtaj() const;
        virtual double centar() const;
        // ...
};

class Trokut : public Figura{
    public:
        virtual void crtaj() const;
        virtual double centar() const;
        // ...
};

Dinamički polimorfizam: Klijenti

void nacrtaj(const Figura& fig)
{
    fig.crtaj();
}

double udaljenost(const Figura& fig1, const Figura& fig2)
{
    return std::abs( fig2.centar() - fig1.centar() );
}

Primjena

int main()
{
    Krug k;
    Trokut t;

    nacrtaj(k);
    nacrtaj(t);

    std::cout << udaljenost(k,t) << std::endl;
    return 0;
}

Statički polimorfizam

static.png

Klase

class Krug{
    public:
         void crtaj() const;
         double centar() const;
        // ...
};

class Kvadrat{
    public:
         void crtaj() const;
         double centar() const;
        // ...
};

class Trokut{
    public:
         void crtaj() const;
         double centar() const;
        // ...
};

Statički polimorfizam: Klijenti

template <typename Figura>
void nacrtaj(const Figura& fig)
{
    fig.crtaj();
}

template <typename Figura1, typename Figura2>
double udaljenost(const Figura1& fig1, const Figura2& fig2)
{
    double tmp =std::abs( fig2.centar() - fig1.centar() );
    return tmp;
}

Primjena

int main()
{
    Krug k;
    Trokut t;

    nacrtaj(k);
    nacrtaj(t);

    std::cout << udaljenost(k,t) << std::endl;
    return 0;
}

Predložak funkcije kao članica klase

Predložak funkcije može biti članica klase:

class X{
    private:
        int i;
    public:
        template <typename T> void assign(T x) { i=x; }
        void print(){ std::cout << i << std::endl; }
};

Klasa X nije parametrizirana, ali sadrži parametriziranu funkciju assign.

X xi;
xi.assign(3L);
xi.print();
xi.assign(45.6L);
xi.print();

Generira se funkcija assign za T=long int i za T=long double.

Predložak funkcije kao članica predloška klase

Parametrizirana funkcija članica može se naći unutar parametrizirane klase. U tom slučaju imamo dva parametra:

template <typename TT>
class XX{
    private:
        TT i;
    public:
        template <typename T> void assign(T x);
        void print(){ std::cout << i << std::endl; }
};

Definicija:

template <typename TT>  template <typename T>
void XX<TT>::assign(T x) { i = x; }

Instancijacija se vrši prema tipu parametra: prvo se određuje parametar klase iz tipa objekta na kome je funkcija pozvana, a zatim parametri metode.

Napomena. Parametrizirana funkcija članica ne može biti virtualna.

Konstruktor kopije i OP ne mogu biti predlošci

Napomena
Prevodilac nikad neće uzeti u obzir predložak za generiranje konstruktora kopije ili operatora pridruživanja. Tada pored predloška moramo napisati neparametrizirane verzije tih funkcija.
#include <iostream>
using std::cout;

template <typename T>
class X{
  public:
    X(){}
    template <typename T1>
      X(X<T1> const & rhs){
        cout << "template CCtor\n";
      }
    template <typename T1>
    X& operator=(X<T1>const & rhs){
      cout << "template OP\n";
      return *this;
    }
};

int main()
{
  X<int> x;
  X<int> y(x);   // nema ispisa -- sintetizirani CCtor
  X<double> z(x);// ispis, template CCtor

  y = x; // nema ispisa -- sintetizirani OP
  z = x; // ispis -- template OP

  return 0;
}

U klasu trebamo dodati svoje verzije CCtor-a i OP-a. Pri tome kako je svaki parametrizirani konstruktor običan konstruktor s jednim parametrom dobro ga je učiniti eksplicitnim.

#include <iostream>
using std::cout;

template <typename T>
class X{
  public:
    X(){}
    // Konstruktor kopije
    X(X const & rhs) { cout << "CCtor\n"; }

    template <typename T1>
      explicit X(X<T1> const & rhs){
        cout << "template CCtor\n";
      }

    // Operator pridruživanja
    X& operator=(X const & rhs){
      cout << "OP\n";
      return *this;
    }


    template <typename T1>
    X& operator=(X<T1>const & rhs){
      cout << "template OP\n";
      return *this;
    }
};

int main()
{
  X<int> x;
  X<int> y(x);   // CCtor
  X<double> z(x);// template CCtor

  y = x; // OP
  z = x; // template OP

  return 0;
}

Friend deklaracije u predlošku klase

Pretpostavimo da u predlošku klase želimo imati deklaraciju friend "funkcije" koja je i sama predložak funkcije. Na primjer,

template <typename T>
class A{
    public:
        A(T _i) : i(_i) {}
    private:
        T i;
        template <typename S> friend void f(const S&);
};

Definicija predloška funkcije prijatelja može doći iza definicije predloška klase :

template <typename S>
void f(const S& a){
    cout << "f: i = " << a.i << endl;
}

Glavni program:

int main()
{
    A<int> a(5);
    A<string> b("A<string>");

    f(a);  // f(const A<int>&)
    f(b);  // f(const A<string>&)

    return 0;
}

Friend deklaracije u predlošku klase (2)

Ovdje ostvarujemo "užu" povezanost dva predloška:

template <typename T> class A;

template <typename T>
void f(const A<T>& a);   // deklaracija je obavezna

template <typename T>
class A{
    public:
        A(T _i) : i(_i) {}
    private:
        T i;
        friend void f<T>(const A&); // A je ovdje A<T>
};

Sada je klasa funkcija sužena na samo one koje mogu biti prijatelji klase.

int main()
{
    A<int> a(5);
    A<string> b("A<string>");

    f(a);  // f(const A<int>&)
    f(b);  // f(const A<string>&)

    return 0;
}

Friends (3)

Prema C++11 standardu parametar predloška se može deklarirati kao friend predloška.

#include <utility>
template <typename T>
class X{
friend T; // T ima pristup do X<T>
 // ....
};

Prema C++11 standardu pomoću using možemo uvesti parametrizirani alias za neko ime:

template <typename T> using twin = std::pair<T,T>;

int main()
{
    twin<int> a;
    a= std::make_pair(1,1);

    return 0;
}

Statički članovi predloška klase

Svaka instancijacija parametrizirane klase ima svoje statičke varijable koje dijele sve njene instance (objekti):

template <typename T>
struct X{
     T val;
     static std::string  error_message;
     static void print_error_message();
};

template <typename T>
void X<T>::print_error_message(){
    std::cout << error_message << std::endl;
}

Definicija statičke varijable iz predloška klase može biti u datoteci zaglavlja:

// Definicija statičkog člana
template <typename T>
std::string X<T>::error_message = "Error!";

U sljedećem primjeru imamo dvije statičke varijable: X<int>::error_messge i X<double>::error_messge:

int main()
{
    X<int> x;
    x.print_error_message();
    X<double> xx;
    xx.print_error_message();

    return 0;
}

Specijalizacija predloška

Predloške klasa možemo specijalizirati i tada specijaliziramo sve funkcije članice:

template <typename T, typename S>
class A
{
    private:
        T x;
        S y;
    public:
        A(T _x, S _y) : x(_x), y(_y) {}
        T get_x() const {return x;}
        S get_y() const {return y;}
        void h() { std::cout << "Predložak klase A" << std::endl; }
};
// Specijalizacija T=int, S=double
template <>
class A<int,double>  // tip mora biti naveden jer se "ne deducira"
{
    private:
        int x;
        double y;
    public:
        A(int _x, double _y) : x(_x), y(_y) {}
        int get_x() const {return x;}
        double get_y() const {return y;}
        void h() { std::cout << "Specijalizacija 1 klase A" << std::endl; }
};

Specijalizacija predloška ne mora slijediti ni sučelje ni implementaciju predloška

// Specijalizacija T=char *, S=char *
//
template <>
class A<char *, char *>  // tip mora biti naveden jer se "ne deducira"
{
    private:
        char * x;
        char * y;
    public:
        A(char* _x, char* _y) : x(_x), y(_y) {}
        ptrdiff_t get_xy() const {return y - x;}
        void h() { std::cout << "Specijalizacija 2 klase A" << std::endl; }
};

Parcijalne specijalizacije (1)

U parcijalnim specijalizacijama specijalizirani predložak je ponovo predložak. Ilustracija:

#include <iostream>

template <typename T, typename S>
class A
{
    private:
        T x;
        S y;
    public:
        A(T _x, S _y) : x(_x), y(_y) {}
        T get_x() const {return x;}
        S get_y() const {return y;}
};

Napravimo tri djelomične (parcijalne) specijalizacije:

Parcijalne specijalizacije (2)

// Specijalizacija S = T
template <typename T>
class A<T,T>
{
    private:
        T x;
        T y;
    public:
        A(int _x, double _y) : x(_x), y(_y) {}
        T get_x() const {return x;}
        T get_y() const {return y;}
};

// Specijalizacija T = int
template <typename S>
class A<int,S>
{
    private:
        int x;
        S y;
    public:
        A(int _x, double _y) : x(_x), y(_y) {}
        int get_x() const {return x;}
        S get_y() const {return y;}
};

// Specijalizacija T -> T *, S -> S *
//
template <typename T, typename S>
class A<T *, S *>
{
    private:
        T * x;
        S * y;
    public:
        A(T* _x, S* _y) : x(_x), y(_y) {}
        T* get_x() const {return x;}
        S* get_y() const {return y;}
};

Parcijalne specijalizacije (3)

Princip: prevodilac će pri instanciranju klase uvijek preferirati (naj)specijaliziraniju verziju.

int main()
{
    A<char,int>     a('a',1); // A<T,S>
    A<int,double>   b(2,3);   // A<int,S>
    A<int*,double*> c(0,0);   // A<T*,S*>

    // Greške zbog dvosmislenosti
    A<int,int>      d(1,1);    // A<int,S> ili A<T,T> ?
    A<int*,int*>    e(0,0);    // A<T*,S*> ili A<T,T> ?

    return 0;
}

Zaključak: Treba paziti da se ne generiraju dvosmislene situacije.

Dodijeljeni (default) parametri predloška

Neki parametri u predlošku klase ili funkcije mogu imati dodijeljenu vrijednost (to nije vrijedilo za predložak funkcije prije C++11).

#include <vector>
#include <stdexcept>

template <typename T, typename CONTAINER = std::vector<T> >
class Stack{
    public:
        void push(T const& x){ data.push_back(x); }
        void pop();               // pop element
        const T& top() const;     // vrati const referencu na top element
        T& top();                 // vrati referencu na top element
        bool empty() const { return data.empty(); }
    private:
        CONTAINER data;     // elementi
};

Primjer korištenja ove klase dan je ovdje:

// stack int-ova s defaultnim spremnikom (vektorom)
Stack<int> stack_i;
// stack doubleova se deque-om kao spremnikom
Stack<double, std::deque<double> > stack_d;
// ...

Zadatak: Implementirajte u klasi Stack operator pridruživanja koji može uzeti kao desni operand Stack bilo kojeg tipa. Deklaracija operatora treba biti sljedeća:

template <typename T, typename CONTAINER = std::vector<T> >
class Stack{
        // ...
        template <typename S, typename CONTAINER2>
        Stack<T,CONTAINER>& operator=(Stack<S,CONTAINER2>const &);
    private:
        CONTAINER data;     // elementi
};

Template template parametar

Gornji primjer upotrebe klase Stack pokazuje kako je u nekim situacijama prirodno da parametar predloška bude i sam predložak. Željeli bismo, na primjer, umjesto

Stack<double, std::deque<double> > stack_d;

pisati

Stack<double, std::deque> stack_d;

no za to je potrebno da drugi parametar predloška klase Stack (CONTAINER) bude predložak a ne klasa. Jezik nam dozvoljava da parametar predloška deklariramo kao predložak klase.

template <typename T, template <typename S> class CONTAINER>
class Stack{
    public:
        void push(T const& x){ data.push_back(x); }
        void pop();               // pop element
        const T& top() const;     // vrati const referencu na top element
        T& top();                 // vrati referencu na top element
        bool empty() const { return data.empty(); }
        void clear();
    private:
        CONTAINER<T> data;     // elementi
};

Template template parametar: implementacija

Na primjer, metoda top:

template <typename T, template<typename> class CONTAINER>
T& Stack<T, CONTAINER>::top()
{
    if (data.empty()) throw std::out_of_range("Stack::top: empty stack");
    return data.back();      // vrati const referencu na top element
}

Uočimo da smo ovdje (u template deklaraciji) ispustili ime template parametra klase CONTAINER, što je dozvoljeno budući da se to ime ne koristi.

Napomena: To smo mogli napraviti i u deklaraciji klase, tako da bismo prirodnije pisali:

template <typename T, template <typename> class CONTAINER>
class Stack{
        // ...
};

Template template parametar: dodijeljeni parametar

Htjeli bismo template template parametru dati dodijeljenu vrijednost. Na primjer,

template <typename T, template <typename> class CONTAINER=std::deque>
class Stack{
        // ...
};

no dobili bismo grešku pri kompilaciji. Problem: standardni spremnici imaju jedna dodatni template parametar — alokator, koji ima defaultnu vrijednost.

Ispravna deklaracija:

template <typename T, template <typename S,
                               typename ALLOC = std::allocator<S>
                               >
                               class CONTAINER  = std::deque
        >
class Stack{
   public:
       void push(T const& x){ data.push_back(x); }
       void pop();               // pop element
       const T& top() const;     // vrati const referencu na top element
       T& top();                 // vrati referencu na top element
       bool empty() const { return data.empty(); }
       void clear();
       template <typename U, template <typename V,
                             typename ALLOC = std::allocator<V> > class CONT>
           Stack& operator=(const Stack<U, CONT>&);
   private:
       CONTAINER<T> data;     // elementi
};

Predlošci klasa i nasljeđivanje

Klase generirane iz predložaka mogu se koristiti polimorfno i sve što smo naučili o nasljeđivanju i virtualnim funkcijama jednako se odnosi i na parametrizirane klase. Ipak, kada klasa ima parametriziranu bazu javlja se jedan problem vezan uz način na koji prevodilac traži imena. Sljedeći kod ilustrira problem.

Primjer

template <typename T>
class Base {
    public:
        void bar(){}
};

template <typename T>
class Derived : public Base<T> {
    public:
        void foo() { bar(); }
};

Premda je funkcija bar() javna u baznoj klasi prevodilac je neće naći i javit će grešku.

baza.cpp: In member function ‘void Derived<T>::foo()’:
baza.cpp:10:26: error: there are no arguments to ‘bar’ that depend on a template parameter, so a declaration of ‘bar’ must be available [-fpermissive]
         void foo() { bar(); }
                          ^
baza.cpp:10:26: note: (if you use ‘-fpermissive’, G++ will accept your code, but allowing the use of an undeclared name is deprecated)

Zavisna i nezavisna imena

Imena unutar predloška klase dijele se na ona koja ne ovise o template parametrima i ona koja o njima ovise. Za prva imena kažemo da su nezavisna, dok su druga zavisna (o parametru predloška).

Two phase lookup

  • Imena koja ne ovise o parametrima predloška (nezavisna imena) traže se čim prevodilac na njih naiđe, a to znači u trenutku prolaska kroz definiciju predloška.
  • Imena koja ovise o parametrima predloška (zavisna imena) traže se tek na mjestu instancijacije predloška, jer je tek tu poznat tip parametara te se može odrediti tip imena koje se traži.

Two phase lookup …

  • prevodilac nikad neće tražiti nezavisno ime u parametriziranoj baznoj klasi jer se takva imena moraju naći prije instanciranja klase.

Naime, ako imamo parametriziranu bazu, onda njene varijable i funkcije članice nisu poznate sve do trenutka instanciranja jer do tog trenutka baza može biti potpuno izmijenjena specijalizacijom za pojedine tipove.

Rješenje: nezavisno ime iz bazne klase treba učiniti zavisnim. Na primjer, bar() zamijenimo s Base<T>::bar() ili this->bar().

Primjer, nastavak

Rješenje: dovoljno je učiniti ime bar ovisnim (na implicitan način) o parametru T, što možemo učiniti tako da na njega referiramo kao Base<T>::bar() ili kao this->bar(). Na primjer, sljedeći kod je ispravan:

template <typename T>
class Base {
    public:
        void bar(){}
};

template <typename T>
class Derived : public Base<T> {
    public:
        void foo() { this->bar(); }
};