Podrozdziały


15.1 Metody stałe

Metody klasy mogą być zdeklarowane jako metody stałe. Oznacza to „obietnicę”, że dana metoda nie zmienia stanu obiektu, na rzecz którego została wywołana, czyli nie zmienia żadnej jego składowej. Kompilator może wtedy wygenerować bardziej efektywny kod; sprawdzi on oczywiście, czy rzeczywiście obiekt nie jest zmieniany. Deklarujemy metodę jako stałą umieszczając słowo kluczowe const tuż za nawiasem zamykającym listę parametrów, a przed

Oczywiście, deklarujemy tę stałość tylko raz, jeśli metodę definiujemy bezpośrednio wewnątrz klasy. Jeśli natomiast, jak to zwykle czynimy, w klasie tylko metodę deklarujemy, a definicję podajemy poza klasą, to const trzeba podać w obu miejscach. Pamiętać bowiem trzeba, że dwie metody — nawet o tej samej sygnaturze — z których jedna jest stała, a druga nie są różnych typów! Zatem

deklaracja stałości musi wystąpić zarówno w definicji, jak i w deklaracji metody.

Zauważmy też, że metoda stała nie może zmienić składowych obiektu, na rzecz którego została wywołana, ale może zmienić składowe innych obiektów tej samej klasy, do których ma dostęp.

Rozpatrzmy przykład:


P109: constmet.cpp     Metody stałe

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  class Point {
      5.      double x, y;
      6.  public:
      7.      Point(double x, double y) {
      8.          this->x = x;
      9.          this->y = y;
     10.      }
     11.      Point translate(double dx, double dy) const;
     12.      void  translate(double dx, double dy);
     13.  };
     14.  
     15.  Point Point::translate(double dx, double dy) const {
     16.      cout << "const translate\n";
     17.      return Point(x+dx,y+dy);
     18.  }
     19.  
     20.  void Point::translate(double dx, double dy) {
     21.      cout << "nonconst translate\n";
     22.      x += dx;
     23.      y += dy;
     24.  }
     25.  
     26.  int main() {
     27.      const Point p1(1,1);
     28.      Point p2(2,2);
     29.  
     30.      p1.translate(3,3);
     31.      p2.translate(4,4);
     32.  }

W klasie Point zadeklarowane są dwie metody translate (o tej samej nazwie i tej samej liczbie i typach parametrów!), ale pierwsza jest stała, a druga nie, więc ich typ różni się wystarczająco, aby takie przeciążenie było możliwe. W programie definiujemy dwa punkty: p1, który jest ustalony(const) i  p2, który ustalony nie jest. Dla obu tych punktów wywołujemy metodę translate ignorując ewentualną wartość zwracaną. Jak widzimy z wydruku

    const translate
    nonconst translate
w pierwszym przypadku, kiedy metoda jest wywołana na rzecz obiektu stałego, kompilator wybierze metodę, która „obiecuje”, że obiektu nie zmieni. Gdyby jej nie było, powstałby błąd kompilacji, bo

na rzecz obiektu stałego można wywoływać wyłącznie metody zadeklarowane jako stałe.

W drugim natomiast przypadku, kiedy obiekt ustalony nie jest, kompilator wybiera nieustaloną wersję metody jako lepiej pasującą. Zauważmy, że gdyby tej nieustalonej wersji nie było, to wywołana zostałaby w tym przypadku wersja ustalona. Tak więc pisząc metodę, która nie zmienia stanu obiektu, lepiej jest napisać ją jako ustaloną, bo wtedy można ją będzie wywoływac zarówno na rzecz obiektów zmiennych jak i stałych. Natomiast metodę nieustaloną można wywoływać tylko na rzecz obiektów zmiennych, nawet jeśli w rzeczywistości wcale obiektu nie zmienia.


Z oczywistych względów konstruktor nie może być deklarowany z modyfikatorem const — jego głównym zadaniem jest bowiem właśnie modyfikowanie stanu tworzonego obiektu.


15.1.1 Pola mutable

Z metodami stałymi występuje pewien problem: czasem chcemy, aby pewna metoda była stała, bo wynika to z logiki klasy do której należy, ale jednocześnie powinna jednak mieć możliwość dokonania pewnych zmian stanu obiektu (czyli jego składowych), dzięki czemu może na przykład być zaimplementowana w sposób bardziej efektywny.

W takich sytuacjach można niektóre pola klasy zadeklarować ze specyfikatorem mutable. Oznacza to właśnie, że odpowiednie składowe obiektów tej klasy mogą być modyfikowane nawet przez metody stałe, zadeklarowane jako const.

Aby zobaczyć, że taka pozorna niekonsekwencja może jednak mieć sens, rozpatrzmy klasę opisującą, w bardzo uproszczony sposób, osobę, którą może na przykład być klient banku.


P110: mutab.cpp     Pola mutable

      1.  #include <iostream>
      2.  #include <string>
      3.  using namespace std;
      4.  
      5.  struct FullInfo {
      6.      string address;
      7.  
      8.      FullInfo(string name) {
      9.          cout << "Szukam adresu w bazie danych" << endl;
     10.          address = "Adres pana " + name;
     11.      }
     12.  };
     13.  
     14.  class Klient {
     15.      string  name;
     16.      mutable FullInfo *fullInfo;
     17.  public:
     18.      Klient(string n) {
     19.          name     = n;
     20.          fullInfo = nullptr;
     21.      }
     22.  
     23.      string getInfo() const {
     24.          return name;
     25.      }
     26.  
     27.      string getFullInfo() const {
     28.          if (fullInfo == nullptr)                 
     29.              fullInfo = new FullInfo(name);
     30.          return name + ", " + fullInfo->address;
     31.      }
     32.  
     33.      ~Klient() {
     34.          delete fullInfo;
     35.          cout << "usuwam " + name << endl;
     36.      }
     37.  };
     38.  
     39.  int main() {
     40.      Klient klient("Nowak");
     41.      cout << klient.getInfo()     << endl;        
     42.      cout << klient.getFullInfo() << endl;        
     43.      cout << "Koniec \'main\'\n";
     44.  }

Obiekt klasy Klient zawiera pewną informację o kliencie, w naszym uproszczonym przypadku jest to po prostu nazwisko (składowa name). Wyobraźmy sobie, że w większości zastosowań ta informacja o kliencie wystarcza. Czasem jednak potrzebna jest informacja bardziej szczegółowa, opisana klasą FullInfo (w naszym przypadku jest tam adres klienta). Ta informacja jest trudno dostępna, bo na przykład wymaga połączenia z odległą bazą danych albo skomplikowanej procedury autoryzacyjnej. Użytkownik klasy Klient nie musi jednak o tym wiedzieć — dla niego ważne jest, że mając obiekt tej klasy powinien być w stanie w każdej chwili zapytać zarówno o nazwisko jak i o adres. Jeśli interesuje go tylko informacja skrócona, używa metody getInfo () — ta metoda oczywiście jest const, bo tylko zwraca żądaną informację, nie modyfikując obiektu.

Co jednak będzie, jeśli użytkownik potrzebuje informacji pełnej? Wywołuje metodę getFullInfo (), która też powinna być stała, bo tylko zwraca informację. Ale implementacja jest inna — zastosowaliśmy tu strategię leniwego wyliczania (lazy evaluation): pełną informację pobieramy dopiero, gdy użytkownik rzeczywiście jej zażąda, gdyż dla większości obiektów nie będzie potrzebna, a zatem pobieranie jej dla wszystkich tworzonych obiektów klasy Klient nie byłoby rozsądne. Widzimy, że metoda getFullInfo sprawdza, czy jest to pierwsze wywołanie (składowa fullInfo jest wtedy nullptr, linia ) i jeśli tak, tworzy dopiero teraz obiekt klasy FullInfo, co wymaga połączenia z bazą danych. Oczywiście, gdy ta sama metoda zostanie wywołana drugi raz dla tego samego obiektu, pełna informacja będzie już dostępna i ponowne łączenie z bazą danych nie będzie potrzebne. A zatem metoda getFullInfo z punktu widzenia „kontraktu” z użytkownikiem powinna być stała; z drugiej strony, przy pierwszym wywołaniu musi zmienić stan obiektu zmieniając składową fullInfo. Dlatego pole fullInfo musiało być zadeklarowane jako mutable. Gdybyśmy ten specyfikator opuścili, program w ogóle by się nie skompilował:

    cpp> g++ -pedantic-errors -Wextra mutab.cpp
    mutab.cpp: In member function
        `std::string Klient::getFullInfo() const':
    mutab.cpp:29: error: assignment of data-member
        `Klient::fullInfo' in read-only structure
Wydruk tego programu (w postaci poprawnej, ze specyfikatorem mutable)
    Nowak
    Szukam adresu w bazie danych
    Nowak, Adres pana Nowak
    Nowak, Adres pana Nowak
    Koniec 'main'
    usuwam Nowak
dodatkowo ilustruje fakt, że obiekty zdefiniowane w funkcji są usuwane już po jej opuszczeniu i wywoływany jest dla nich destruktor.

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