Kiedy programuje się w języku, który jest – jak to ładnie mówią Amerykanie – ‘standardem przemysłowym’ w danej dziedzinie, chcąc nie chcąc trzeba się do niego przyzwyczaić. A to oznacza, że musimy nauczyć się żyć z jego wadami, które niekiedy mogą być tylko irytujące, a niekiedy bardzo nieprzyjemne. Ważne, by mieć świadomość ich istnienia i w miarę możliwości sobie z nimi radzić.
C++ jako ustandaryzowany język ma już prawie dziesięć lat, więc lista jego niedoskonałości w porównaniu z nowszymi językami siłą rzeczy staje się coraz dłuższa. Takie listy są jednak w dużym stopniu subiektywne, zatem i ten, który rozpoczynam poniżej, absolutnie nie pretenduje do miana ostatecznej wyroczni :) Na pewno inni usunęliby z niego część pozycji, niektóre uznali za mniej lub bardziej dotkliwe, a także dodali własne propozycje.
Dla mnie sprawą, która w C++ jest powodem największego bólu zębów, jest sposób organizacji kodu zaproponowany w tym języku. Jest on bodaj jedynym mi znanym (poza swoim poprzednikiem C), w którym występuje podział plików z kodem na dwa rodzaje: pliki nagłówkowe (*.h, *.hpp) i moduły kodu (*.cpp). W tych pierwszych teoretycznie umieszczany jest interfejs klas i funkcji, czyli informacje potrzebne do ich użycia w innym miejscu programu. W tych drugich jest zaś zawarta implementacja rzeczonych klas oraz funkcji.
Tyle teorii. W praktyce osiągnięcie idealnej separacji obu tych elementów wymaga sztuczki nazywanej szumnie wzorcem projektowym, o nazwie Pimpl (skrót od private implementation). Wygląda on na przykład następująco:
Wtedy faktycznie nie widać, co siedzi w środku naszej klasy, ale cena takiej hermetyzacji to konieczność stworzenia dodatkowej klasy i przekierowania do niej metod. Może nie jest to dwa razy więcej roboty, ale przynajmniej 10-20%.
Z drugiej strony większość języków nowszych niż C++, jak Java, C# czy Python, w ogóle zna takich pojęć jak ‘prototyp funkcji’ czy ‘deklaracja zapowiadająca’. Tam kod piszemy od początku do końca: funkcję zawsze z jej treścią, a klasę zawsze w całości łącznie ze wszystkimi polami i kodem wszystkich metod. Nie ma podziału na pliki z ‘interfejsem’ i z implementacją.
A w C++ ten podział istnieje i tak naprawdę służy on tylko i wyłącznie… wygodzie kompilatora. Dzięki temu, że treść plików nagłówkowych jest taka, a nie inna (i np. zawierają one deklaracje prywatnych pól klas, które są przecież częścią implementacji), mogą być one zwyczajnie dołączane do modułów przy pomocy arcyprymitywnej dyrektywy #include
. Dla kompilatora jest to najprostsze rozwiązanie, bo może on pracować nad każdym modułem osobno i nie musi się martwić żadnymi niejawnymi zależnościami między plikami. A dla programisty oznacza to wybór między wygodą kodowania, a jakością i “odpornością” stworzonego kodu.
C++ stoi więc w pewnym sensie pośrodku i wcale nie jest to złoty środek. Z jednej strony mógłby pójść w kierunku wyznaczonym przez wspomniane nowsze języki, czyli wprowadzenia jednego typu plików źródłowych, zawierających kod bez podziału na deklaracje i implementacje. To by było jednak mało oryginalne :) Bardziej interesujące byłoby, jak sądzę, polepszenie istniejącego rozwiązania w odwrotnym kierunku; być może automatyczne stosowanie wzorca Pimpl dla każdej klasy jest jednym ze sposobów.
Niestety, patrząc na obecny roboczy szkic standardu C++0x, w którym nic na ten temat nie znajdziemy, nie możemy raczej oczekiwać, że coś tu się zmieni w przewidywalnej przyszłości.
Zdarza się, że chcemy napisać coś “na szybko”. Możliwe na przykład, że znaleźliśmy przepis na ciekawy efekt graficzny i chcemy go od razu wypróbować. Albo natrafiliśmy na nietypowy algorytm rozwiązujący jakiś problem i mamy ochotę go wypróbować w akcji. Zależnie od tego, jak bardzo lubimy eksperymentować, takie sytuacje mogą się zdarzać się z różną częstotliwością.
Tradycyjne środowiska programistyczne nie są w tym celu zbyt pomocne, gdyż stworzenie w nich nawet prostego programu wymaga ustawienia przynajmniej kilku opcji, stworzenia katalogu na kilkanaście wygenerowanych przy okazji plików – i tak dalej. Zamiast sedna musimy więc skupić się na nieistotnych szczegółach.
Kiedyś pomyślałem sobie, że dobrze byłoby mieć takie proste środowisko, w którym można by było oszczędzić sobie całego tego zachodu. Gdzie nie trzeba przejmować się takimi sprawami, jak kompilacja, wejście od użytkownika, ustawienie trybu graficznego , właściwa konstrukcja głównej pętli aplikacji, itp.
W zamierzchłych i na szczęście dawno minionych czasach namiastką czegoś takiego było środowisko dla “języka” LOGO, którym to niekiedy zamęczano biedne dzieci w gimnazjach :) W czasach nieco bliższych powstały interpretowane języki programowania z linią poleceń – jak na przykład Python – mogące służyć do wspomnianej ‘niezobowiązującej zabawy’. Fachowo nazywa się ją prototypowaniem.
Ostatnio zaś znalazłem (za sprawą noogi z kanału #warsztat) coś, co wydaje się o wiele bliższe ideałowi. To open source’owe środowisko o ambitnej nazwie Processing, mające nieporównywalnie większe możliwości niż te wspomniane wyżej. Język programowania w nim zawarty jest bardzo przyjemny i ma “normalną” składnię z wyodrębnionymi blokami kodu – jak C(++/#), PHP czy Java – i możliwości zbliżone do ww. języków. Programy napisane w oparciu o Processing są krótkie i zwięzłe, bowiem większość potrzebnych narzędzi, takich jak inicjalizacja grafiki (opartej o OpenGL) czy podpięcie wejścia klawiatury i myszy, zostało już zintegrowanych. Naszym zadaniem jest tylko wypełnienie treścią dość oczywistych funkcji jak
draw
, setup
czy mousepressed
; niekoniecznie zresztą wszystkich.
Od strony technicznej Processing też prezentuje się całkiem znośnie. Jego IDE ma aczkolwiek jeden bolesny mankament: jest napisane w Javie i wszyscy wiemy, co to oznacza dla szybkości jego uruchamiania :) Cały system jest zresztą oparty właśnie o Javę, ale to staje się akurat bardziej zaletą niż wadą. Jedną z nich jest ładny język programowania, a inną to choćby możliwość bezbolesnego eksportu napisanych programów do postaci zwykłych plików wykonywalnych dla różnych systemów operacyjnych. Można też tworzyć z nich aplety do umieszczenia na stronach WWW, co pokazuje choćby ten atraktor Lorenza.
Aplikacje wygenerowane w ten sposób na pewno nie zastąpią tych pisanych z użyciem prawdziwych kompilatorów, ale podejrzewam, że nie taki jest cel. Chodzi przede wszystkim o tę ‘piaskownicę’, w której można bezboleśnie wypróbowywać nowe idee.
Nikt nie jest nieomylny – zwłaszcza jeżeli chodzi o programistów. Błędy w kodzie zawsze były, są i będą się pojawiały z mniejszą lub większą regularnością. Najważniejsze więc to umieć sobie z nimi radzić. Według mnie jest to tak samo ważna umiejętność jak posługiwanie się jakimś językiem programowania czy biblioteką graficzną. Tyle że jest ona o wiele przydatniejsza, bo znacznie bardziej uniwersalna.
Często – przede wszystkim w firmach tworzących oprogramowanie – usuwaniem błędów zajmują się wyspecjalizowane osoby, czyli testerzy. Jak wiadomo w każdym programie jest przynajmniej jeszcze jeden błąd, a przy użyciu odpowiednich technik można wyłapać przynajmniej te, z którymi miałaby szansę zetknąć się przynajmniej pewna część końcowych użytkowników.
Tego rodzaju błędy muszą być zwykle specjalnie wyszukiwane, jednak na co dzień koderzy spotykają się z takimi, które same ‘znajdują’ programistów. Mówiać wprost, często (o wiele za często) zdarza się, że po prostu coś nie działa – albo nie działa tak, jak powinno.
Co wtedy zrobić? Osobiście stosuję poniższe metody. Uszeregowałem je w kolejności od najmniej do najbardziej drastycznej, a jednocześnie od najmniej do najbardziej skutecznej. Kryteriami rosnącymi w dół listy jest także czasochłonność oraz stopień desperacji programisty :) A rzeczone sposoby są następujące:
x
zamiast y
.Jeżeli wszystko zawodzi, pozostaje jeszcze ostatnia deska ratunku, czyli szukanie pomocy z innych źródeł, np. na forach. Z praktyki widać jednak, że często preferowana kolejność postępowania jest dokładnie odwrotna. A to na dłuższą metę nie jest to rozsądne, ponieważ osobą najlepiej przygotowaną do znalezienia błędu w kodzie jest sam jego autor.
Świat były aczkolwiek o wiele piękniejszy, gdyby konieczność takich poszukiwań nie zdarzała się zbyt często :)
Co można powiedzieć o czymś tak prozaicznym, jak zwykły przycisk? Ano to, że przycisk jest po to, aby go wciskać :) Od strony użytkownika wygląda to więc bardzo prosto – może nawet zbyt prosto, co czasami kończy się źle, jeśli nie czytamy komunikatów przed pochopnym wciśnięciem OK.
Kwestia oprogramowania takiego tworu jak przycisk, aby zachowywał się zgodnie z oczekiwaniami, jest już jednak trochę trudniejsza.
Przy okazji przycisku wychodzi bowiem kwestia tak zwanego mouse capture, które to pojęcie nie ma dobrego tłumaczenia na język polski. Pozwala ono użytkownikowi rozmyślić się i nie dokonać kliknięcia nawet wtedy, gdy zdążył już wcisnąć przycisk myszki. Wystarczy, że – nie puszczając go – odjedzie kursorem poza obszar kontrolki i tam puści przycisk myszy. Wówczas zarówno zdarzenie wciśnięcia, jak i puszczenia przycisku myszy będzie zarejestrowane, lecz to najważniejsze – kliknięcia – już nie.
Ten dość abstrakcyjny byt mouse capture może być przez kontrolkę posiadany lub nie. Jeżeli kontrolka go posiada, wtedy otrzyma ona informacje o zdarzeniach myszy nawet gdy kursor znajduje się poza jej obszarem. Dzięki temu kontrolka przycisku może być poinformowana o tym, że użytkownik zrezygnował z jej wciśnięcia.
Pozostaje jeszcze drobna kwestia graficzna. Otóż w Windows kontrolka przycisku “wyciska się”, jeżeli odjedziemy kursorem poza jej obszar (oczywiście cały czas trzymając wciśnięty przycisk myszki). Kiedy znów wrócimy, wciśnie się ponownie, i tak dalej (ciekawym zalecam własnoręczne eksperymenty ;]). Osobiście nie sądzę, żeby takie zachowanie było bardzo intuicyjne, bo chyba bardziej by mi odpowiadało, gdyby kontrolka pozostała cały czas wciśnięta.
Niestety, nie ja ustanawiam standardy interfejsu, więc postanowiłem się dostosować i zaimplementować windowsowe rozwiązanie :)
W ramach kontynuacji przeglądu nietypowych konstrukcji językowych – który to nieopatrznie rozpocząłem, zajmując się pętlami w Pythonie – obejrzymy sobie jeden z elementów języka Object Pascal. Są to referencje do klas, zwane też czasem metaklasami.
Ten dziwny twór działa jak odwołanie wskazujące na klasę jako typ, a nie na jej konkretny obiekt. Deklaruje się go mniej więcej w taki sposób:
[delphi]type TClass = class of TObject;[/delphi]
Zmienne należące do tak zdefiniowanego typu TClass
mogą pokazywać na wszystkie klasy dziedziczące po TObject
. Innymi słowy, takie zmienne są swego rodzaju dynamicznymi aliasami na nazwy klas; używając ich, nie musimy nawet wiedzieć, z jakiego typu klasą konkretną mamy do czynienia. Przypomina to oczywiście normalny dynamiczny polimorfizm obiektów, osiągany przy pomocy funkcji wirtualnych. Tutaj jest to niejako dynamiczny polimorfizm samych klas.
Użycie takiego typu referencyjnego może wyglądać choćby tak:
[delphi]type TFoo = class(TObject) // klasa dziedzicząca po TObject
// …
end;
var
AnyClass : TClass; // zmienna będąca referencją do klasy
AnyObject : TObject; // zwykłe odwołanie do obiektu
begin
AnyClass := TFoo; // referencja pokazuje na klasę TFoo
AnyObject := AnyClass.Create; // tworzy obiekt klas TFoo przy pomocy referencji
end;[/delphi]
Ten przykład pokazuje, że w Delphi przy pomocy referencji do klas możliwe jest łatwe zrealizowanie wzorca wirtualnego konstruktora. Nie musimy bowiem wiedzieć, na jaką klasę wskazuje referencja, a utworzony obiekt możemy “odebrać” posługując się zmienną typu bazowego (tutaj TObject
).
Co na to C++? Nie ma tam naturalnie podobnej konstrukcji. Zbliżone do niej – w sensie możliwości korzystania z jakiegoś typu bez wiedzy, czym on naprawdę jest – są parametry szablonów. Podstawowa różnica polega jednak na tym, że szablony są rozwijane w trakcie kompilacji i “wartości” tych parametrów są niezmienne.
Nie wiem, czy można w jakiś sposób zaimplementować w C++ metaklasy o funkcjonalności zbliżonej do powyższej. Znając możliwości C++, to całkiem prawdopodobne :) Ich ewentualny brak nie były jednak jakoś szczególnie dotkliwy, gdyż większość ich zastosowań z powodzeniem daje się zastąpić szablonami lub zwykłymi funkcjami wirtualnymi.
Dla odmiany zająłem się ostatnio czymś nieco prostszym niż GUI, a mianowicie systemem logującym. To zdecydowanie niezbędne narzędzie, które docenia się zwłaszcza wtedy, kiedy coś innego psuje. Jak dotąd aczkolwiek nic poważnego nie zdążyło się jeszcze, ale w programowaniu jest to, jak wiadomo, tylko kwestią czasu ;)
Do logowania istnieje mnóstwo podejść, które różnią się głównie tym, gdzie i jak zapisujemy nasze informacje. Bo jeśli o to, co mamy logować, to odpowiedź jest prosta: wszystko albo jak najwięcej.
Dość obszerny przegląd możliwych wyjść loggera przedstawił TeMPOraL w jednej swoich notek. Jakkolwiek długa ta lista by nie była, pewne jest jedno: na pewno nie może być ona zapisana “na sztywno” w kodzie głównej klasy systemu logującego. Same ‘wyjścia’ należy bowiem opakować w osobne klasy, wywodzące się ze wspólnej bazy i wykorzystać zalety polimorfizmu. W ten sposób można kierować komunikaty zarówno na konsolę, do zwykłego pliku tekstowego, jak i do funkcji OutputDebugString
.
Pomyślałem jednak nad rozszerzeniem tego pomysłu, który wyraża się w tytułowym równaniu: wyjście = format + ujście. Postanowiłem oto podzielić wyjście logowania na dwie części:
Nie jest to oczywiście żadna rewolucja. Czasami też obie części muszą być do siebie ściśle dopasowane, aby osiągnąć pożądany efekt (np. wypisywanie kolorowych komunikatów w asynchronicznej konsoli Windows). Zwykle jednak można je zestawiać dowolnie i otrzymywać ciekawe rozwiązania. Najważniejsze, że tą dodatkową elastyczność da się osiągnąć małym kosztem.
Programy rezydujące w zasobniku systemowym (system tray) są zawsze wyposażone w menu kontekstowe. Zwykle pojawia się ono po kliknięciu prawym przyciskiem myszy na ikonkę aplikacji i zawiera najczęściej używane opcje programu – co czasem oznacza też “wszystkie opcje programu” :)
Czasami jednak to menu jest uparte i jeśli nie zdecydujemy się na wybranie żadnej pozycji, ono pozostaje otwarte nawet mimo tego, że już dawno zajęliśmy się czymś innym. Zupełnie jakby Windows “zagapił się” i przeoczył moment, gdy menu utraciło fokus (wtedy właśnie powinno było zniknąć). Bywa to o tyle denerwujące, że takie osierocone menu przykrywa wszystkie inne okna w systemie.
Niekiedy pomaga wtedy ponowne kliknięcie w ikonkę programu. Jeżeli jednak to nie działa, trzeba jakoś przywrócić feralnemu menu utracony fokus, aby mogło go ponownie stracić – i, miejmy nadzieję, zniknąć na dobre. Jak to zrobić bez uaktywniania żadnej pozycji menu? Są co najmniej dwa sposoby:
Kiedy następnie klikniemy w jakiekolwiek miejsce poza menu, oporna lista powinna się w końcu schować.