Dziedziczenie i polimorfizm

Dziedziczenie

Klasy podstawowe i pochodne

Dziedziczenie to jeden z najważniejszych mechanizmów programowania obiektowego. Polega on na ponownym wykorzystaniu kodu w taki sposób, że nowe klasy tworzone są na podstawie już istniejących dziedzicząc jej metody i pola i jednocześnie dodając nowe metody i nowe pola.

Dziedziczenie

Aby zapisać relację dziedziczenia w C++ stosuje się następujący schemat:

class NazwaKlasyPochodnej : Tryb dziedziczenia NazwaKlasyBazowej { definicja klasy pochodnej }

Chcąc zapisać W C++ relację z rysunku, moglibyśmy to zrobić w następujący sposób:

class CzlonekUczelni{...};
class Student: public CzlonekUczelni{...};
class Pracownik: public CzlonekUczelni{...};
class PracownikNaukowy : public Pracownik{...}
class PradownikAdministracyjny: public Pracownik{...};
//Dziedziczenie wielokrotne
class Stażysta : public Student, PracownikNaukowy {...};

Dziedziczenie składowych

Dostęp do składowych klasy bazowej może być ograniczony modyfikatorami dostępu zastosowanymi w klasie bazowej jak również typem dziedziczenia.

Poniższa tabela przedstawia zależności pomiędzy trybem dziedziczenia a tym jak zmienia sie znaczenie modyfikatorów dostępu w klasie pochodnej.

Modyfikator dostępu w klasie bazowejRodzaj dziedziczenia
publicprotectedprivate
publicpublic w klasie pochodnej.protected w klasie pochodnejprivate w klasie pochodnej
protectedprotected w klasie pochodnej protected w klasie pochodnejprivate w klasie pochodnej
privateUkryte w klasie pochodnejUkryte w klasie pochodnejUkryte w klasie pochodnej

Konstruktory i destruktory

Klasa pochodna tak naprawdę jest jednocześnie klasą bazową, dlatego nie można utworzyć jej obiektu bez wcześniejszego utworzenia obiektu klasy bazowej. Każdy konstruktor klasy pochodnej w pierwszej kolejności będzie wywoływał konstruktor klasy bazowej.

Dziedziczenie

Konstruktora klasy bazowej nie można wywołać w ciele konstruktora klasy pochodnej, ponieważ klasy pochodne dziedziczą pola klasy podstawowej, a wszystkie pola klasy muszą zostać utworzone przed utworzeniem danej klasy.

Chcąc wywołać konstruktor klasy Punkt2D przez konstruktor klasy pochodnej Punkt3D stosujemy następujący zapis:

#include "Punkt2D.h"
#include "Punkt3D.h"
 
Punkt3D::Punkt3D(double x, doubley, double _z) : Punkt2D(x,y) {
z = _z;
}

Metody składowe

Ponieważ każdy obiekt klasy pochodnej jest jednocześnie obiektem klasy bazowej nie ma żadnych różnic pomiędzy używaniem metod klasy bazowej i pochodnej w klasie pochodnej, czy tez na obiekcie klasy pochodnej. Należy jednak pamiętać o trybach dziedziczenia i ograniczeń jakie z tego wynikają.

W przypadku gdy w klasie pochodnej zdefiniowana zostanie ponownie taka sama metoda jak w klasie bazowej, nastąpi tak zwane napisanie tej metody. Wywołując tą metodę na obiekcie klasy pochodnej będzie wywoływana metoda nowo zdefiniowana.

Jeśli klasa bazowa ma przeciążony operator=, nie zostanie on odziedziczony przez klasę pochodną! (pozostałe zostaną odziedziczone)

#include <iostream>
using namespace std;
 
class LiczbaRzeczywista{
  protected:
    double re;
  public:
    LiczbaRzeczywista(double r){re=r;}
    void wypisz(){
      cout << re << endl;
    }
    void powitaj(){
      cout << "Czesc!" << endl;
    }
 
    LiczbaRzeczywista operator+(const LiczbaRzeczywista& r){
      LiczbaRzeczywista rr(re+r.re);
      return rr;
    }
 
};
 
class LiczbaZespolona : public LiczbaRzeczywista{
  protected:
    double im;
  public:
    LiczbaZespolona(double re, double i):LiczbaRzeczywista(re){im=i;}
    void wypisz(){
      cout << re << " + " << im << "i" << endl;
    }
};
 
int main(){
  LiczbaRzeczywista a(12);
  LiczbaZespolona b(23,5);
 
  a.powitaj();         //Czesc!
  a.wypisz();          //12
  (a+a).wypisz();      //24
 
  b.powitaj();         //Czesc!
  b.wypisz();          //23 + 5I
 
 
  (b+b).wypisz();      //Co się wypisze?
}            

Polimorfizm

Polimorfizm to możliwość różnej odpowiedzi na ten sam komunikat (wywołanie tej samej metody) przez obiekty różnych klas powiązanych dziedziczeniem. Poniższe podsekcje składają się na opis tego mechanizmu.

Funkcje wirtualne

Podczas dziedziczenia, klasy pochodne mogą nadpisywać metody swoich klas bazowych. Kiedy jednak dokonujemy rzutowania w górę (np. z LiczbaUrojona na LiczbaRzeczywista), nadpisane metody wracają z powrotem do swoich pierwotnych postaci (np. obliczanie pola przekroju powierzchni kuli za pomocą rzutowania na koło). Takie zachowanie nie zawsze jest pożądane.

Załóżmy, że chcemy zbudować prosty program obsługujący bazę pracowników pewnej firmy. Zakładamy, że firma zatrudnia pracowników na umowę o dzieło, lub na umowę o pracę; wpływa to na obliczenie pensji netto każdego pracownika.

Mamy zatem taką relację:

Umowa - diagram UML

Taka relacja umożliwia stworzenie tylko jednej klasy Pracownik, która będzie miała jedno pole Umowa, któremu z kolei będzie przypisywana albo UmowaDzielo albo UmowaPraca. W takim wypadku po rzutowaniu UmowaDzielo lub UmowaPraca na typ bazowy Umowa, musi być możliwe wywołanie metod odpowiednio obliczających wynagrodzenie netto. W tym celu stosowane są funkcje wirtualne.

Funkcja wirtualna, to taka metoda klasy, która nadpisana w klasie pochodnej, nawet po rzutowaniu obiektu na typ bazowy zachowa swoją implementacje. Klasa bazowa z funkcją wirtualną będzie polimorficzna - w zależności od tego jaki obiekt klasy pochodnej był na nią rzutowany, taka implementacja metody zostanie uruchomiona.

Przykład:

#include <iostream>
#include <list>
#include <memory>
 
using namespace std;
 
class Umowa{
  public:
    Umowa(double pensja):wynagrodzenie_brutto_(pensja){};
    virtual double PobierzNetto() const;
    double PobierzBrutto() const;
  protected:
    double wynagrodzenie_brutto_;
};
 
class UmowaDzielo: public Umowa{
  public:
    UmowaDzielo(double pensja):Umowa{pensja} {};
    virtual double PobierzNetto() const override;
};
 
class UmowaPraca: public Umowa {
  public:
    UmowaPraca(double pensja):Umowa{pensja} {};
    virtual double PobierzNetto() const override;
};
 
class Pracownik{
  public:
    Pracownik(string imie,string nazwisko,string pesel,std::unique_ptr<Umowa> umowa)
      :imie_{imie},nazwisko_{nazwisko},pesel_{pesel},umowa_{move(umowa)}{};
    double PobierzPensje() const;
    friend std::ostream& operator<<(std::ostream&,Pracownik&);
  private:
    std::string imie_,nazwisko_,pesel_;
    std::unique_ptr<Umowa> umowa_;
};

Stary dobry C

Przypomnijmy sobie zadanie z wcześniejszych laboratoriów z utworzeniem tablicy jednowymiarowej. Wtedy algorytm składał się z dwóch kroków alokacja pamięci i inicjalizacja wartości tablicy. Ale co powinniśmy zrobić, gdyby była potrzeba zdefiniowania kilku różnych sposobów wypełniania tablicy w zależności od tego co zażyczy sobie użytkownik (a więc nie możemy tego przewidzieć w trakcie pisania programu) :?:

Kod mógłby wyglądać następująco:

int *CreateArray(size_t size, int version_of_filling) {
   int *tab = NewArray(size);
   FillArray(tab, size, version_of_filling);
}
 
void FillArray(int *tab, size_t size, int version_of_filling) {
   switch(version_of_filling) {
     case 0: {//INIT TO 0s
        for (int i=0; i<size; i++) {
           tab[i] = 0;
        }
        break;
     }
     case 1: {//INIT TO NUMBER?
        for (int i=0; i<size; i++) {
           tab[i] = NUMBER; //skąd ją wziąć? dołożyć nowy parametr do obydwu funkcji?
        }
        break;
     }
     //tutaj trzeba będzie pamiętać o dołożeniu kolejnego algorytmu jak pojawi się kolejne rozszerzenie
   }
}

Ten kod ma jednak sporo wad przy większej ilości i komplikacji algorytmów metoda Fill się niebezpiecznie rozrasta. Należy pamiętać, że jak pojawi się nowe wymaganie to należy dodać nowy warunek do switcha, itd…

Czy da się to zrobić jednak inaczej? Tak z wykorzystaniem wskaźników do funkcji :!:

int *CreateArray(size_t size, int (*filler)(int)) {
   int *tab = NewArray(size);
   FillArray(tab, size, version_of_filling);
}
 
void FillArray(int *tab, size_t size, int (*filler)(int)) {
    for (int i=0; i<size; i++) {
       tab[i] = filler(i);
    }
}

Kod FillArray znacznie się uprościł, ale żeby pozostawić poprzednią funkcjonalność musimy jeszcze dopisać brakujące metody. Każda metoda, która spełnia zadany interfejs się nada (tzn. metoda musi zwracać int jako wartość pola tablicy i przyjmować int jako index ustawianego pola):

int UniformFillWithZero(int index) {
  return 0;
}
 
int UniformFillWith77(int index) {
  return 77;
}
 
int IncrementalFill(int index) {
  return 8 * index + 14;
}

I przykładowe wywołanie kodu:

int * tab = CreateArray(1024, IncrementalFill);

Znacznie lepiej, ale wciąż musimy zadeklarować mnóstwo funkcji jeśli byśmy chcieli chociażby wypełnić tablicę identycznymi liczbami, ale każdą kolejną tablicę inną wartością. Nasze funkcje są pozbawione niestety kontekstu :!:

Jednak kontekst można dołożyć do każdej z tych funkcji w postaci pomocniczej struktury danych, która dodatkowo będzie przechowywać kontekst dla funkcji:

struct Filler {
  int (*fill)(Filler *, int);
};
 
void FillArray(int *tab, size_t size, Filler *filler) {
    for (int i=0; i<size; i++) {
       tab[i] = filler->fill(filler, i);
    }
}

I wypełniacze:

//sepcyficzna struktura kontekstu pasująca do jednorodnego wypełniacza
struct UniformFiller {
  int (*fill)(UniformFiller *,int);
  int value;
};
 
//context to self z Pythona!
int UniformFillerMethod(UniformFiller *context, int index) {
  return context->value;
}
 
//inicjalizacja naszej struktury danych ustawienie wskaźnika do odpowiedniej funkcji
//Python i C++ robią to automatycznie
UniformFiller f;
f.fill = UniformFillerMethod; 
f.value = 77;
 
//wywołanie utworzenia tablicy
int *tab = CreateTable(1024,(Filler*)f);

Wracając do C++

Zdefiniowanie w klasie metody jako wirtualnej jest równoważne do zdefiniowania wskaźnika do funkcji. Klasa pochodna ustawia tylko wskaźnik na swoją wersję metody. Jeśli definiujemy metodę wirtualną jako abstrakcyjną jest to równoważne z nieustawieniem żadnej metody dla wskaźnika funkcji. Context jest automatycznie przesyłany do metody jako wskaźnik this.

A teraz kod (odpowiednik struktury Filler z C i metodą Value odpowiednikiem wskaźnika do funkcji fill):

class ArrayFill {
 public:
  virtual int Value(int index) const =0;
};

Odpowiednik FillArray:

void FillArray(size_t size, const ArrayFill &filler, std::vector<int> *v) {
  v->clear();
  v->reserve(size);
  for (size_t i = 0; i < size; i++) {
    v->emplace_back(filler.Value(i));
  }
}

I wreszcie odpowiednik UniformFiller (C++ sam ustawi nasz wskaźnik na odpowiednią metodę, do tego jak się pomylimy kompilator jest w stanie wychwycić czy w ogóle przesłaniamy odpowiednią metodę i czy ona instnieje:

class UniformFill : public ArrayFill {
 public:
  UniformFill(int value = 0) : value_{value} {}
  virtual int Value(int index) const override;
 private:
  int value_;
};
 
 
int UniformFill::Value(int index) const {
  return value_;
}

I wreszcie wywołanie:

std::vector<int> vs;
FillArray(1024, UniformFill {77}, &vs);

Destruktory wirtualne

Zwróć uwagę, że podobnie do metod zachowują się destruktory. Jeśli obiekt jest jawnie niszczony prze operator delete to wywoływany jest jego destruktor:

#include <iostream>
...
int main(){
  Umowa* umowa = new UmowaPraca(10000);
 
  // zostanie wywołany destruktor klasy Umowa, a nie UmowaPraca!
  // Jeśli UmowaPraca alokowałaby dodatkowo jakąś pamięć
  // NIE ZOSTAŁABY ONA ZWOLNIONA
  delete umowa;
}

Rozwiązaniem jest deklarowanie destruktora jako wirtualnego:

class Umowa{
  ...
  virtual ~Umowa();
};
 
class UmowaPraca :public Umowa{
  ...
  virtual ~UmowaPraca();
};
 
int main(){
  Umowa* umowa = new UmowaPraca(10000);
 
  // zostanie wywołany destruktor klasy UmowaPraca
  delete umowa;
}

Klasy abstrakcyjne

Czasem w klasie bazowej implementowanie jakiejś metody jest bezcelowe. W naszym przykładzie z pracownikami implementowanie metody pobierzNetto w klasie Umowa nie ma sensu, bo nie wiadomo jaki jest to typ umowy.

Metody takie można zadeklarować jako czysto wirtualne:

class UmowaDzielo: public Umowa{
  protected:
    double wynagrodzenieBrutto;
  public:
    Umowa(double pensja):wynagrodzenieBrutto(pensja){};
    // Metoda czysta. Nie posiada implementacji w klasie bazowej.
    // MUSI jednak być zaimplementowana w pochodnej (lub ponownie 
    // zadeklarowana jako czysta
    virtual double PobierzNetto() = 0;
};

Klasę zawierającą choć jedną metodę czystą nazywamy klasą abstrakcyjną. Nie jest możliwe utworzenie obiektu takiej klasy! Służy ona jedynie jako klasa bazowa dla innych klas. Swego rodzaju interfejs.

Rzutowanie

Rzutowanie to inaczej konwersja pomiędzy typami. Wyróżnia się dwa rodzaje rzutowania:

  • rzutowanie w górę (z klasy pochodnej na klasę bazową)
  • rzutowanie w dół (Z klasy bazowej na klasę pochodną)

Rzutowanie w górę

Rzutowanie w górę można wykonywać niejawnie, ponieważ każdy obiekt klasy pochodnej jest jednocześnie obiektem klasy bazowej:

void foore(LiczbaRZeczywista r){
  cout << "Wyswietlamrzeczywista ";
  r.wypisz();
}
 
int main(){
  LiczbaZespolona z(12,4);
 
  foore(z);  //nastąpi rzutowanie w górę
 
}

Rzutowanie w dół

Rzutowanie w dół oznacza rzutowanie z klasy bazowej na pochodną. Jeśli nie zostanie przeciążony odpowiedni operator rzutowania, operację taką należy wykonywać przy użyciu jednego z poniższych operatorów:

OperatorOpis
static_cast<nowy_typ>(wyrazenie)Sprawdzanie poprawności typów podczas rzutowania wykonywane jest podczas kompilacji
const_cast<nowy_typ>(wyrazenie)Może być stosowany tylko ze wskaźnikami lub referencjami. Pozwala na anulowanie stałości wskaźnika obiektu (const).
dynamic_cast<nowy_typ>(wyrazenie)Może być stosowany tylko ze wskaźnikami lub referencjami.Pozwala na rzutowanie w górę (dla wszystkich klas) i na rzutowanie w dół (tylko dla klas polimorficznych), sprawdzając czy obiekt który rzutujemy jest faktycznie kompletnym obiektem klasy na którą chcemy go rzutować. Operator przeprowadza ten test podczas działania programu; Jeśli konwersja nie może się odbyć, zwracany jest wskaźnik null ( w przypadku referencji rzucany jest wyjątek bad_cast)
reinterpretc_cast<nowy_typ>(wyrazenie)Stosowanie tego operatora jest potencjalnie bardzo niebezpieczne. Może on rzutować dowolny typ na dowolny inny typ, nawet jeśli oba typy nie mają ze sobą nic wspólnego.

Poniżej przedstawiono przykłady użycia operatorów i różnice w ich stosowaniu:

class A {
  public:
    virtual void foo() = 0;
};
 
class B: public A{
  public:
    virtual void foo(){}
};
 
class C {
  public:
    void bar(){};
};
 
class D: public C{
  public:
    void otherbar(){}
};
 
int main(){
  // static_cast - zwykłe rzutowanie w górę
  D* ptrD = new D();
  C* ptrC = static_cast<C*>(ptrD);
 
  // const_cast - pozbywamy się modyfikatora const
  const C* constPtrC = new C();
  C* ptrCC = const_cast<C*>(constPtrC);
 
  // dynamic_cast - rzutowanie w dół
  A* ptrA = new B();
  B* ptrB = dynamic_cast<B*>(ptrA);
 
  // reinterpret_cast - rzutowanie pomiedzy kompletnie niepowiazanymi klasami
  B* ptrBB = new B();
  D* ptrDD = reinterpret_cast<D*>(ptrBB);
}

Ćwiczenia

UWAGA
Przesyłając rozwiązania zadań należy zamieścić (w formie komentarza w kodzie) odpowiedzi na problemy postawione w zadaniach 5-7 wraz z uzasadnieniami. Zadanie bez właściwego uzasadnienia nie będzie zaliczone.

  1. [2 plusy] Zdefiniować metody wypełniania tablicy std::vector<int>:
    • jednorone (zawsze ta sama wartość), z wartością domyślną 0 konstruktor klasy:
      UniformFill(int value = 0)
    • z inkrementacją (uwzględniająca wartość początkową start i krok step, który ma wartość domyślną 1)
      IncrementalFill(int start, int step = 1)
    • za pomocą generatora liczb losowych
      RandomFill(std::unique_ptr<std::default_random_engine> generator, std::unique_ptr<std::uniform_int_distribution<int>> distribution)
    • z kwadratem indeksu (a*index^2+b), zarówno a i b mogą przyjąć domyślne wartości
      SquaredFill(int a = 1, int b = 0)
  2. [3 plusy] Przygotować klasę abstrakcyjną StudentComparator z abstrakcyjną metodą
    bool IsLess(const Student &left, const Student &right)

    i zdefiniowanym operatorem wywołania funkcji, delegującym zachowanie do abstrakcyjnej meteody. Następnie zdefiniować różne implementacje porównywania studentów:

    • ByFirstNameAscending
    • ByFirstNameDescending
    • ByLastNameAscending
    • ByProgramAscendingEmptyFirst, porównuje kierunki studiów alfabetycznie ale przesuwa na początek nieustawiony program (string pusty)
    • ByProgramAscendingEmptyLast, j.w. ale przesuwa na koniec nie ustawiony program studiów
    • [1 plus] w programie main wykorzystać wybrany porównywacz do posortowania wektora studentów (algorytm sort z biblioteki algorithm)
  3. [2 plusy] Zdefiniować klasę abstrakcyjną Query z pojedynczą abstrakcyjną metodą
    bool Accept(const Student &student);

    . Klasa query reprezentuje ogólne zapytanie do repozytorium stuentów. Klasa repozytorium powinna udostępniać nową metodę

    std::vector<Student> FindByQuery(const Query &query)

    która przegląda wszystkich zgromadzonych studentów i każdego po kolei przekazuje do zaakceptowania, jeśli student został zaakceptowany powinien znaleźć się w wynikowym wektorze. Zdefiniować następnie implementacje zapytań:

    • [2 plusy]
      • ByFirstName
      • ByLastName
      • ByOneOfPrograms
      • ByYearLowerOrEqualTo
    • [2 plusy]
      • OrQuery
      • AndQuery
        AndQuery(std::unique_ptr<Query> left, std::unique_ptr<Query> right)
  4. [1 plus] Przetestuj przykład z sekcji Metody składowe
  5. [3 plusy] Napisz dwie klasy: Kolo i Kula. Klasa Kolo powinna być klasą bazową dla klasy Kula.
    • W klasie Kolo powinny znaleźć się następujące pola i metody:
      • double x, y, r - określające odpowiednio współrzędne środka koła i jego promień.
      • double pole() - obliczająca pole koła
    • W klasie Kula powinny znaleźć się następujące pola i metody:
      • double z - określajaca współrzędną przestrzenną środka kuli
      • double pole() - obliczająca pole powierzchni kuli
    • W jaki sposób mając następującą funkcję main można obliczyć pole przekroju zdefiniowanej tam kuli płaszczyzną przechodzącą przez środek kuli, wywołując wyłącznie jedną metodę?
      #include "Kula.h"
      #include "Kolo.h"
      #include <iostream>
       
      using namespace std;
       
      int main(){
        Kula k(0,0,0,10);
      }
  6. [2 plusy] Dopisz brakujące metody z przykładu Funkcje wirtualne i uruchom program.

Zadanie domowe:

Iteratory to uogólnienie wskaźników wykorzystywanych do iteratowania po tablicy. W c żeby przeiterować po tablicy i tylko odczytać jej elementy można było wykonać następujący kod:

  int tab[128];
  int *p;
  int *tab_end = &tab[128];
  int value;
  InitTab(tab);
  for (p = &tab[0]; p != tab_end; ++p) {
     value = *p;
  }

Wraz z c++ przyszły klasy i przeciążanie operatorów i w tym języku da się uogólnić ten schemat na dowolny obiekt, który posiada odpowiednioo zdefiniowane metody:

  array<int,128> tab;
  array<int,128>::iterator p;
  array<int,128>::iterator tab_end = tab.end();
  int value;
  InitTab(tab);
  for (p = tab.begin(); p != tab_end; ++p) {
     value = *p;
  }

Ze względu na to, że kompilator udziela niezbyt jasnych informacji o błędach i o tym co jest niezbędne do zaimplementowania, spróbujemy wykorzystać dziedziczenie. W tym celu zdefiniujemy klasę bazową która mogłaby być wykorzystywana przez dowolną implementację iteratora:

  1. [1 punkt] (lab7_iterable_tests) przygotować klasę bazową IterableIterator udostępniającą następujący zestaw metod bez zdefiniowanego zachowania (abstrakcyjnych, czysto wirtualnych), wszystkie te metody mają być przesłanialne przez klasy pochodne, zastanowić się które z tych metod nie powinny modyfikować this:
    • std::pair<int, std::string> Dereference()

      - odpowiada operacji cos = *it, czyli jej implementacja w klasach pochodnych powinna zwracać wartość pokazywaną przez iterator w akutalnym stanie. Nie umożliwiamy edycji tego stanu, stąd wartość zwracana jest przez kopię, a nie referencję.

    • IterableIterator &Next()

      - odpowiada operacji ++it jej implementacja w klasie pochodnej powinna przesuwać wskaźnik na następną wartość w sekwencji, jeśli nie ma następnej wartości powinna przesuwać wskaźnik tuż za koniec

    • bool NotEquals(const IterableIterator &other)

      - odpowiada operacji it != other, jej implementacja w klasie pochodnej powinna zwracać prawdę jeśli iterator other wskazuje na ten sam element sekwencji (względnem jego indeksu w sekwencji, a nie tylko jego wartości zwracanej przez Dereference()).

    • Destruktor - jako jedyna metoda w tej klasie powinien mieć domyślną implementację, najlepiej skorzystać z pomocy kompilatora i kazać mu ją wygenerować (= defualt).
  2. rozgrzewka Należy zaimplementować klasę pochodną dla IterableIterator, a mianowicie: ZipperIterator, zipper iterator powinien być w stanie przeiterować po dwóch wektorach na raz, biorąc po kolei pierwszy element z pierwszego wektora i pierwszy element z drugiego wektora, drugi element z pierwszego i drugi element z drugiego, itd.. Najłatwiej to zrobić jeśli ZipperIterator będzie posiadał po dwa const iteratory do początu i końca obu wektorów. Wtedy Derefencja, Next i NotEquals są bardzo proste w implementacji. (Testy wymagają konstuktora postaci:
    explicit ZipperIterator(std::vector<int>::const_iterator left_begin,
                     std::vector<std::string>::const_iterator right_begin,
                     std::vector<int>::const_iterator left_end,
                     std::vector<std::string>::const_iterator right_end);
  3. [1 punkt] przygotować
    1. klasę IterableIteratorWrapper opakowującą dowloną podklasę IterableIterator i udostępniającą operatory i fukncje potrzebne by klasa mogła być używana jako iterator c++ między innymi w rage for, czyli (zastanowić się, które z tych funkcji muszą być typu const):
      • konstruktor:
        IterableIteratorWrapper(std::unique_ptr<IterableIterator> iterator)
      • bool operator!=(const IterableIteratorWrapper &other)

        - powinna wywoływać NotEquals z przekazanych iteratorach (pole składowe i argument fukncji)

      • std::pair<int, std::string> operator*()

        - powinna wywoływać Dereference na polu składowym

      • IterableIteratorWrapper &operator++()

        - powinna wywoływać Next na polu składowym

    2. przygotować klasę bazową Iterable o następujących metodach (zastanowić się, które z tych funkcji muszą być typu const):
      • std::unique_ptr<IterableIterator> ConstBegin()

        - czysto abstrakcyjna metoda (bez domyślnej implementacji) przesłanialna w klasach pochodnych ma za zadanie zwracanie odpowiedniego iteratora do początku sekwencji dla właściwej klasy.

      • std::unique_ptr<IterableIterator> ConstEnd()

        - czysto abstrakcyjna metoda (bez domyślnej implementacji) przesłanialna w klasach pochodnych ma za zadanie zwracanie odpowiedniego iteratora za końcem sekwencji dla właściwej klasy.

      • IterableIteratorWrapper cbegin() const

        - konkretna metoda (nie wirtualna), która wywołuje ConstBegin i tworzy odpowiedni typ do zwrócenia.

      • IterableIteratorWrapper cend() const

        - konkretna metoda (nie wirtualna), która wywołuje ConstEnd i tworzy odpowiedni typ do zwrócenia.

      • IterableIteratorWrapper begin() const

        - konkretna metoda (nie wirtualna), która wywołuje cbegin()

      • IterableIteratorWrapper end() const

        - konkretna metoda (nie wirtualna), która wywołuje cend()

  4. [3 punkty] (lab7_iterable_zipper_tests, lab7_iterable_product_tests, lab7_iterable_enumerate_tests)przygotować klasy Zipper, Product, Enumerate, które implementują klasę Iterable i klasy ProductIterator i EnumerateIterator, które implementują klasę IterableIterator.
    • klasa zipper ma za zadanie utworzenie możliwości przeglądania dwóch wektorów jednocześnie w pętli typu range for.
    • klasa enumerate ma za zadanie utworzenie możliwości przeglądania jednego wektora w pętli typu range for, ale tak, że każda wartość wskazywana przez jej iterator zwraca parę indeks tego elementu, wartość tego elementu.
    • klasa product ma za zadanie utworzenie możliwości przeglądania dwóch wektorów jednocześnie w pętli typu range for, ale w przeciwieństwie do zipper, ma tworzyć iloczy kartezjański wszystkich par
        const vector<int> vi {4, 77, -91};
        const vector<string> vs {"4", "9991", "adfskld"};
       
        for (const auto &p : Zipper(vi, vs)) {
          cout << "(" << p.first << ", \"" << p.second << "\") ";
        }
        //wypisze: (4, "4") (77, "9991") (-91,"adfskld")
       
        for (const auto &p : Enumerate(vs)) {
          cout << "(" << p.first << ", \"" << p.second << "\") ";
        }
        //wypisze: (0, "4") (1, "9991") (2,"adfskld")
       
        for (const auto &p : Product(vi,vs)) {
          cout << "(" << p.first << ", \"" << p.second << "\") ";
        }
        //wypisze: (4, "4") (4,"9991") (4, "adfskld") (77, "4") (77,"9991") (77, "adfskld") (-91,"4") (-91,"9991") (-91, "adfskld")
pl/dydaktyka/jimp2/2017/labs/dziedziczenie.txt · ostatnio zmienione: 2019/06/27 15:50 (edycja zewnętrzna)
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0