czwartek, 11 lutego 2010

Reklama na tvn24.pl



Dzieciom życzymy kochających nowych właścicieli.

poniedziałek, 8 lutego 2010

Refleksje na temat inteligentnych wskaźników w C++ i projektowania programu.

Czy je polecam?
Tylko początkującym.
Czy coś dają?
I tak i nie.

Czemu "tak"?

Chciałbym zwrócić uwagę czytelnika na zaletę tych wskaźników,
nie będącą kolejnym powtarzaniem, że umożliwiają niepisanie 'delete'.

W przeciwieństwie do referencji z Javy czy C# inteligentne wskaźniki nadal wymuszają dobry projekt programu, robiąc problemy ze względu na cykle referencji. Konkretnie, w przypadku cyklu A-B-A wymuszają, żeby np. obiekt A miał inteligentny wskaźnik do B a B tylko zwykły wskaźnik do A. To jawnie wskazuje, który obiekt jest właścicielem
drugiego. To nie daje tylko destrukcji we właściwej kolejności. To również wymusza strukturę właścicielską.

Nazwijmy słowem "właściciel" jakiś obiekt, który ma inteligentny wskaźnik na inny obiekt. Program zbudowany w ten sposób jest acyklicznym grafem skierowanym (DAGiem) z właścicielem wszystkiego jako korzeń. Nad takim kodem dużo łatwiej jest zapanować niż nad dowolnym grafem, gdzie użycie jakiegoś konkretnego elementu jest bardzo utrudnione bo nigdy nie wiadomo, jakie obiekty trzeba mu dać, gdzie go stworzyć, w jakiej kolejności wykonać inicjalizację itd. W dagu, jak jest właściciel, to
można go poprosić o stworzenie jakiejś posiadłości. To właśnie właściciel albo od razu potrafi stworzyć posiadłość, bo miał wymuszoną inicjalizację, albo ma dobrą dokumentację i jakieś asercje uniemożliwiające tworzenie rzeczy w niewłaściwej kolejności.

Dag wykazuje duże podobieństwo do drzewa, zatem łatwo go, albo jakiś jego sektor w drzewo zamienić, czyniąc projekt jeszcze bardziej eleganckim. Przeprojektowanie dagu też jest prostsze, niż zwykłego grafu, bo zmiany będą miały wpływ tylko na określony podgraf, nie na cały system. W przypadku zaś, gdy robimy program współbieżny, daje to możliwość uruchomienia współbieżnie podgrafów tam, gdzie się rozłączają, bez obaw że gdzieś poza wspólnym właścicielem są jakieś współdzielone dane, które wymagają synchronizacji.

Kwestia poprawnego zaprojektowania programu mogłaby być przez kogoś zrozumiana jako zrobienie takiego programu, gdzie poprawnie działają tworzenie i niszczenie obiektów i dzięki temu nie ma np.odniesienia przez wskaźnik z adresem null lub przedwczesnego niszczenia obiektu. Problem w tym, że niektóre tego typu błędy wynikają ze złego projektu a niektóre to po prostu zwykłe usterki techniczne, spowodowane np. nienapisaniem 'delete'. Inteligentne wskaźniki, eliminując niektóre proste błędy pozwalają oddzielić te dwa zagadnienia. Jako efekt uboczny, możliwe jest łatwiejsze eksperymentowanie ze strukturą programu, bez obaw że gdzieś zapomni się 'delete'.

Czemu "nie"?

Jeśli potrafi się dobrze projektować program jako dag, to nie dadzą dużo, bo kolejność destrukcji jest jak najbardziej spójna i problem sprowadza się do napisania 'delete' dla każdego 'new', a to można osiągnąć zwykłym opakowaniem, nie wspierającym inteligentnego współdzielenia obiektu.

Inteligentne wskaźniki zabierają jednak trochę czasu, bo są z nimi czasami duże problemy. Problemem może być np. to, że jakiś obiekt jest przedwcześnie niszczony, gdy inteligentny wskaźnik przez przypadek na moment zamienia się na zwykły po to, by zaraz znowu przemienić się w inteligentny. Wtedy obiekt jest niepotrzebnie niszczony i ciężko jest potem znaleźć taki błąd, często objawiający się gdzieś daleko od zręcznie zakamuflowanego przez niejawne wywołanie destruktora miejsca zniszczenia. Gdzieś spotkałem się nawet z określeniem "smark pointers", nawiązującym do tego problemu.

Innym problemem są częste problemy z debugowaniem niszczenia obiektów, które są niszczone przez destruktory obiektów globalnych. Brak jawności destrukcji sprawia wtedy więcej szkody niż pożytku, bo pisząc ją jawnie mamy przynajmniej pewność, że jeżeli wszystko zadziała, to zadziała w określony przez nas sposób.

Jest jeszcze inny problem. Konkretnie to może się zdarzyć, że inteligentny wskaźnik na dany obiekt może zostać stworzony więcej niż raz, nie kopiowaniem wskaźnika, tylko z surowego adresu. To również powoduje przedwczesne zniszczenie obiektu. Jest strategia zapobieżenia temu, konkretnie stworzenie globalnej mapy (adres->licznik odniesień), gdzie wskaźniki sprawdzałyby liczbę odniesień, zamiast przechowywać ją bezpośrednio samemu. To jednak wymaga danych globalnych i przez to sprawia olbrzymie problemy we współbieżnym kodzie.


Werdykt

Ostatecznie mój werdykt jest następujący: nie używać inteligentnych wskaźników, które utrudniają życie i nie przerzucać się na Javę, gdzie wszyscy współpracownicy będą uprawiać w projekcie wolną Amerykankę, rzygając w czasach kryzysu obiektami NullPointerException. Projektować dobrze program i używać w klasie-właścicielu opakowania (np. auto_ptr) na posiadany obiekt.

EDIT: (Żeby nie wywoływać świętej wojny Java kontra C++, dodam, że mam zastrzeżenia do zbyt luźnego modelu orientowania obiektowego w Javie, natomiast nie mam ich samej Javy jako platformy, bo ilość bibliotek standardowych sprawia, że każdy powinien przynajmniej rozważyć Javę jako język do swojego nowego projektu).