C++/Obiekty stałe

< C++

Obiekty stałe to takie, których stan - z punktu widzenia interfejsu klasy - nie może się zmienić; obiekt stały można rozumieć jako widok na dane, które można jedynie czytać. To rozróżnienie, wspierane wprost przez język, ma jeden cel: uniemożliwić modyfikację, także przypadkową; dodatkowo kompilatory potrafią wykorzystać te informacje przy optymalizacji kodu.

Obiekty mogą być stałe już w chwili deklaracji (np. napisy), albo stać się takie w obrębie funkcji do której zostały przekazane jako argument. Aby zadeklarować obiekt stały należy poprzedzić nazwę typu słowem kluczowym const:

const Klasa obiekt;
const std::string = "wikibooks.pl";

Analogicznie przy przekazywaniu argumentu do funkcji:

void funkcja(const Klasa obiekt) {
    // działania na obiekcie
}

Można bardziej formalnie powiedzieć, że typy Klasa oraz const Klasa są różne; co więcej, konwersja z typu Klasa na const Klasa jest dopuszczalna, odwrotna jest zabroniona.

Na obiekcie stałym można wywołać jedynie metody oznaczone jako stałe oraz wyłącznie czytać pola, jeśli takie są publicznie dostępne. Metoda jest stała jeśli została zadeklarowana z klasyfikatorem const - w przykładowej klasie poniżej taką metodą jest wartosc.

Metody stałe mogą wywoływać tylko inne metody stałe i odczytywać pola (z małym wyjątkiem, o czym w kolejnych sekcjach) - zapis wartości do pól obiektu oraz wołanie nie-stałych metod jest zabronione. UWAGA: To ograniczenie nie zależy od tego, czy sam obiekt na którym wołana metoda jest stały.

class Klasa {
private:
    int liczba;

public:
    Klasa() : liczba(0) {}

    void dodaj(int x) {
        liczba += x;
    }

    void zmien_znak() {
        liczba = -liczba;
    }

    int wartosc() const {
        return liczba;
    }
};

int main() {

    const Klasa obiekt;

    // obiekt.dodaj(42);    // niemożliwe, metoda zmienia obiekt
    // obiekt.zmien_znak()  // niemożliwe, metoda zmienia obiekt
    
    return obiekt.wartosc(); // wartosc jest const, tylko odczyt
}

Istotne jest, że już istniejący obiekt może być używany jako stały. Funkcja wyswietl nie zmieni szerokości ani wysokości, jednak legalnie wywołuje inną funkcję, która również jedynie czyta parametry prostokąta.

#include <iostream>

class Prostokat {
public:
    int szerokosc;
    int wysokosc;
};

int pole(const Prostokat& p) {
    return p.szerokosc * p.wysokosc;
}

void wyswietl(const Prostokat& p) {
    std::cout << p.szerokosc << " x " << p.wysokosc << ", pole = " << pole(p) << '\n';
}

int main() {
    Prostokat p;

    p.szerokosc = 12;
    p.wysokosc  = 5;

    wyswietl(p);

    p.wysokosc  = 6;

    wyswietl(p);
}

Stałe pola klasy

edytuj

Pola klasy również mogą być zadeklarowana jako stałe, ich wartości muszą zostać ustawione na liście inicjalizacyjnej.

class Terminal {
    const int kolumny;
    const int wiersze;

public:
    Terminal() : kolumny(80), wiersze(25) {

        //kolumny = 80; // mimo, że w konstruktorze,
        //wiersze = 25; // to przypisanie niemożliwe
    }
};

Pola mutable

edytuj

Niekiedy istnieje potrzeba, aby nawet stały obiekt mógł zmieniać swój wewnętrzny, niepubliczny stan. Można pomyśleć o algorytmach ze spamiętywaniem (ang. memoization), częstym przykładem jest też cache dla niezmieniającej się kolekcji. Metoda wyszukująca istotnie nie ma prawa zmienić samej kolekcji, ale mogłaby zapisywać wynik kilku ostatnich wyszukiwań i szybciej dawać odpowiedź. Z punktu widzenia użytkownika klasy nic się nie zmienia, ponieważ wyniki metody będą zawsze takie same, niezależnie od tego, czy zapytanie trafi w cache, czy nie (zakładając oczywiście bezbłędną implementację całości).

Zacznijmy od klasy bez pamięci podręcznej:

#include <vector>

class Kolekcja {

    std::vector<int> wartosci;
    
public:
    int indeks(int wartosc) const {
        for (auto i=0; i < wartosci.size(); i++) {
            if (wartosc == wartosci[i]) {
                return i;
            }
        }

        return -1; // brak danych
    }
};

(Celowo został tu użyty nieoptymalny algorytm wyszukiwania liniowego, żeby wykazać potrzebę zastosowania pamięci podręcznej. Normalnie należałoby użyć typu std::map lub std::unordered_map albo jakiejś własnej, lepszej struktury danych.)

Teraz klasa, która ma cache. Najistotniejsze są tutaj dwa pola: ostatnia_wartosc i ostatni_wynik, oba zostały poprzedzone słowem kluczowym mutable, to znaczy, że zgadzamy się, żeby metody stałe je modyfikowały.

class KolekcjaZCache: public Kolekcja {

    mutable int ostatni_wynik;
    mutable int ostatnia_wartosc;
    
public:
    int indeks(int wartosc) const {
        if (wartosc == ostatnia_wartosc) {
            return ostatni_wynik;
        }

        ostatnia_wartosc = wartosc;
        ostatni_wynik = Kolekcja::indeks(wartosc);

        return ostatni_wynik;
    }
};