====== 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ę.