24.3 Przykłady

Na zakończenie tego krótkiego wstępu do biblioteki standardowej przeanalizujmy jeszcze dwa przykłady ilustrujące jej zastosowania.


Ważną klasą, a właściwie szablonem klas, jest pair z nagłówka utility. Szablon ten opisuje proste, ale często bardzo przydatne struktury składające się z pary składowych pewnych typów, które mogą być różne; mogą to być zarówno typy wbudowane jak i definiowane w programie. Tak więc, jeśli potrzebne nam są często pary (string, int), na przykład do opisu imienia i wieku osób, to właściwym typem byłaby konkretyzacja pair<string,int>. Struktura ta zawiera dwie składowe o nazwach first — w tym przypadku typu string — oraz second, w naszym przypadku typu int. Klasa jest tak zaimplementowana, o co już sami nie musimy się martwić, że prawidłowo działają operatory porównywania (' ==' i ' !='), przypisania itd. — jeśli oczywiście jest tak dla typów składowych.

Taką właśnie klasę tworzymy w poniższym programie. Dane o osobach czytane są z pliku tekstowego pary.dat o następującej zawartości:

    Ania  17 Zosia  7 Ala 19
    Ula   36 Asia  18
    Ola    4
Przy wczytywaniu (linia 46) dane są bezpośrednio kopiowane do wektora — obiektu klasy konkretyzującej wzorzec vector, której elementami są pary typu pair<string,int>. Na wektorze tym dokonujemy następnie kilku operacji ilustrujących algorytmy z biblioteki standardowej i zastosowanie obiektów funkcyjnych.

P195: pary.cpp     Pary

      1.  #include <fstream>
      2.  #include <string>
      3.  #include <utility>   // pair
      4.  #include <vector>
      5.  #include <algorithm> // copy, sort, itd.
      6.  #include <iostream>
      7.  using namespace std;
      8.  
      9.  typedef pair<string,int> PARA;
     10.  typedef vector<PARA>     VECT;
     11.  typedef VECT::iterator   VECTIT;
     12.  
     13.  template <typename P>
     14.  class niepelnoletni {
     15.      int wiek;
     16.  public:
     17.      niepelnoletni(int wiek) : wiek(wiek) { }
     18.  
     19.      bool operator()(const P& p) const {
     20.          return p.second < wiek;
     21.      }
     22.  };
     23.  
     24.  template <typename P1, typename P2>
     25.  ostream& operator<<(ostream& str, const pair<P1,P2>& p) {
     26.      return str << "[" << p.first << "," << p.second << "]";
     27.  }
     28.  
     29.  template <typename P1, typename P2>
     30.  istream& operator>>(istream& str, pair<P1,P2>& p) {
     31.      return str >> p.first >> p.second;
     32.  }
     33.  
     34.  template <typename P>
     35.  bool komp(const P& p1, const P& p2) {
     36.      return p1.second < p2.second;
     37.  }
     38.  
     39.  int main() {
     40.      ifstream plik("pary.dat");
     41.  
     42.      PARA   p;
     43.      VECT vec;
     44.      VECTIT it, fin;
     45.  
     46.      while (plik >> p) vec.push_back(p);
     47.  
     48.      cout << "Po wczytaniu:\n";
     49.      fin=vec.end();
     50.      for (it = vec.begin(); it != fin; ++it)
     51.          cout << *it << " ";
     52.  
     53.      cout << "\nNajstarsza "
     54.           << *max_element(vec.begin(),vec.end(),komp<PARA>)
     55.           << ", najmlodsza "
     56.           << *min_element(vec.begin(),vec.end(),komp<PARA>);
     57.  
     58.      it = remove_if(vec.begin(),fin,
     59.                     niepelnoletni<PARA>(18));
     60.      vec.erase(it,vec.end());
     61.  
     62.      cout << "\nPo usunieciu niepelnoletnich:\n";
     63.      fin=vec.end();
     64.      for (it = vec.begin(); it != fin; ++it)
     65.          cout << *it << " ";
     66.  
     67.      sort(vec.begin(),fin,komp<PARA>);
     68.  
     69.      cout << "\nPo uporzadkowaniu:\n";
     70.      fin=vec.end();
     71.      for (it = vec.begin(); it != fin; ++it)
     72.          cout << *it << " ";
     73.  
     74.      cout << endl;
     75.  }

W liniach 9-11 definiujemy aliasy nazw typów, których będziemy używać w dalszym ciągu. Nazwa PARA jest synonimem nazwy typu powstającego z konkretyzacji szablonu pair, w którym typem składowej first jest string, a typem składowej second jest int. Zatem „prawdziwą” nazwą tego typu jest pair<string,int>. Z kolei VECT (linia 10) jest synonimem nazwy klasy powstającej z konkretyzacji wzorca vector, w której typem elementów wektora jest typ pair<string,int> czyli PARA. Bez użycia typedef musielibyśmy jako nazwy tego typu wektora używać nieporęcznego vector<pair<string,int> >. Zwróćmy uwagę, że odstęp pomiędzy znakami ' >' byłby tu konieczny: bez niego parser zinterpretowałby dwuznak '>>' jako operator wyjmowania ze strumienia.

W linii 43 tworzymy obiekt vec typu VECT. Obiekt ten jest, na razie pustym, wektorem elementów typu PARA. W linii 40 otwieramy strumień wejściowy plik związany z plikiem dyskowym pary.dat (patrz rozdział o operacjach plikowych ). W jaki sposób zachodzi czytanie z pliku obiektów typu PARA? Jest to typ przez nas zdefiniowany, a więc operator '>>' nie wie jak takie obiekty wyjąć ze strumienia. Dlatego musieliśmy dla tego typu przeciążyć operator '>>'. Zrobiliśmy to w liniach 29-32. Zdefiniowany tam szablon zadziała dla par (obiektów klas konkretyzujących wzorzec pair) dowolnych typów, pod warunkiem, że dla typów składowych firstsecond działanie '>>' jest dobrze okeślone. W naszym przypadku typami składowych są stringint, więc nie będzie żadnych kłopotów.

W liniach 49-51 drukujemy zawartość kolekcji vec. Drukowanymi elementami kolekcji są obiekty typu PARA przez nas zdefiniowanego, więc musieliśmy dla niego przeciążyć operator '<<'. Zrobiliśmy to w liniach 24-27, analogicznie do jak dla operatora '>>'.

Linia 54

       *max_element(vec.begin(),vec.end(),komp<PARA>)
demonstruje użycie funkcji (algorytmu) max_element znajdującej największy element kolekcji. Kolekcja jest jak zwykle określona za pomocą iteratorów. Gdybyśmy nie podali trzeciego argumentu, to do porównań elementów zostałby użyty operator ' <'. Elementami kolekcji są obiekty typu PARA, więc aby to zadziałało, w klasie PARA operator ' <' musiałby być przeciążony. To mogliśmy zrobić, ale w tym przypadku zastosowane zostało inne rozwiązanie. Jako trzeci argument podaliśmy funkcję porównującą obiekty (predykat w postaci szablonu —  linie 34-37). Zauważmy, że jako parametru wzorca użyliśmy tu tylko jednego typu (a nie pary typów). Wzorzec zadziała więc dla każdego typu, w którym istnieją i dają się porywnywać za pomocą ' <' składowe o nazwie second. Oczywiście typ PARA spełnia te warunki. Jak widać z definicji, komparator będzie poównywał obiekty typu PARA według wieku, będącego w nich składową second. Funkcja max_element zwraca iterator wskazujący największy obiekt kolekcji.

Na podobnej zasadzie działa funkcja min_element użyta w linii 56, która, jak łatwo się domyślić, znajduje najmniejszy element kolekcji. Prawidłowość działania obu funkcji widzimy z wydruku

    Po wczytaniu:
    [Ania,17] [Zosia,7] [Ala,19] [Ula,36] [Asia,18] [Ola,4]
    Najstarsza [Ula,36], najmlodsza [Ola,4]
    Po usunieciu niepelnoletnich:
    [Ala,19] [Ula,36] [Asia,18]
    Po uporzadkowaniu:
    [Asia,18] [Ala,19] [Ula,36]
Często się zdarza, że z kolekcji chcemy usunąć elementy spełniające pewien warunek. Można to wygodnie zrobić za pomocą algorytmu remove_if użytego w linii 58. Jak zwykle ciąg elementów kolekcji zadany jest przez iteratory będące pierwszymi dwoma argumentami wywołania. Jako trzeci argument podajemy funkcję (wskaźnik do funkcji) albo obiekt funkcyjny, który wywołany z elementem kolekcji jako argumentem zwraca wartość logiczną true, jeśli element ten ma zostać usunięty. W naszym przypadku jest to obiekt klasy konkretyzującej szablon niepelnoletni dla typu PARA. Tworząc w linii 59 obiekt tej klasy posyłamy do konstruktora liczbę, która w obiekcie zapamiętana zostanie jako składowa wiek (patrz definicja szablonu w liniach 13-22). Musimy też przeciążyć w klasie operator wywołania (linie 19-21) tak, aby zwracał true jeśli warunek jest spełniony, czyli gdy wiek osoby jest mniejszy od tego zapamiętanego w składowej wiek obiektu.

Funkcja remove_if, wbrew swej nazwie, nie usuwa niechcianych elementów. Przestawia je tylko tak, aby znalazły się na końcu kolekcji. Tak więc po wywołaniu funkcji liczba elementów kolekcji nie zmienia się. Zmienia się tylko ich kolejność: elementy, dla których warunek nie był prawdziwy znajdują się na początku, a te dla których warunek był spełniony na końcu kolekcji. Funkcja zwraca iterator do pierwszego elementu z tych „niechcianych”, dlatego typem zmiennej it z linii 58 jest VECTIT czyli VECT::iterator. Gdyby takich elementów w ogóle nie było, funkcja zwróciłaby iterator vec.end(). Fizycznego usunięcia elementów, a więc skrócenia kolekcji, dokonujemy za pomocą wywołania funkcji składowej o nazwie erase na rzecz kolekcji (linia 60). Pobiera ona jako argumenty iteratory określające podciąg kolekcji przeznaczony do usunięcia —  w naszym przykładzie jest to podciąg od elementu wskazywanego przez it do końca.

W linii 67 sortujemy pozostałe w wektorze elementy za pomocą wywołania znanego nam już algorytmu sort. Do porównywania elementów używamy takiego samego komparatora jak ten, który został zastosowany w funkcjach max_elementmin_element


Jako drugi przykład rozpatrzmy program ilustrujący użycie map (słowników). Wzorzec klas reprezentujących słownki nazywa się map (z nagłówka map). Aby skonkretyzować ten szablon należy podać dwa typy, Typ1Typ2,

       #include <map>
       // ...
       map<Typ1,Typ2> mapa;
które określają typy tzw. kluczy i wartości. Poszczególne elementy mapy będą parami typu pair<const Typ1,Typ2>. Zwróćmy uwagę, że typ kluczy jest modyfikowany tak, aby odpowiadać typowi ustalonemu (const), a więc klucze są niemodyfikowalne, wartości związane z kluczem mogą natomiast być modyfikowalne. W mapie nie mogą wystąpić dwa elementy z takim samym kluczem. Najbliższym „kuzynem” mapy w Javie jest klasa TreeMap. Ważną różnicą jest jednak to, że w mapach w C++ zarówno kluczami jak i wartościami mogą być obiekty typów wbudowanych, jak int czy double. Pamiętać tylko trzeba, że dla typu kluczy musi być zdefiniowane porównywanie za pomocą operatora ' <' (bo słowniki są w C++ implementowane jako drzewa czerwono-czarne, a nie „prawdziwe” mapy haszowane). Tak oczywiście jest dla typów liczbowych czy dla typu string; jeśli klucze są typu przez nas zdefiniowanego, to musimy poprzez mechanizm przeciążania działanie operatora ' <' zdefiniować (odpowiedni komparator można też przesłać jako trzeci typ dla szablonu map podczas jego konkretyzacji).

Każdy element mapy, będąc obiektem typu pair, ma składowe firstsecond odpowiadające odpowiednio kluczowi i związanej z tym kluczem wartości. Operator indeksowania (' []') jest w mapach przeciążony w ten sposób, że wyrażenie ' mapa[key]' jest referencją do wartości związanej z kluczem key. Jeśli w mapie taki klucz nie występuje, to błędu nie będzie: nowy element mapy zostanie automatycznie utworzony, a jako wartość wstawiona zostanie wartość domyślna odpowiedniego typu (wartość zerowa dla typów liczbowych). Na przykład we fragmencie kodu

      1.      map<string,int> slownik;
      2.      slownik["Jan"] = 20;
      3.      slownik["Ira"] = ++slownik["Ula"] + 18;
zostanie w linii 2 dodany do początkowo pustej mapy slownik element o kluczu "Jan" i wartości 20. W linii 3 najpierw tworzony jest element [ "Ula",0], a następnie jego wartość (czyli zero) jest inkrementowana. Zatem po opracowaniu wyrażenia ' ++slownik["Ula"]' element z kluczem "Ula" ma wartość 1. Wartością całej prawej strony przypisania jest zaś 19.

Opracowanie lewej strony przypisania z linii 3 spowoduje utworzenie elementu [ "Ira", 0] i następnie przypisanie do wartości skojarzonej z kluczem "Ira" wartości prawej strony przypisania, czyli liczby 19. Po wykonaniu tego fragmentu elementami slownika są zatem pary [ "Jan",20], [ "Ula",1] i [ "Ira",19].


Rozpatrzmy jako przykład następujący program:


P196: mapy.cpp     Mapy (słowniki)

      1.  #include <iostream>
      2.  #include <iomanip>   // left, setw
      3.  #include <string>
      4.  #include <map>
      5.  #include <utility>   // pair
      6.  #include <algorithm>
      7.  using namespace std;
      8.  
      9.  typedef pair<string,int> Emp;
     10.  typedef map<string,Emp>  MAP;
     11.  
     12.  class Zakr {
     13.      int min,max;
     14.  public:
     15.      Zakr(int min,int max) : min(min), max(max) {}
     16.  
     17.      bool operator()(const pair<const string,Emp>& p) const {
     18.          int zarobki = p.second.second;
     19.          return  (min < zarobki) && (zarobki < max);
     20.      }
     21.  };
     22.  
     23.  void druk(const MAP& m) {
     24.      MAP::const_iterator it, fin=m.end();
     25.  
     26.      for (it = m.begin(); it != fin; it++)
     27.          cout << "Klucz: "   << left << setw(7) << it->first
     28.               << "Imie: "    << setw(10) << it->second.first
     29.               << "Zarobki: " << it->second.second << endl;
     30.  }
     31.  
     32.  int main() {
     33.      MAP emp;
     34.  
     35.      emp["jan"]    = Emp("Jan K.",    1900);
     36.      emp["piotr"]  = Emp("Piotr M.",  2100);
     37.      emp["ola"]    = Emp("Ola S.",    3100);
     38.      emp["prezes"] = Emp("Prezes",    9900);
     39.      emp["adam"]   = Emp("Adam A.",   1600);
     40.      emp["emilia"] = Emp("Emilia P.", 2600);
     41.  
     42.      druk(emp);
     43.  
     44.      int mn = 0, mx = 2000;
     45.      int ile = count_if(emp.begin(),emp.end(),Zakr(mn,mx));
     46.  
     47.      cout << ile << " osob ma zarobki w zakresie od "
     48.           << mn << " do " << mx << endl;
     49.  }

W liniach 9 i 10 definiujemy alias Emp dla typu pair<string,int> oraz alias MAP dla typu słownikowego map<string,Emp>. Będzie to słownik złożony z elementów (par) w których klucz jest typu const string, a wartość jest parą złożoną z napisu i liczby. W linii 33 tworzymy pusty obiekt (słownik) typu MAP, a w liniach 35-40 dodajemy do tego slownika kilka elementów. Na przykład w linii 35 tworzony jest nowy element słownika o kluczu "jan" i wartości będącej obiektem typu Emp (a więc parą typu pair<string,int>) ze składową first "Jan K." i składową second równą 1900.

Tak utworzony słownik przesyłamy do funkcji druk, która drukuje kolejne elementy słownika. Użyta tam zmienna it jest iteratorem wskazującym na pojedynczy element słownika, czyli obiekt typu pair<const string, pair<string,int> >. Zatem ' it->first' to klucz (np.  "jan"), a ' it->second' to para złożona z napisu i liczby. Jeśli więc chcemy wypisać zarobki osoby odpowiadającej elementowi słownika, musimy użyć składni ' it->second.second'. Pierwszy wybór składowej odbywa się przez operator „strzałki”, bo iterator ma semantykę wskaźnika, ale drugi wybór już nie: wyrażenie ' it->second' jest referencją do obiektu typu Emp, a nie wskaźnikiem, więc używamy tu kropki.

    Klucz: adam   Imie: Adam A.   Zarobki: 1600
    Klucz: emilia Imie: Emilia P. Zarobki: 2600
    Klucz: jan    Imie: Jan K.    Zarobki: 1900
    Klucz: ola    Imie: Ola S.    Zarobki: 3100
    Klucz: piotr  Imie: Piotr M.  Zarobki: 2100
    Klucz: prezes Imie: Prezes    Zarobki: 9900
    2 osob ma zarobki w zakresie od 0 do 2000
W linii 45 tworzymy wywoływalny obiekt typu Zakr przekazując do konstruktora dwie liczby określające pewien zakres. Jest to obiekt funkcyjny który może być wywołany z elementem słownika jako argumentem, a zatem obiekt ten może pełnić rolę predykatu w wywołaniu funcji count_if z linii 45:
       count_if(emp.begin(),emp.end(),Zakr(mn,mx));
Algorytm count_if zlicza liczbę tych elementów kolekcji określonej dwoma iteratorami, dla których predykat będący trzecim argumentem wywołania zwraca true. W naszym przypadku, dla każdego elementu kolekcji (słownika) emp wywołana będzie metoda operator(), która zwróci true, jeśli dla danego elementu słownika składowa określająca zarobki mieści się w zakresie definiowanym przez składowe minmax obiektu funkcyjnego Zakr(mn,mx).

Zauważmy, że typ MAP zdefiniowany został jako map<string,Emp>, a to oznacza, jak już podkreślaliśmy, że typem elementów tej kolekcji nie będzie wcale typ pair<string,Emp>, tylko typ pair<const string,Emp>. Dlatego właśnie taki jest typ parametru metody operator().


Biblioteka standardowa dostarcza oczywiście dużo więcej narzędzi niż te, o których tu wspomnieliśmy. Pełny ich opis można znaleźć w książkach cytowanych w spisie literatury .

T.R. Werner, 25 lutego 2017; 22:31