====== Stałość fizyczna i logiczna ===== ===== Deklarowanie stałych ===== Stałość fizyczna występuje, gdy w momencie kompilacji określamy przy pomocy modyfikatora ''const'', że jakiejś wartości nie należy zmieniać. const int foo = 42; /* Definicja "zmiennej" foo będącej stałą liczbą całkowitą i inicjacja jej wartością 42. */ foo = 666; /* Błąd kompilacji */ Modyfikator ten odnosi się do typu, co ma szczególne znaczenie przy wskaźnikach: int bar, baz; const int *ptr1 = &bar; /* Wskaźnik na stały int. */ ptr1 = &baz; /* Poprawne. */ *ptr1 = 666; /* Błąd kompilacji. */ int *const ptr2 = &bar; /* Stały wskaźnik na int. */ ptr2 = &baz; /* Błąd kompilacji. */ *ptr2 = 666; /* Poprawne. */ const int *const ptr3 = &bar; /* Stały wskaźnik na stały int. */ ptr3 = &baz; /* Błąd kompilacji. */ *ptr3 = 666; /* Błąd kompilacji. */ Rzecz jasna mamy do dyspozycji także większe "zagłębienia" wskaźników: const int **ptr4; /* Wskaźnik na wskaźnik na stały int. */ int *const *ptr5; /* Wskaźnik na stały wskaźnik na int. */ int **const ptr6; /* Stały wskaźnik na wskaźnik na int. */ const int *const *ptr7; /* Wskaźnik na stały wskaźnik na stały int. */ const int **const ptr8; /* Stały wskaźnik na wskaźnik na stały int. */ int *const *const ptr9; /* Stały wskaźnik na stały wskaźnik na int. */ const int *const *const ptrA; /* Stały wskaźnik na stały wskaźnik na stały int. */ Możnaby tak w nieskończoność, szczególnie, że mamy jeszcze wskaźniki do funkcji... No ale, powstrzymam się. ;-) Przypominam, że w C oraz C++ typy czyta się "od prawej do lewej". ===== Stałość a modyfikowalność ===== Należy zwrócić uwagę, iż z faktu, że wskaźnik (referencja) jest stałego typ nie wynika, iż wskazywana wartość nie może się zmienić. Jest to częste, a niesłuszne domniemanie. Najprostrzym przykładem może być kod: int foo = 42; int *bar = &foo; const int *baz = &bar; /* *baz ma wartość 42 */ *bar = 666; /* *baz ma wartość 666 */ Różnie bywa z wartościami zadeklarowanymi jako stałe. Mogą one zostać zapisane w niemodyfikowalenj przestrzeni pamięci i próba zmiany ich wartości może spowodować coś pokroju //segmentation fault//, ale równie dobrze mogą być umieszczone w zwykłej przestrzeni i wówczas sztuczka z rzutowaniem pozwoli na ich zmianę: const int foo = 42; *const_cast(&foo) = 666; /* niezdefiniowane zachowanie */ const_cast("bar")[2] = 'z'; /* niezdefiniowane zachowanie */ ===== Stałość argumentów funkcji ===== Deklarowanie argumentów (chodzi o sam argument, a nie ewentualne wskazywane typy, a więc referencje odpadają z rozważań) funkcji jako stałych nie wpływa na zewnętrzne zachowanie funkcji, gdyż i tak argumenty przekazywane są przez wartość (o ile nie jest to referencja). Można to jednak stosować, aby zapobiedz zmianie wartości argumentów, co jest zalecaną przez niektórych praktyką, np.: int add(const int a, const int b) { a += b; /* Błąd kompilacji. */ return a + b; /* Poprawne. */ } Deklarowanie typów wskazywanych przez funkcje jako stałe ma za to duży wpływ na zachowanie zewnętrzne programu i **należy** je stosować wszędzie tam, gdzie jest to możliwe. Tzn. jeżeli jakaś funkcja przyjmuje wskaźnik lub referencję na argument, którego nie zamierza modyfikować powinna wskazywany typ zadeklarować jako stały, np.: int sum(unsigned n, const int *nums) { int ret = 0; while (n--) { ret += *num++; } return ret; } static const int nums[] = { /* ... */ }; /* ... */ sum(sizeof nums / sizeof *nums, nums); /* gdyby w prototypie funkcja miała typ `int*`, a nie `const int*` ta linijka spowodowałaby błąd kompilacji. */ Ponadto, często o wiele lepiej przekazywać argument przez stałą referencję zamiast przez wartość, gdyż nie wymaga to kopiowania całego obiektu, np.: int foo(const std::vector &vec) { /* Rób coś na wektorze */ } /* versus */ int foo(std::vector vec) { /* Rób coś na wektorze */ } W obu przypadkach, wołając funkcję, mamy pewność, iż wektor przekazany jako argument nie zostanie zmodyfikowany (mowa o zachowaniu na zewnątrz funkcji), ale przekazywanie wektora przez wartość jest po prostu stratą czasu. Dotyczy to również innych mniejszych i większych obiektów. Szczególnie, gdy definiujemy jakiś szablon należy stosować mechanizm przekazywania argumentów przez stałą referencję zamiast przez wartość, gdyż nie wiemy z jakim typem będziemy mieli do czynienia. W szczególności klasa może nie mieć (publicznego) konstruktora kopiującego. ===== Rzutowanie ===== Jak można się domyślić, rzutowanie z typu bez modyfikatora ''const'' na tym z takim modyfikatorem jest automatyczne, np.: int foo = 42; const int *bar = &foo; Rzutowanie w drugą stronę nie jest już automatyczne i wymaga zastosowanie operatora rzutowania: const int foo = 42; int *bar = (int*)&foo; /* styl C */ int *baz = const_cast(&foo); /* styl C++ */ Generalnie zalecany jest styl C++, gdyż w ten sposób jesteśmy pewni, że rzutowanie zmieni jedynie stałość typu. Przykładowo, gdybyśmy zmienili typ zmiennej ''foo'', a zapomnieli zmienić typy w operatorach rzutowania kompilator bez żadnych ostrzeżeń skompilowałby rzutowanie w stylu C, ale zgłosiłby błąd przy rzutowaniu w stylu C++, gdyż zmiana dotyczy nie tylko stałości typu: const long foo = 42; int *bar = (int*)&foo; /* Skompiluje się. */ int *baz = const_cast(&foo); /* Błąd kompilacji. */ Pewien wyjątek stanowią literały ciągów znaków, który wywodzi się z czasów, gdy w języku C nie było słówka ''const''. Zasadniczo literały ciągów znaków są typu ''const char[]'' jednak, aby nie psuć tysięcy istniejących programów, przypisanie literału do zmiennej typu ''char*'' jest poprawne. **Nie** oznacza to jednak, iż literały takie można modyfikować! Następująca instrukcja powoduje niezdefiniowane zachowanie: ''%%char *foo = "foo"; foo[0] = 'F';%%'' (problem ten został już poruszony przy omawianiu modyfikowalności). Kolejnym aspektem, który może wydać się dziwny, jest fakt, iż konwersja ''Foo%%**%%'' do ''const Foo%%**%%'' **nie** jest automatyczna. Można powiedzieć, że stałość musi być dodana wszędzie po prawej stronie (za wyjątkiem samej zmiennej) i konwersja ''Foo%%**%%'' do ''const Foo* const *'' jest już dozwolona: void bar(const Foo **arr); void baz(const Foo *const *arr); int main(void) { Foo **arr; /* ... */ bar(arr); /* Błąd kompilacji. */ baz(arr); /* Poprawny kod. */ /* ... */ } Aby zrozumieć czemu tak jest należy przeanalizować poniższy kod: const int x = 42; int *p; const int **pp = &p; /* konwersja `int**` do `const int**`*/ *pp = &x; /* `*pp` jest typu `const int*`, więc można mu przypisać adres zmiennej `x`. */ /* w tym momencie `p` wskazuje na `x` (gdyż `pp` wskazywało na `p`. */ *p = 666; /* `x` jest modyfikowane! */ ===== Stałość w strukturach ===== Stałe struktury (klasy) nie umożliwiają zmiany pól w ich wnętrzu. Przykładowo, poniższy kod się nie kompiluje: struct Foo { int bar; }; const Foo baz = { 42 }; baz.bar = 666; Jednakże, jeżeli elementem struktury (klasy) jest wskaźnik jedynie on sam staje się stały, ale wartość wskazywana nie, np.: struct Foo { int *bar; }; static int qux = 42, quux = 042; const Foo baz { &qux }; *baz.bar = 666; /* Poprawne. */ baz.bar = &quux; /* Błąd kompilacji. */ Jest to problem stałości logicznej, o której poniżej. Niestatyczne metody klas również mogą być zadeklarowane jako stałe. Metody takie nie mogą modyfikować pól klasy (chyba, że posiadają modyfikator ''mutable'', o którym niżej) ani wołać innych metod, które nie są zadeklarowane jako stałe. struct Foo { int get() { return bar; } /* [1] */ int get() const { return bar; } /* [2] */ int sum(int v) { return get() + v; } /* [3]; woła [1] */ int sum(int v) const { return get() + v; } /* [4]; woła [2] */ void set(int v) { bar = v; } /* [5] */ void set(int v) const { bar = v; } /* niepoprawne */ void add(int v) { set(sum(v)); } /* woła [3] i [5] */ void add(int v) const { set(sum(v)); } /* niepoprawne, woła [4], ale [5] nie jest metodą stałą. */ private: int bar; }; Czasami bywa tak, że metoda ma swoją wersję stałą i niestałą, które robią identyczną rzecz, ale np. jedna zwraca wskaźnik do stałego obiektu, a druga do niestałego obiektu. Aby nie musieć pisać dwa razy tego samego kodu można zastosować rzutowanie, np.: struct Foo { /* ... */ const int *find(int arg) const; int *find(int arg) { return const_cast(const_cast(this)->find(arg)); } /* ... */ }; lub struct Foo { /* ... */ const int *find(int arg) const { return const_cast(this)->find(arg); } int *find(int arg); /* ... */ }; Rzutowanie słówka ''this'' wymusza wołanie stałej lub niestałej wersji danej metody. Bez niego mielibyśmy do czynienia z niekończącą się rekurencją. C++ wprowadza jeszcze jedno słowo kluczowe -- ''mutable'', które oznacza, że dany element struktury może być modyfikowany, w tej klasie nawet jeżeli jest ona stała. Przykładowo: struct Foo { int bar; mutable int baz; void mutate() const { bar = 042; /* Błąd kompilacji. */ baz = 42; /* Kod poprawny. */ } }; const Foo foo = { 42, 042 }; foo.bar = 666; /* Błąd kompilacji. */ foo.baz = 666; /* Kod poprawny. */ Mechanizm ten powinien być stosowany tylko i wyłącznie dla pól, które nie wpływają na zewnętrzny wygląd i zachowanie klasy (patrz stałość logiczna). Przykładowo, można zaimplementować cache, w której przechowywane by były ostatnie wyniki jakichś zapytań czy wyszukiwań: struct Foo { /* ... */ const int *find(int arg) const { if (last_arg == arg) { return last_found; } else { int *found; /* Wyszukaj i ustaw `found` odpowiednio. */ last_arg = arg; return last_found = found; } } /* ... */ private: mutable int *last_found; mutable int last_arg; }; Alternatywą dla słówka ''mutable'' byłoby rzutowanie, jednak w różnych przypadkach może ono spowodować niezdefiniowane zachowanie, co zostało już omówione. ===== Stałość logiczna ===== Stałość logiczna to oczekiwanie, że zewnętrzne zachowanie i wygląd jakiegoś obiektu nie ulegnie zmianie. Dla przykładu weźmy strukturę do przechowywania liczb zespolonych: struct Complex { double re, im; }; W tym przypadku zadeklarowanie jakiejś zmiennej jako stałej gwarantuje nam stałość logiczną, np.: double abs(const Complex &c) { return hypot(c.re, c.im); } Jednak w bardzo rzadkich przypadkach stałość fizyczna może być //nadgorliwa//, np. jeżeli cacheujemy wyniki: struct Complex { double re, im, abs; bool abs_valid; } double abs(const Complex &c) { if (c.abs_valid) { return c.abs; } else { c.abs_valid = true; return c.abs = hypot(c.re, c.im); } } Powyższy kod oczywiście się nie skompiluje, ale z pomocą przychodzi nam już wcześniej opisane słówko ''mutable''. O wiele częściej stałość fizyczna jest //za mało gorliwa//. Przykładowo, jeżeli mamy strukturę przechowującą ciągi znaków, to spodziewamy się, iż po zadeklarowaniu zmiennej jako stała struktura nie będzie możliwości zmieniać samego napisu, ale niestety tak nie jest: struct String { char *data; unsigned length; }; void modify(const String &s) { s.data[0] = 'A'; } W takich przypadkach należy odpowiednio obudowywać klasy dodając im przeciążone akcesory istniejące zarówno w wersji jako metoda stała oraz w wersji jako zwykła metoda. struct String { char *getData() { return data; } const char *getData() const { return data; } unsigned getLength() const { return length; } private: char *data; unsigned length; }; Oczywiście nadal wewnątrz stałej metody obiekty wskazywane przez pole ''data'' mogą być modyfikowane i dlatego cała odpowiedzialność, za utrzymanie stałości logicznej spada na programiście, który implementuje daną klasę.