C++ bez choletsterolu

Właściwości wartości

Program, poza kodem, składa się ze struktur danych. W szczególności zaś, elementami wykonującego się programu są również obiekty o określonych definicjach. Obiekty te składają się z różnych części, które przechowują określone wartości przez określony czas. Z tych wartości się potem korzysta. Wartość przechowywana w takiej zmiennej charakteryzuje stan owej cząstki. Inaczej, stan cząstkowy programu.

Zanim skupię się na tym, jakie konsekwencje ma to wszystko dla struktur, zajmę się zwykłymi, prostymi wartościami typów ścisłych. Taki int na przykład. Wydawałoby się np., że co za różnica, czy tworzymy zmienną, czy stałą tego typu. Owszem, w C nie ma to znaczenia (dlatego, że w C niczego nie zmieniono w praktyce po wprowadzeniu słowa const, że nie wspomnę o tym, że zostało ono w tym języku zapożyczone z... C++ ;). W C++ różnica jest taka, że – jak powiedziałem – stałe nie muszą posiadać tożsamości. No bo w sumie po co im ona? Po co w ogóle istnieje coś takiego jak tożsamość?

Wyobraźmy sobie dwa obiekty o identycznych wartościach. Jeśli teraz dokona się zmiany w jednym obiekcie, to drugi pozostanie przy starej wartości. Gdyby te dwa obiekty miały jedną tożsamość (jak np. na początku poprzedniego rozdziału w przykładzie ze zmienną referencyjną), to zmiana w jednym obiekcie pociągnęłaby zmianę w drugim obiekcie (może powiedzmy to inaczej: zmiana będzie widoczna po odczytaniu wartości przez takoż pierwszą jak i drugą referencję). Jak widać zatem, tożsamość obiektów jest określana przez równość wartości referencji (tzn. ich wskaźników): jeśli dwa obiekty mają te same wartości referencji, to jest to jeden i ten sam obiekt. Zatem zapis pod jakąś referencję modyfikuje obiekt pod tą referencją. I to wszystko. Czy zatem gdy nie modyfikujemy obiektu, to tożsamość ma jakieś znaczenie? Od razu widać, że nie. Można co najwyżej stwierdzić, że dwa obiekty kto inny tworzył, tworzone były w innym czasie itd. Zatem jeśli nie ma obserwowalnych efektów czasu tworzenia obiektów, to i żadna informacja na ten temat do niczego nam nie posłuży (konkretnie: jeśli kompilator to zignoruje, to nie zmieni to finalnego efektu).

No dobrze, ustaliliśmy zatem jedną rzecz (jest ona co nieco umowna, ale chyba nie jest to nic niebezpiecznego): tylko obiekty zmienne posiadają tożsamość. Jest to jedna z konsekwencji istnienia możliwości ZMIAN w obiektach zmiennych. Jeśli zaś chodzi o obiekty stałe, to skoro i tak nie można im niczego zmienić, to istotna jest tylko ich wartość (nie mówię oczywiście o referencjach z volatile!). W obiektach zmiennych zaś owe wartości są potem przez coś używane, a więc pewne konkretne wartości są w odpowiednim czasie przez odpowiednie procedury oczekiwane (również po to, żeby nadać nową wartość, być może też zależną od aktualnej wartości). Zatem jedną z pierwszych rzeczy, jaką należy na takiej zmiennej wykonać, to nadać jej wartość początkową. Jeśli tego nie zrobimy, zmienna będzie posiadała wartość osobliwą. Możliwość zrobienia tego mamy oczywiście tylko w przypadku zmiennych typów ścisłych i to też nie zawsze (tylko gdy tworzy się obiekty zmienne). Natomiast z obiektami typów strukturalnych jest sprawa trochę bardziej skomplikowana.

Wyjaśnijmy więc sobie może dokładnie, co to jest wartość osobliwa. Konkretnie, jak rozumieć fakt, że zmienna ma wartość osobliwą. Nie jest to takie proste do zrozumiemia, dlatego jest takie ważne, aby to dokładnie określić. Wielu myli wartość osobliwą z wartością niewłaściwą. Podajmy więc oba twierdzenia, które określą owe zachowania:

Niech dany będzie typ T który określa zbiór X wartości, które są dla tego typu prawidłowe. Ponieważ zmienna typu T jest zapisana w pamięci, która stanowi jej wewnętrzną reprezentację, dla niektórych typów może istnieć taka kombinacja bitów, która nie odpowiada żadnej wartości ze zbioru X. Weźmy więc zmienną 'a' typu T i porównajmy ją z dowolną wartością typu T.

Brzmi to trochę dziwacznie, ale dość dobrze oddaje naturę wartości osobliwej. Spróbujmy podać drobny przykład. Wyobraźmy sobie, że w którymś miejscu w programie mamy zmienną typu int i sprawdzamy jej wartość. Wiemy, że jeśli ta wartość jest równa 5, to znaczy, że coś tam konkretnego się w programie stało, w wyniku czego ta zmienna ma właśnie taką wartość. Załóżmy jednak, że zmienna taka ma wartość osobliwą. Nie przeszkadza to oczywiście, żeby ona miała wartość 5. Nie przeszkadza jednakoż, by miała dowolną inną wartość i to niezależnie od tego, co się naprawdę w programie stało, jak też w tej samej sytuacji może ta wartość być różna za każdym razem, kiedy uruchamia się program (czy nawet po prostu przechodzi miejsce pobierania jej wartości). Ważne jest jednak to, że nawet jeśli ta zmienna ma wartość 5, to i tak nic z tego nie wynika - to 5 nie wzięło się stąd, że ktoś rzeczywiście do tej zmiennej jakieś 5 przypisał, ani że powstała w wyniku jakiejś operacji. No dobrze, więc z czego? Czas zatem przedstawić definicję samej wartości osobliwej (zrezygnowałem tutaj z "wersji naukowej" - w końcu strona ta jest przeznaczona dla praktyków, a nie dla naukowców):

Obiekt dowolnego typu ścisłego ("POD")`T' ma wartość osobliwą, jeśli jego wewnętrzna reprezentacja została zmieniona poza kontrolą referencji, tzn.:

Pierwsze dwa są oczywiście oczekiwane, ale skąd trzecie? Otóż jeśli przypiszemy obiektowi wartość innego obiektu, który miał wartość osobliwą, to ten pierwszy obiekt również będzie miał wartość osobliwą. Zatem, jak widać, osobliwość jest chorobą zakaźną :).

Jak zatem widać, w C++ wartość osobliwa "powstaje" (czyli pomijam przypadek, gdy jest kopiowana) albo w przypadku niezainicjalizowania obiektu typu POD, albo gdy narusza się system typów. W C++ jest dokładnie określone, co narusza statyczny system typów - patrz dodatek o rzutowaniu. Z tym tylko że w tym mamy też parę wyjątków, a są nimi wszystkie operatory rzutowania z wyjątkiem reinterpret_cast. Polega to na tym, że opierając się na dynamicznym systemie typów naruszenie statycznego systemu typów uważamy za... no powiedzmy za niebyłe. W przypadku dynamic_cast jest to naruszenie, ale z zapewnieniem poprawności przez dynamiczny system typów (czyli naruszenia nie ma, bo kod, który naruszył również dynamiczny system typów po prostu się nie wykona). W przypadku static_cast i const_cast z kolei konieczne jest "zapewnienie użytkownika" co do tego, że rzeczywisty typ obiektu podlegającego rzutowaniu (w tym również referencji) jest taki jak podano w operatorze lub niejawnie-konwertowalny - czyli to użytkownik musi zatwierdzić, że nie nastąpiło naruszenie DYNAMICZNEJ typizacji.

Dla typów POD wartość osobliwa ma prostą definicję. Dla typów strukturalnych zaś musimy się podeprzeć definicją nie-wprost, tzn. kiedy taki obiekt nie ma wartości osobliwej:

Wartość typu strukturalnego T (czyli deklarowanego słowem class lub struct) jest nieosobliwa, jeśli został wykonany jej konstruktor lub wykonano zmienialną operację, która jest zdefiniowana dla typu T (tu uwaga: właśnie tu zakłada się, że jeśli użytkownik zdefiniował operację zmienialną dla konkretnego typu, to "ufa" się mu, że tak to zorganizował, aby wartość obiektu po takiej operacji była nieosobliwa, nawet zakładając wszelkie naruszania typów ścisłych – polecam w tej kwestii np. deklarację basic_string w gcc). Jeśli zmodyfikowano obiekt po jawnym naruszeniu systemu typów (w sytuacji, gdy nie można zapewnić poprawności dynamicznego systemu typów, oczywiście bierze się wtedy pod uwagę już tylko naruszenie statycznego systemu typów), to jego wartość uważa się za osobliwą.

Tu ważna uwaga! Weźmy np. strukturę: { int a, b; }, która POSIADA konstruktor. Jeśli teraz zadeklarujemy jej konstruktor, który nie zrobi niczego, to mimo że pola a i b będą miały wartość osobliwą (osobliwą jako obiekty typu int), to wartość obiektu całej struktury uważa się za nieosobliwą. To bardzo niebezpieczne założenie. Dlaczego tak jest? Ano dlatego, że - jak pokazałem w definicji - obiekt, któremu wywołano konstruktor uważa się za nieosobliwy. Dlatego też właśnie obiekty typów klasowych (tzn. takich, którym zadeklarowano konstruktor) są teoretycznie bezpieczniejsze, gdyż nie istnieje dla nich możliwość stania się osobliwym przez brak inicjalizacji. Jednocześnie jednak nikt nie mówi, jak należy zrobić konstruktor - zatem w praktyce to jest dużo większe potencjalne źródło błędów. Należy więc pamiętać, że jeśli wewnątrz obiektu typu klasowego jakiś pod-obiekt ma wartość osobliwą, to jest to osobliwość tylko na terenie tego obiektu - kwestia ta nie wpływa na właściwość wartości całego obiektu. Jeśli oczywiście nie zadeklarujemy konstruktora, to wtedy struktura jest traktowana tak jak typy POD, czyli ma wartość osobliwą tak długo, jak długo nie nada się początkowej wartości każdemu z jej pól.

Osobnym zagadnieniem są struktury, które nie mają konstruktora, ale zawierają pola, których typy mają konstruktory domyślne (gdyby nie miały domyślnych, to wtedy taka struktura musiałaby już mieć konstruktor), jak np. std::string. Po utworzeniu obiektu takiego typu, oczywiście, jak się można domyślać, tylko te pola, które są typów POD są osobliwe. Nie zmienia to jednak faktu, że dopóki choć jedno pole ma wartość osobliwą, to cały obiekt uważa się za osobliwy (faktem jest, owszem, że taka struktura już nie jest agregatem, ale to nie zmienia faktu, że struktura nie ma zdefiniowanego konstruktora, w związku z czym nie ma żadnego "czynnika zapewniającego nieosobliwość").

Typ unijny nie posiada wartości i dla takiego typu nieosobliwą wartość może mieć tylko jedno z pól w jednym czasie, natomiast pozostałe posiadają wartość osobliwą. Oczywiście typ unijny ma swoją wyjątkowość, w wyniku której można powiedzieć tylko, że odczyt tylko ostatnio zapisanego pola (bo konstruktorów unie nie mają) da w wyniku wartość nieosobliwą. Jeśli nie zapisano żadnego pola, to oczywiście każde ma wartość osobliwą - normalna sytuacja niezainicjalizowanych obiektów.

Jak wynika z powyższej definicji, gwarantuje się pewne rzeczy dla typów ścisłych, natomiast dla typów złożonych pewne rzeczy należy zapewnić samemu. Bazując oczywiście na typach ścisłych, bo w końcu każda, najbardziej złożona nawet struktura bazuje na typach ścisłych. Jak inicjalizować struktury, to już wiemy, jak również, że ten sposób inicjalizacji jest przestarzały. Struktury można też nie inicjalizować. Co się stanie? Nic. Niestety owo "nic" nie oznacza, że jest to bez znaczenia; weźmy sobie np. strukturę:

struct S {
  int a, b;
  string s;
};

Możemy sobie teraz zadeklarować obiekt takiego typu i to utworzy obiekt o wartości osobliwej:

S s;

Ale w tych przypadkach:

S s = S();
S s = { a, b };

obiekt `s' jest nieosobliwy. Tak, jak to opisałem w rozdziale "Wspomaganie organizacji kodu", jeśli S nie ma zdefiniowanego konstruktora to jest parę skomplikowanych reguł, w każdym razie w bieżącym przypadku (w tej pierwszej deklaracji) pola a i b mają wartości osobliwe. Sam obiekt również jest osobliwy, co wynika z faktu, że S, choć nie jest typem POD, nie ma zadeklarowanego konstruktora, a konstruktor "pusty" dla typów POD (w tym wypadku pól a i b) nie nadaje im żadnych wartości.

Jednak oczywiście, jak też można się domyślić, tak konstruować obiekty można tylko podczas tworzenia obiektów zmiennych! Gdybyśmy chcieli utworzyć stały obiekt takiego typu strukturalnego, to wtedy niestety taka deklaracja zostanie odrzucona przez kompilator. Dlaczego? Ano dlatego, że – jak wspomniałem – nie można utworzyć obiektu stałego typu ścisłego (lub – jak w tym przypadku – agregatu) nie inicjalizując go. Tutaj nastąpiłaby właśnie taka próba, gdyż aby utworzyć cały obiekt typu strukturalnego, należy utworzyć każdą z jego części (a dokładnie to konstruktor domyślny wywołuje po kolei konstruktory domyślne każdego z pól). Jeśli chcemy utworzyć obiekt stały, a żadnemu polu nie określono wariancji, więc będą miały wariancję stałą. W związku z czym można je utworzyć tylko jako obiekty stałe. Jak wiemy, obiekty stałe typów ścisłych musimy czymś zainicjalizować. I ta sytuacja ma miejsce również tutaj. Zatem... zresztą spójrzmy na przykład:

struct X { int a, b, c; };
X x1; // dobrze, ale pola x1 są osobliwe
X x2 = { 1, 2, 3 }; // dobrze, pola x2 są nieosobliwe
const X cx1; // źle! nie ma konstruktora domyślnego dla const int
const X cx2 = { 1, 2, 3 }; // dobrze

Jak wiec widać, to że możemy napisać "X x1;" wynika tylko stąd, że dla obiektów zmiennych typów ścisłych, czyli tutaj pod-obiektow typu X, są dostępne konstruktory puste - i takoż dla X, gdyż żadnego konstruktora nie zdefiniowaliśmy. Owe konstruktory nie są dostępne, jeśli tworzy się obiekt stały. Dzięki temu istnieje zapewnienie, że nie zostaną utworzone obiekty stałe o wartości osobliwej.

Niestety użytkownik może łatwo złamać to zapewnienie. Oczywiście jeśli wewnątrz struktury zamieści pole stałe, to kompilator mu nie popuści. Ale jeśli stały będzie tylko nad-obiekt, a użytkownik zrobi sobie konstruktor domyślny, który nie robi niczego, to w efekcie dopuszcza możliwość zrobienia sobie wartości stałej osobliwej. Jest to właśnie jedna z sytuacji, kiedy użytkownikowi pozostawia się inicjatywę co do niektórych gwarancji i on może je obejść, jeśli tak zechce.