C++ bez cholesterolu

Smaczki

No, skoro więc, jak mam nadzieję, C++ ci się spodobał, to może czas pokazać mnóstwo różnych dziwnych rzeczy, na które się swego czasu naciąłem. W szczególności są to albo rzeczy, z którymi długo nie miałem styczności, albo takie, które trudno się domyślić, jak miałyby wyglądać. Przedstawię je już teraz w sposób swobodny, z niczym nie związany. Wszystkie praktycznie dotyczą programowania obiektowego w C++.

Metody wirtualne nie istnieją w konstruktorze.

Cóż... to już jest po części kwestia teoretyczna: czy w ogóle powinno się spod konstruktora wywoływać metody wirtualne? Ja po jakimś czasie nabrałem wręcz mniemania, że metody wywoływane spod konstruktora powinny mieć specjalne oznaczenie. Tylko jak zabezpieczyć się wtedy przed kimś, kto wywoła zewnętrzną funkcję, a ta zewnętrzna funkcja wywoła metodę nieoznaczoną jako konstrukcyjna? No dobrze, nie teoretyzujmy jednak. Jeśli spod konstruktora wywoła się metodę, to ta metoda jest wywołana zawsze jako metoda z tej klasy, w której ją zdefiniowano, nawet jeśli jest wirtualna.

Mimo usilnych prób, nie udało mi się w żaden sposób zmusić do wywołania wirtualnego spod konstruktora. Podejrzewam nawet, że wiem, jak to jest robione (bo że wcale nie polega to na tym, że specjalnie dla konstruktora wiązanie jest statyczne, to można łatwo sprawdzić). W każdym razie można to zrobić w bardzo prosty sposób: konstruktor każdej klasy polimorficznej pierwsze co robi, to zapisuje wskaźnik na obiekt charakterystyczny wartością wskaźnika na swój własny obiekt charakterystyczny. Robi tak konstruktor każdej klasy w całej hierarchii danej klasy. Mając zatem obiekt klasy B wyprowadzonej z A, konstruktor klasy A najpierw zapisuje w obiekcie wskaźnik na SWÓJ obiekt charakterystyczny (a nie na obiekt charakterystyczny klasy B!). W takim przypadku, jeśli nawet nastąpi wywołanie wirtualne, to niestety zanim konstruktor klasy B się nie rozpocznie, to obiekt niejako "myśli, że jest obiektem klasy A". Dopiero konstruktor klasy B zapisuje ten wskaźnik już wskaźnikiem na swój obiekt charakterystyczny. Jak zatem widać, nic tu się oszukać nie da :).

Niektóre języki pozwalają na wywołania polimorficzne spod konstruktorów, np. Java i C# (co ciekawe, w większości kwestii można mówić o jednym z tych języków, ale wiadomo, że dotyczy obu). Reguła ta jest, delikatnie mówiąc, dziwna. Wypadałoby wręcz zapytać, to po co w takim razie są tam konstruktory. Konstruktorzy języków wręcz zachowują się jak pewna kobieta, której się wydaje, że lusterko w środku w samochodzie jest po to, żeby się mogła w nim przeglądać. Konstruktor w C++ służy do tego, żeby dokonać początkowego wypełnienia zawartości obiektu, a dokładnie, żeby zamienić kawałek pamięci w obiekt. Często konstruktor nie wymaga żadnych akcji, ale też często nie można obiektu w ogóle używać, zanim się nie zakończy jego konstruktor. Pozwalając na wywołanie polimorficzne jednocześnie pozwalamy na wywołanie metody, która w normalnych warunkach powinna spodziewać się już zainicjalizowanego obiektu! W każdym razie, trudno sobie wyobrazić sytuację, żeby inicjalizacja podobiektu klasy podstawowej nakazywała zrobienie czegoś obiektowi klasy pochodnej. Przecież w końcu co za różnica; i tak za chwilę wywoła się konstruktor klasy pochodnej i tam już user zrobi co mu się żywnie podoba. Czyżby zatem było to dla twórców Javy jakimś przejawem wyrywania się na wolność i "palenia ołtarzy"? Wszystko na to wskazuje (ale równie dobrze może to być niedopatrzenie). Bo że twórcy C# zrobili to z pełną premedytacją i przemyśliwszy sprawę, to już w to nie uwierzę; do Microsoftu to niepodobne.

Sytuacja taka nie istnieje oczywiście w najprawdziwszym języku obiektowym, Smalltalku. Ale tam nie istnieją również konstruktory. Można oczywiście utworzyć sobie metodę, która będzie tworzyła i inicjalizowała obiekt, ale to już prywatna sprawa programisty. Na dodatek, nie wierzę, że programiści (tacy już bardziej doświadczeni) wywołują spod takiej metody jakieś inne, które mogą zostać przedefiniowane w klasie pochodnej. Prawdopodobnie miałoby to ten sam sens, co i w C++: wywołanie "Dwa new" i tak tworzy obiekt klasy Dwa, a "Jeden new" i tak tworzy obiekt klasy Jeden.

Na co nie pozwala private w metodach wirtualnych

Wyobraźmy sobie taką sytuację, że klasa podstawowa dostarcza metodę wirtualną, tyle że jest ona prywatna. Nie rozważajmy na razie, czy ma to sens, tylko skupmy się na konsekwencjach. Czego nie możemy zrobić z taką metodą w klasie pochodnej, jeśli w klasie podstawowej jest ona prywatna?

Otóż nie możemy jedynie jej używać. Sekcje dostępu nie dotyczą bowiem kwestii możliwości przedefiniowania metod. Inaczej mówiąc, private nie pozwoli klasie pochodnej na wywołanie tej metody z klasy podstawowej, ale nikt nie zabroni jej przedefiniowania.

W językach C# i Java sytuacja jest identyczna, z tym tylko że jest tam też słowo final, które zabrania przedefiniowywać metodę w klasie pochodnej. Podobno nadużywanie tego doprowadziło do konieczności solidnego przerobienia biblioteki standardowej w Javie, ale to tak na marginesie.

W C++, gdyby się dobrze przyjrzeć, finalizacja metod wcale nie jest potrzebna. Z zasady, jeśli się dziedziczy, to dziedziczy się również interfejs. Można oczywiście różne elementy prywatyzować lub upubliczniać, ale to niewiele zmienia. Jedną z alternatyw dziedziczenia jest, jak wiemy, zawieranie obiektu takiej klasy jako pola. Jeśli chcemy ukryć cały interfejs, czy też udostępnić tylko jego część, to jest to jedno z lepszych rozwiązań. A kwestia finalizacji już wtedy odpada, bo przez brak dziedziczenia i tak uniemożliwiamy dostęp (nawet tylko dla przedefiniowania metod) do klasy podstawowej.

Po co właściwie dziedziczy się prywatnie?

Dziedziczenie prywatne, zaznaczam, nie istnieje tak naprawdę w programowaniu obiektowym, a przynajmniej nie jest to w ogóle dziedziczenie z punktu widzenia czysto obiektowego. Dziedziczenie w C++ posiada dokładnie dwie funkcje: zapożyczenie metod z klasy bazowej wraz z interfejsami oraz użycie klasy bazowej na swoje potrzeby. Inaczej mówiąc, jedno z nich jest rozszerzaniem właściwości danej klasy (to jest właśnie to typowo obiektowe), a drugie to jest skorzystanie z usług danej klasy. O ile pierwsze to istota dziedziczenia, o tyle drugie to jest właściwie używanie młotka do podparcia kiwającego się krzesła. Dziedziczenie prywatne tak naprawdę nie różni się niczym od zawierania pól. No, z wyjątkiem wspomnianego w punkcie powyżej przedefiniowania metod - tak, niestety, nawet jeśli ktoś wyprowadzi klasę z naszej klasy, w której wprowadziliśmy jakąś klasę prywatnie, to i tak z tej prywatnie dziedziczonej klasy user będzie mógł przedefiniowywać metody. Czy to się może do czegoś przydać - nie wiem. To jest właściwie jedyna - poza składnią wywołania metod z klasy podstawowej - różnica pomiędzy dziedziczeniem prywatnym klasy, a umieszczeniem obiektu tej klasy jako pola.

Dziedziczenia prywatnego takoż nie da się oszukać w prosty sposób. Zdefiniowanie operatora konwersji na klasę bazową może zostać przez kompilator skwitowane "chyba sobie jaja robisz, stary". No ale gdyby zdefiniować operator konwersji na WSKAŹNIK do klasy podstawowej... :-).

Przeciążone metody należy przedefiniowywać grupowo

Cóż niestety. Mówiąc inaczej, jeśli mamy w klasie podstawowej trzy wersje metody o jednej nazwie, to przedefiniowanie tylko jednej z nich w klasie pochodnej blokuje pozostałe dwie. Po prostu przedefiniowanie metody o określonej nazwie traktuje się w klasie pochodnej jako przedefiniowanie metody o tej nazwie (a nie nazwie i liście argumentów). Jeśli dostarczy się zatem tylko jedną z wersji w klasie pochodnej, pozostałe stają się niedostępne. Trzeba wtedy dostarczyć definicje również pozostałych metod. Albo, znacznie prościej, użyć konstrukcji: using Klasa::metoda;.

Sekcje dostępu wyznaczają dostęp w obrębie tej samej klasy.

Ale:

Dwa typy powstałe z jednego wzorca to nie da sama klasa.

No właśnie. Co do wzorców, nie wszyscy pamiętają, że typy powstałe przez konkretyzację wzorca przez dwa różne parametry są odrębnymi i niczym nie powiązanymi typami.

W C++ ochronie podlega klasa, nie pojedynczy obiekt. To oznacza, że jeden obiekt może drugiemu "grzebać" w jego polach prywatnych (jest to jedno z zastrzeżeń, które puryści obiektowi mają do C++). Fakt, że jest to taka "nie do końca ochrona", ale w tym wypadku nie jest to niebezpieczne - programista, który opracowuje daną klasę wie, w jaki sposób i z jakiego powodu może się dobierać do sekcji prywatnej.

Zasada ta stosuje się nadal, jeśli dostęp nastąpi w metodzie wołanej na rzecz obiektu tego samego typu. Jeśli jest ona zdefiniowana we wzorcu klasy, to oczywiście te reguły również są tu zachowane. Problem jednak zaczyna się w momencie, gdy mamy wzorzec metody, dzięki czemu metoda może operować obiektem typu opartego na tym samym wzorcu, ale skonkretyzowanym innym parametrem. Może się wtedy okazać, że jeśli przyjmuje się obiekt, którego typ jest konkretyzowany z tym samym parametrem, to kod, który zawierał odwołania do części z sekcji prywatnej/chronionej, skompiluje się. Kiedy jednak pójdzie tam obiekt o typie powstałym z wzorca skonkretyzowanego innym parametrem, to już się przestanie kompilować.

Oczywiście lepiej generalnie nie polegać za mocno na tej regule, że można zawsze odwołać się do pól prywatnych z tej samej klasy, nawet innego obiektu. A jeśli już, to tylko w metodach zwykłych.

Klasa dziedziczona wirtualnie musi mieć zawsze jawnie wołany konstruktor

Wyobraźmy sobie taką hierarchię: mamy X, to jest dziedziczone wirtualnie przez A i B, a te dwie ostatnie przez C. Zatem wewnątrz obiektu klasy C będzie tylko jeden podobiekt klasy X. Konstruktor klasy C musi zawierać wywołania konstruktorów klas A i B (mogą być domyślne, ale żeby sytuację bardziej unaocznić, załóżmy że wszystkie te klasy mają konstruktory z co najmniej jednym argumentem). Kiedy jest tworzony obiekt klasy A (na przykład) i woła się jego konstruktor, to ten zawoła konstruktor klasy X. Podobnie z B. I mamy teraz C, którego konstruktor zawoła konstruktor klasy B. Gdyby dalej X nie było dziedziczone wirtualnie, to konstruktory A() i B() zawołałyby X() (każde swoje oczywiście). A co w przypadku, gdy owo X jest dziedziczone wirtualnie, a zatem współdzielone przez A i B, a takoż i C? W szczególności, kto ma wywołać konstruktor klasy X? Ktoś to musi zrobić, bo przecież nie może go wołać A() i B(), bo konstruktor musi być zawołany raz, nie mówiąc już o tym, że A() i B() mogły zawołać X() z innymi argumentami.

Jak się zatem można domyślać, konstruktor C() sam musi zawołać konstruktor X(). I to samo musi zrobić konstruktor każdej klasy wyprowadzonej zarówno z A i B jak i z C.

Wewnątrz definicji wzorca klasy nie wszystko jest rozpoznawane tak, jak wewnątrz definicji klasy.

Niektórzy nagminnie zapominają, że wzorce w C++ są takie chamskie, że z ich definicji zostanie wzięte do konkretyzacji tylko to, co rzeczywiście jest użyte. Sytuacja taka dodatkowo komplikuje się, jeśli używamy wzorca klasy, który "dziedziczy" z innego wzorca klasy. Nieprzypadkowo "dziedziczy" napisałem w cudzysłowiu. Nie jest to bowiem żadne dziedziczenie wzorców, a jedynie dziedziczenie klas, które powstaną z konkretyzacji tych wzorców. W takim przypadku będzie to przebiegało w ten sposób: z użycia wzorca tworzy się konkretyzację wzorca, a ponieważ wzorzec ma zdefiniowane dziedziczenie, więc konkretyzuje się też wzorzec, który stanowi jego klasę podstawową. Następuje tu oczywiście całkowite odfiltrowanie wszystkiego, co nie jest w sposób jawny użyte.

Co się zatem stanie, jeśli w definicji wzorca "pochodnego" użyliśmy np. odwołania do pól albo metod z "wzorca bazowego"? Otóż tu jest "trochę problem", bo ponieważ wewnątrz metody odwołania do pól nie różnią się od odwołania do zmiennych (a do metod od odwołania do zewnętrznych funkcji), więc kompilator rozwijając taki wzorzec musi przyjąć jakieś ogólne założenia nt. tego, co to może być. Przyjmuje zatem, że to są symbole zewnętrzne. Jeśli to ma być symbol zdefiniowany w klasie, zawsze można użyć "this->".

Dlatego też jeśli we "wzorcu pochodnym" chcesz się odwołać do pól lub metod z "wzorca podstawowego", to musisz użyć jednej z dwóch metod: