Posts tagged ‘errors’

Chilijskie łososie mają anemię

2010-02-21 15:00

Źródło: Glenn Oliver/Visuals Unlimited/Getty Images

Tak, to stuprocentowa prawda – większość tych ryb w hodowlach w Chile jest dotknięta tą chorobą. Efektem tego będzie na pewno wzrost cen łososia również w sklepach.
No dobrze, ale co z tego – a dokładniej, czemu o tym piszę tutaj (bo fakt, że lubię dania z tych ryb pewnie nie jest wystarczającym uzasadnieniem ;])?… Otóż tytuł tej notki to doskonały przykład informacji, która jest prawdziwa, dokładna, a jednocześnie całkiem nieprzydatna i wywołująca tylko zdziwienie u odbiorcy.

To zupełnie tak, jak z niektórymi… komunikatami o błędach, produkowanymi przez kompilator C++ pracujący w ramach Visual Studio. Z punktu widzenia analizy kodu przez tenże kompilator mają one sens, jednak dla czytającego je programisty często mówią tyle co nic.
Z czasem aczkolwiek można nabrać wprawy i zacząć domyślać się, co tak naprawdę kompilator “miał na myśli”, produkując tę czy inną wiadomość o błędzie. To jednak wciąż wybitnie niepraktyczne i dlatego postaram się dzisiaj pomóc w tej kwestii, wyjaśniając prawdziwe znaczenie niektórych komunikatów wypluwanych przez Visual C++:

  • error C2146: syntax error : missing ‘;’. Czasami powodem tego błędu jest faktycznie brak średnika (np. na końcu definicji klasy). Często jednak pojawia się w – na oko – poprawnych deklaracjach zmiennych. Wtedy przyczyną jest nieznajomość typu tej zmiennej, więc pośrednio jest to brak dołączonego nagłówka, literówka w deklaracji tego typu, itp.
  • error C2181: illegal else without matching if. Pozornie “luźny” else to często efekt postawienia o jednego średnika za dużo – mianowicie średnika po bloku if. Jeśli używamy makr do zastępowania powtarzających się fragmentów kodu, to może tu chodzić o niepoprawne zdefiniowanie takiego makra.
  • error C2301: left of ‘->foo’ must point to class/struct/union. Ten i podobne błędy: C2302, C2510 oznaczają tyle, że kompilator nie rozpoznaje nazwy stojącej przed operatorem wyłuskania ->, . (kropką) lub ::. Niekoniecznie musi to znaczyć, że została ona źle zadeklarowana (i nie jest klasą/strukturą/unią) – najczęściej po prostu nie została zadeklarowana w ogóle.
  • error C2360: initialization of ‘foo’ is skipped by ‘case’ label. Ten komunikat (oraz analogiczny C2361) jest spowodowany brakiem nawiasów klamrowych otaczających bardziej skomplikowane kawałki kodu zawarte w poszczególnych przypadkach instrukcji switch (takie, które zawierają deklaracje nowych zmiennych). Bardzo podobny błąd dotyczy też używania etykiet i instrukcji goto, które, jak wiadomo, są złem potwornym (w tym przypadku żartuję oczywiście).
  • error C2440: ‘conversion’ : cannot convert from ‘Foo’ to ‘Bar’. Jeśli któryś z występujących w komunikacie (lub podobnym C2446) typów nie jest typem wbudowanym, to najpewniej brak tutaj dołączenia jakiegoś nagłówka, który daną konwersję by definiował. Inna możliwa przyczyna to przypuszczenie, że kompilator będzie na tyle sprytny i zastosuje dwie konwersje przez jakiś typ pośredni; niestety tak nie jest – naraz może być stosowana tylko jedna niejawna konwersja zdefiniowana przez programistę.
  • error C2512: ‘Foo’ : no appropriate default constructor available. Błąd ten może naprawdę zbić z tropu, o ile nie wiemy dokładnie, co jest jego przyczyną. Żeby się objawił, nie potrzeba tworzyć żadnych obiektów – wystarczy odziedziczyć po klasie, która nie ma domyślnego konstruktora. W takiej sytuacji musimy w każdym konstruktorze klasy potomnej zapewnić wywołanie konstruktora klasy bazowej (na liście inicjalizacyjnej) – albo po prostu dodać ten brakujący konstruktor domyślny w klasie bazowej.
  • error C2621: member ‘Foo::Bar’ of union ‘Foo’ has copy constructor. Typowym scenariuszem objawienia się tego błędu jest próba stworzenia unii zawierającej pole typu (w)string. Nie jest to możliwe – ze względu na obecność konstruktora kopiującego – i choć teoretycznie możliwe jest obejście tego faktu, stosowanie go jest bardzo, bardzo złym pomysłem. Lepiej po prostu przymknąć oko na “marnotrawstwo pamięci” i zmienić unię w strukturę.

Nie jest oczywiście możliwe wyliczenie wszystkich okoliczności, w których może objawić się każdy możliwy błąd. Powyższa lista jest więc trochę subiektywna w tym sensie, że zawiera tylko te pozycje, które przytrafiły mi się osobiście podczas kodowania. Bardziej doświadczeni programiści pewnie mogliby ją znacznie poszerzyć.

Tags: , ,
Author: Xion, posted under Programming » 2 comments

Indeksowanie od tyłu

2010-01-12 20:58

Pewna programistyczna mądrość ludowa uczy, że jedynymi liczbami bezpośrednio występującymi w kodzie powinny być tylko 0 lub 1. Brak lub nadmiar tej drugiej jest przy tym często pojawiającym się błędem, który zyskał swoją własną nazwę pomyłek o jedynkę (off-by-one errors).

Okazji do popełnienia tego rodzaju gaf jest sporo, a objawiają się one przede wszystkim wtedy, gdy mamy do czynienia z tablicami, łańcuchami znaków, kolekcjami, itd. – innymi słowy wszystkim, co da się indeksować. Jako rzecze C++ i większość innych “normalnych” języków, indeksy tablic rozpoczynają się od zera. Startowanie ich od jedynki ma aczkolwiek pewną zaletę: pętle przeglądające zawartość tablic mają wtedy prawie identyczne postaci niezależnie od kierunku przeglądania:

  1. for (int i = 1; i <= n; ++i) // w przód
  2. for (int i = n; i >= 1; --i) // w tył

W przypadku indeksowania od zera pętla odliczająca wstecz jest wyraźnie różna:

  1. for (int i = 0; i < n; ++i) // w przód
  2. for (int i = n - 1; i >= 0; --i) // w tył

Pisząc ją, trzeba więc zwrócić baczniejszą uwagę na to, co się robi.

Jednak nawet wtedy można potknąć się o pewien kruczek, gdy pod n podstawimy rzeczywiście używane wartości. Niech to będzie np. wielkość kolekcji STL czy długość łańcucha znaków std::string – a więc coś w stylu x.size(). Otóż takie wyrażenie zwraca liczbę typu równoważnego size_t, który z kolei jest równy ni mniej, ni więcej, jak tylko unsigned int. Jest to więc liczba bez znaku.
W zwykłej wersji pętli (liczonej do przodu) powoduje to ostrzeżenie kompilatora o niezgodności typów ze znakiem i bez znaku (signed/unsigned mismatch) w warunku i < x.size(), gdy i jest typu int. Jednym ze sposobów na pozbycie się tego warninga jest oczywiście zamiana typu licznika na size_t. Jeśli teraz mamy wystarczająco zły dzień, to przez analogię będziemy licznikiem tego typu indeksować również pętlę odwrotną:

  1. for (size_t i = x.size() - 1; i >= 0; --i) // no i zonk

I nieszczęście gotowe. Zauważmy bowiem, że odliczanie do tyłu tablicy/kolekcji indeksowanej od zera wymaga, by na koniec licznik przyjął na koniec wartość ujemną; inaczej warunek i >= 0 nigdy nie stanie się fałszywy. To się jednak nigdy nie stanie, gdy i jest liczbą bez znaku; zamiast tego nastąpi przekręcenie na maksymalną wartość dodatnią. Skutkiem będzie pętla nieskończona lub (częściej) access violation.

Co z tego wynika? Ano to, żeby… indeksowania generalnie unikać :) Po to bowiem zarówno STL, jak i każda inna biblioteka pojemników w każdym sensownym języku ma inne sposoby dostępu do elementów – choćby iteratory – aby z nich korzystać. I unikać takich “ciekawych” błędów jak powyższy :)

Tags: , , ,
Author: Xion, posted under Programming » 6 comments

Asercje, wyjątki i inne błędy

2008-07-16 17:09

W każdym programie większym niż Hello World istnieje możliwość wystąpienia błędów w czasie działania. Dotyczy to zwłaszcza takich, które nie są zależne od programisty piszącego kod aplikacji, lecz na przykład od danych zewnętrznych pochodzących od użytkownika czy z plików.
W zależności od typu i stopnia dolegliwości błędy mogą być obsługiwane na różne sposoby. Dość często nie jest wcale łatwo zdecydować się na któryś z nich. Dlatego należy znać typowe metody sygnalizowania błędów i przynajmniej ogólne zasady opisujące sytuacje, w których każda z tych metod jest najwłaściwsza.

Sam przez bardzo długi czas miałem na przykład pewne wątpliwości co do przydatności asercji jako mechanizmu powiadamiania o błędach. W szczególności, nie potrafiłem odróżnić sytuacji, w których właściwsze jest korzystanie właśnie z asercji niż chociażby z wyjątków. Ostatnio aczkolwiek całkiem przypadkowo nadrobiłem te niechlubne zaległości :)
Okazuje się bowiem, że różnica między asercją a wyjątkiem jest znaczna. Asercje służą do sprawdzania pewnych warunków, które uznajemy za obiektywnie prawdziwe. Są to po prostu założenia, które muszą być spełnione w każdym okolicznościach, gdyż warunkują poprawność kodu. Pisanie asercji jest więc częściowo sprawdzaniem samego siebie: dzięki nim łatwiej wykryjemy błędy programistyczne we wczesnej fazie powstawania kodu (czyniąc naturalnie optymistyczne założenie, że same asercje są w porządku :]).

Wniosek z tego taki, że w dobrze działającej aplikacji asercje zawsze powinny być spełnione i nigdy nie przerywać działania programu. W przeciwieństwie do nich wyjątki (exceptions) mogą pojawiać się w zupełnie poprawnym kodzie, bo dotyczą rzeczy, na które program nie ma wpływu. Jednocześnie jednak musi on przewidywać ich ewentualne wystąpienie i posiadać odpowiedni kod ich obsługi. W przeciwnym razie konsekwencje bywają nieprzyjemne.
Dlatego wyjątki (i podobne do nich mechanizmy, jak np. symbianowe leaves) służą do powiadamiania o sytuacjach mających potencjalnie poważne konsekwencje. Dla pozostałych należy stosować mniej radykalne metody informowania o błędach. Najpopularniejszym jest oczywiście wartość zwracana przez funkcję, w której wystąpił błąd (ewentualnie w połączeniu z czymś takim jak errno lub GetLastError w Windows API). Kluczową cechą tego sposobu powiadamiania jest to, że należy specjalnie zadbać o sprawdzenie, czy błąd wystąpił; inaczej zostanie on zignorowany.

Tak w skrócie przedstawia się sprawa od strony teoretycznej. W praktyce odróżnienie sytuacji podpadającej pod asercję od tej, gdzie uprawniony jest wyjątek – a już zwłaszcza wyjątku od zwykłego “return -1;” – bywa trudne, a często subiektywne. Zwykle jednak daje się stosować przynajmniej jedną zasadę: przynajmniej w fazie testów lepiej jest przesadzać i reagować nazbyt histerycznie niż przeoczyć lub zignorować jakiś ważny szczegół.

Tags: , ,
Author: Xion, posted under Programming » 1 comment

Jak nie należy używać operatorów

2008-04-30 22:23

W wielu API funkcje mają bardzo prosty sposób powiadamiania o tym, czy ich wykonanie zakończyło się sukcesem czy porażką. Albo więc wykorzystują typ bool bezpośrednio, albo wpasowują się w konwencję, iż niezerowa wartość liczbowa jest tożsama z prawdą, a zero z fałszem. To sprawia, że możliwe jest pisanie warunków podobnych do poniższego:

  1. if (!RegisterClassEx(&wc))    Error("Can't register window class.");

Ładne to i opisowe – wręcz samodokumentujące się. Ale czasami tak się zrobić nie da, bo wartości zwracane nie chcą współpracować z tym modelem.

Przykład? To większość biblioteki runtime języka C oraz API systemów uniksowych. O ile tylko rezultatem funkcji należącej do któregoś z tych dwóch zbiorów nie jest wskaźnik, konwencja informowania o powodzeniu lub niepowodzeniu jest zwykle dość osobliwa. Według niej zero oznacza sukces, natomiast porażka wykonania jest sygnalizowana przez wartość mniejszą od zera – zazwyczaj -1. Oczywiście nijak nie pasuje to sposobu interpretowania liczb jako wartości logicznych. Sprawia to, że sprawdzanie rezultatu takich funkcji może wyglądać cokolwiek enigmatycznie:

  1. if (close(fd) < 0)    perror("closing file");&#91;/cpp]
  2. Ale nie wszystko stracone :) W przypadku funkcji typu boolowskiego możemy posłużyć się operatorem logicznej negacji (<code>!</code>), dzięki czemu zawierające je <code>if</code>y są całkiem przejrzyste. Okazuje się, że z powodu pewnego zbiegu okoliczności także te wspomniane przed chwilą funkcje można potraktować tak samo... o ile dodamy jeszcze jeden operator. A dokładniej - jeśli oprócz negacji logicznej dodamy też bitową (<code>~</code>):
  3. [cpp]if (!~close(fd))    perror("closing file");

Powód, dla którego to działa, jest dość prosty. W standardowym sposobie zapisu liczb całkowitych, stosowanym na zdecydowanej większości typowych i nietypowych maszyn (zwanym uzupełnieniem do 2 – U2), wartość -1 to w zapisie binarnym same jedynki. Negując je bitowo, otrzymujemy same zera – czyli zero, a więc logiczny fałsz. A odwrotnością fałszu jest oczywiście prawda i wszystko działa poprawnie. Wygląda więc tak, jakby skromna tylda zdołała “naprawić” funkcję, by zachowywała się zgodnie z oczekiwaniami…

Tylko czy aby na pewno nowy zapis jest bardziej sugestywny? Mam nadzieję, że każdy potrafi poprawnie odpowiedzieć na to pytanie we własnym zakresie :) Na koniec jednak muszę – dla spokoju sumienia – ostrzec wszystkich: zdecydowanie nie róbcie tego w domu :D

Tags: , ,
Author: Xion, posted under Programming » 4 comments

Błędne błędy

2008-02-19 19:47

Przeglądając ostatnio kod swojego… nazwijmy to umownie: silnika (;P), zauważyłem pewne dość ciekawe rozwiązanie. Jednak w tym przypadku ‘ciekawe’ znaczy mniej więcej tyle, co ‘zastanawiające’, ‘wątpliwe’ i ‘cokolwiek dziwne’. Chodzi o obsługę przeróżnych błędów czasu wykonania – czyli wyjątków – a w szczególności o ich rodzaje, wyróżnione w postaci klas obiektów reprezentujących rzeczone błędy.
Nie wiedzieć czemu podzieliłem je bowiem ze względu na źródło błędów. Zaowocowało to klasami wyjątków pochodzących od Windows API, od DirectX czy wreszcie takich, których źródło tkwiło w samym kodzie silnika. Oczywiście taki obiekt wyjątku miał też jakieś dodatkowe dane, co w najprostszej wersji ograniczało się po prostu do tekstowego komunikatu o tym, co się stało.

Nie waham się stwierdzić, że taka organizacja klas wyjątków to pomysł po prostu poroniony :) Na etapie obsługi błędów o wiele bardziej interesuje nas bowiem ich przyczyna, która powinna być określona jak najdokładniej. Stwierdzenie, że mamy do czynienia np. z błędną wartością argumentu naszej funkcji, mówi o wiele więcej niż fakt, że wartość ta spowodowała dalej błąd przy korzystaniu z jakiejś zewnętrznej biblioteki – na przykład DirectX. Różnica polega chociażby na tym, że dysponując klasami wyjątków w rodzaju InvalidArgumentException zamiast WindowsAPIError możemy wcześniej sygnalizować nieprawidłowe sytuacje. Ich obsługa jest też wygodniejsza, gdyż duża liczba klas wyjątków ułatwia oddzielanie od siebie fragmentów odpowiedzialnych za radzenie sobie z różnymi typami błędów.

Morał jest więc prosty: ważniejsze jest to, co konkretnie poszło źle – nie zaś to, gdzie rzecz się stała. Świadczą o tym także wyjątki spotykane w bibliotekach pokroju STL, .NET czy JDK. Wszędzie tam powyższa zasada jest nadrzędna.

Tags: ,
Author: Xion, posted under Programming » 1 comment

‘No i co tu jest źle?’ w wersji książkowej

2008-02-02 17:57

Kiedy ma się dość pokaźną biblioteczkę książek, czasami natrafia się na pozycję, która nie wiadomo skąd się w niej wzięła. Pytanie takie staje się tym bardziej uzasadnione, gdy rzeczoną książkę otworzymy, przekartkujemy i pobieżnie przeglądniemy, by po krótkim czasie uznać, że najchętniej widzielibyśmy ją w… punkcie skupu makulatury :P
Okładka książki “Jak NIE programować w C++”Coś takiego przytrafiło mi się zupełnie niedawno. Książką o której mam taką “pochlebną” opinię, jest dziełko opatrzone tytułem Jak NIE programować w C++. Jeszcze ciekawszy jest chyba podtytuł, który mówi, że wewnątrz znajdziemy dokładnie 111 programów z błędami oraz trzy działające. Statystyka jest imponująca, ale o co tak naprawdę tutaj chodzi?… Autor przedstawia nam mianowicie coś w rodzaju zbioru koderskich zagadek do samodzielnego rozwiązania, które polegają oczywiście na znalezieniu błędu w przedstawionym kawałku kodu.

Jak podejrzewam, celem tej książki w zamyśle autora było ustrzeżenie programistów C++ przez różnego rodzaju typowymi błędami, jakie mogą się zakraść do pisanego przez nich kodu. Cel to chwalebny, chociaż dość utopijny – w końcu nie wystarczy wiedzieć, na czym błąd polega, aby w magiczny sposób już nigdy więcej go nie popełnić. Trochę więcej wątpliwości mam natomiast co do obranej metody. Nie wiem, w jaki sposób obejrzenie ponad stu błędnych kodów ma sprawić, że będziemy częściej pisali poprawny kod. Spodziewałbym się raczej podświadomego nauczenia się prezentowanych tam złych przykładów i ich spontanicznego stosowania w rzeczywistych programach, co raczej nie ułatwiłoby nikomu pracy :)
Byłoby oczywiście świetnie, gdyby przykłady te były jedynymi niepoprawnymi kawałkami kodów, z którymi przychodzi nam się mierzyć. Ale przecież jest to odległe od prawdy o całe lata świetlne. Jako koderzy sami nieuchronnie produkujemy niepoprawny kod, który co rusz musimy korygować. Prawdopodobnie więc nie potrzebujemy dodatkowych łamigłówek tego rodzaju, bo wystarczą nam te, które w nieumyślny sposób tworzymy sami dla siebie. I niestety nie możemy w ich przypadku – w przeciwieństwie do kodów z książki – zajrzeć do części końcowej po wskazówki i odpowiedzi.
Jednak nawet wobec takich mankamentów, prezentowane w książce przykłady mogłyby mieć pewną wartość poznawczą. Rzecz w tym, że naprawdę interesujące zagadki można bez trudu policzyć na palcach obu rąk. Pozostałe są albo pomyłkami aż do bólu klasycznymi (na czele z pomyleniem operatora przypisania i równości w warunku logicznym), albo trywialnymi literówkami, albo świadectwami na – delikatnie mówiąc – niekompletną znajomość języka. (Moim faworytem jest deklaracja int n = 1,000,000;, w założeniu inicjująca zmienną wartością równą milion).

Aż chce się zapytać, czy są w tej książce jakieś cechy pozytywne. Odpowiadam prędko, że jak najbardziej, tyle że mają się one nijak do jej zasadniczej treści. Do każdej zagadki autor dołączył bowiem krótką, zabawną historyjkę “z życia wziętą” lub inny śmieszny tekst – wszystko oczywiście z dziedziny IT. Paradoksalnie więc ta “książka o C++” lepiej sprawdza się jako książka z dowcipami.
Jest też druga dobra strona. Przypomniałem sobie mianowicie, skąd u mnie wzięła się ta dziwna pozycja. Otóż zakupiłem ją podczas jakichś tragów wydawniczych, które akurat odbywały się na uczelni, zapłaciwszy za nią około dziesięć złotych. Nietrudno przeboleć taką niewielką sumę, nawet mimo tego, że nikomu nie poleciłbym wydania na tę książkę nawet jednej złotówki :)

Tags: ,
Author: Xion, posted under Books, Programming » 4 comments

“Nie działa! Jejku, co ja zrobię?!”

2007-08-08 16:10

Na forum Warsztatu zdarzają się różne problemy. Część z nich dotyczy nieznanych przyczyn błędnego funkcjonowania programu lub jakiegoś kawałka kodu. Z pewnością nie jest tak, że takie wątki są z góry uznawane za niepożądane. To, co o tym decyduje, to przede wszystkim treść, opisowość i precyzja.

A z tym bywa kiepsko. Bardziej doświadczeni programiści wiedzą oczywiście, że do wyeliminowania błędu potrzebna jest dokładna wiedza, w jakich okolicznościach on występuje. A już zupełnie niezbędne jest określenie, co tak naprawdę się dzieje: błędny rezultat funkcji, wyjątek czasu wykonania, zawieszenie się programu, bluescreen, spalenie płyty głównej (no, może przesadzam ;)) ?…
Nierzadko jednak za cały opis ma wystarczać mgliste stwierdzenie, że coś nie działa. “Serio?” – chce się odpowiedzieć – “więc idź i to napraw ;P”. Przy tak skąpo opisanych objawach trudno przecież oczekiwać, żeby ktokolwiek mógł wywróżyć, co tak naprawdę jest ich przyczyną.

Czasem ta lakoniczność jest spowodowana tym, że dana osoba traktuje fakt niedziałania napisanego przez siebie kodu wręcz jako życiowe niepowodzenie lub – co gorsza – osobistą zniewagę. A gdy w grę wchodzą takie emocje, z zebraniem potrzebnych informacji może być kłopot…
Próbuję się tu wczuć w taką postawę, ale prawdę mówiąc zupełnie jej nie rozumiem. Może każdy na początku przygody z programowaniem reaguje podobnie, a ja zdążyłem już po prostu zapomnieć, że kiedyś mi się to zdarzało? A może jednak zależy to od charakteru i sprawia, że osoby biorące wszelkie niepowodzenia (w tym przypadku błędy) za bardzo do siebie mają trudności w zostaniu dobrymi programistami?

I czy faktycznie podejście emocjonalne tak bardzo przeszkadza?… Nie wiem, jak w jest w istocie, lecz wiem jedno: ostatnio zdecydowanie za często zajmuję się dziwnymi problemami :)

Tags: ,
Author: Xion, posted under Programming, Thoughts » 6 comments
 


© 2023 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.