To, czego oczekuje kompilator

2008-08-19 12:53

Język C++ jest tworem skomplikowanym i jego złożoność daje się we znaki nie tylko programistom. Dość często mają z nią problemy także kompilatory. Obecnie zdecydowana większość narzędzi tego typu spełnia prawie całkowicie wymagania aktualnego standardu. Istnieje jednak sporo miejsca na usprawnienia, chociażby w bodaj najbardziej niedopracowanej dziedzinie: jakości komunikatów o błędach generowanych przez kompilatory.
Jeśli na przykład trafi nam się taka deklaracja zmiennej:

  1. SomeType variable;

a typ SomeType z jakiegoś powodu (niedołączony nagłówek, kwestia zasięgu, itd.) będzie w jej miejscu nieznany, to treść wyprodukowanego przy okazji komunikatu o błędzie może nas trochę zaskoczyć. Zarówno CL (kompilator Microsoftu używany w Visual C++), jak i GCC wyplują bowiem coś w stylu:

error: expected ‘;’ before ‘variable’

To znaczy ni mniej, ni więcej, tylko to, że według tych kompilatorów właściwa jest “deklaracja”:

  1. SomeType;

Dość zaskakujące, nieprawdaż?

Takie zachowanie można częściowo wyjaśnić tym, że kompilator “nie patrzy” na kod tak, jak programista. My widzimy tutaj deklarację zmiennej, natomiast dla kompilatora jest to tylko sekwencja: ‘nieznany identyfikator’, po którym następuje kolejny ‘nieznany identyfikator’. Ponieważ mówi się, że C++ jest językiem kontekstowym (co swoją drogą nie jest do końca ścisłe), taki fragment może sugerować wiele różnych rodzajów pomyłek. To, co sugeruje kompilator, wbrew pozorom też pewnie ma sens, chociaż dość specyficzny. Postawienie średnika w “spodziewanym” miejscu sprawi bowiem, że zamiast dwóch nieznanych nazw w jednej instrukcji będziemy mieli tylko jedną. Cóż za postęp! :)
Na tym prostym przykładzie widać, że interpretacja wyników kompilacji zakończonej niepowodzeniem wymaga niestety pewnego doświadczenia. Dopiero po jakimś czasie nauczymy się trafnie(j) zgadywać, co tak naprawdę oznacza ten czy inny błąd. Najwyraźniej taki już urok C++…

Tags:
Author: Xion, posted under Applications, Programming »


12 comments for post “To, czego oczekuje kompilator”.
  1. reVis:
    August 19th, 2008 o 13:10

    Ale wniosek i tak został jeden. Praktyka, praktyka, praktyka… ;)

  2. Gynvael:
    August 19th, 2008 o 13:29

    Popieram… jakość komunikatów pozostawia duużo do życzenia.
    Szczególny ‘ubaw’ jest jeżeli zapomni się ‘gdzieś na górze’ pliku średnika – gcc/g++ w takim wypadku mają zwyczaj wyrzucać kilka ekranów errorów i warningów – a chodzi o głupi średnik (a konkretniej jego brak)…
    Cóż, jak reVis napisał.. praktyka, praktyka, praktyka ;D

  3. polymorph:
    August 19th, 2008 o 13:51

    Ta, a najlepiej jak się zapomni średnika po definicji klasy… jak pierwszy raz spotkałem się z tym błędem i to jeszcze w całkiem sporym projekcie to zajęło mi prawie tydzień żeby go zlokalizować. Teraz w sumie tez się czasami zdarza że potrzebuje nawet 0,5h żeby pokojarzyć.

  4. lmmilewski:
    August 19th, 2008 o 14:52

    Nie wiem jak to kompilowałeś. U mnie program:
    SomeType variable;
    int main() {
    return 0;
    }

    Daje komunikat:
    /tmp/main.cpp:1: error: ‘SomeType’ does not name a type

    Jeżeli wstawię średnik po SomeType to dostaję dwa błędy:
    /tmp/main.cpp:1: error: expected constructor, destructor, or type conversion before ‘;’ token
    /tmp/main.cpp:1: error: expected constructor, destructor, or type conversion before ‘;’ token

    W obu przypadkach komunikaty są bardzo sensowne.

    Zgadzam się, C++ ma skomplikowaną składnię. Bez trudu można też napisać kod mający 6 linii, który wygeneruje ~1k linii błędu.

    Nie oszukujmy się jednak. Kompilatory potrafią radzić sobie z takimi, prostymi przypadkami.

    Kompilowałem:
    g++ -g -W -Wall -pedantic /tmp/main.cpp
    g++ (Debian 4.3.1-2) 4.3.1

    Język C++ daje się opisać gramatyką kontekstową, więc jest kontekstowy. Jest jakiś niuans?

  5. reVis:
    August 19th, 2008 o 15:04

    I tak trzeba przyznać, że błędy związane z samą składnią, po pewnej ilości praktyki są stosunkowo łatwo rozpoznawalne. Sytuacja ma się znacznie gorzej jak do gry wchodzą szablony. Wtedy dopiero błędy potrafią być uciążliwe przez bardzo długie logi.
    Od strony programisty ciekawym rozwiązaniem są sposoby wykorzystania komunikatów kompilatora jako rozszerzeń dla języka. Mam tutaj na myśli tworzenie constraints (ograniczeń) dla szablonów. Sama technika jest przydatna, ale też przebiegła. Użyte słowa przy ich deklaracji, które trafiają na ekran w czasie kompilacji wskazują na powstały problem i niezgodność.
    http://www.research.att.com/~bs/bs_faq2.html#constraints

  6. Xion:
    August 19th, 2008 o 15:41

    lmmilewski: Deklarowałem zmienną jako lokalną. A C++ jest kontekstowy, oczywiście, ale to tylko dlatego, że wszystkie języki bezkontekstowe też są kontekstowe. Lingwistyka jest naprawdę sensowna, nie ma co ;)

    reVis: No no… To już całkiem blisko do słynnego przykładu z generowaniem liczb pierwszych w kolejnych komunikatach o błędach :)

  7. Kos:
    August 19th, 2008 o 18:05

    Oj, mądra notka. C++ jest po prostu bardzo funkcjonalny, przez co to, co teoretycznie nie ma sensu, jest w nim poprawne. Na przykład mając jakąś zmienną, można sobie napisać:
    zmienna;
    To samo do stałych dosłownych:
    5;
    W pełni poprawna instrukcja kodu (z językowego punktu widzenia rzecz jasna ;) ). C i C++ pozwalają na takie rzeczy, więc i błędy są takie, a nie inne, bo kompilator nie ma możliwości się domyślić, o co nam chodziło. Nawet poczciwy średnik może się mścić:
    instrukcja1();
    instrukcja2();
    Gdy po pierwszej zapomnimy średnika (niby oczywisty błąd), kompilator wyrzuci jakiś tam błąd, ale na pewno nie będzie to nic w stylu “expected ; at end of line”. Kompilator olewa białe znaki, więc równie dobrze mógłby tam (w danych warunkach) stać jakikolwiek operator, ot choćby przecinek, plus lub nawet znak przypisania.
    “Wolność” składni C pozwala też na robienie wielu innych błędów, jak choćby tradycyjny wręcz while (i=0) { kod(); }. Tyle, że to jest o tyle gorsze od poprzednich przykładów, że się kompiluje… Na szczęście generując jednocześnie warninga na każdym sensownym kompilatorze, więc pół biedy.

    Pamiętam, że gdy “wyrosłem” ze starego, dobrego Pascala i zacząłem uczyć się C++, miałem właśnie przez takie rzeczy sporo trudności z przerzutką. Oj, ile to ja wtedy miałem problemów przez te enigmatyczne nazwy błędów. :) Najwięcej miałem zabawy, gdy w programie miałem coś takiego:
    #include “klasa.h”
    #include
    A w nagłówku klasa.h moim zwyczajem zapominałem średnika po klamrze zamykającej definicję. I otwierałem z pasją nagłówek OpenGLa, z nadzieją, że uda mi się poprawić w nim te liczne błędy i jednocześnie zastanawiając się, czemu nigdy wcześniej ich nie zauważyłem. :>

    Motto proste: “With great power comes great responsibility”, co można w tym wypadku dość luźno przetłumaczyć jako “Skoro jesteś na tyle odważny, by pisać w C++, to sam sobie szukaj swoich błędów” :}
    Wzdych… A programiści Javy to dopiero mają luksusy. Wpisze sobie user Eclipse pustą funkcję:
    int funkcja() {
    }
    Od razu ma ładnie podkreślone na czerwono, z komunikatem w stylu “No return statement found”, a w menu kontekstowym od razu może sobie wybrać “Add a return statement” lub “Change returned type to void”. To dopiero IDE dla leniwych, wszystko chce zrobić za programistę :) A my, gamedevowe dziadki lubujące się w C++, musimy sobie radzić samemu…
    Chociaż jako ciekawostkę dodam, że Eclipse, którego na co dzień używam, też już potrafi znajdywać niektóre błędy. Podkreśla wszelkiej maści błędy składniowe, nawet w skomplikowanych instrukcjach (niestety oznacza je raptem jako “Syntax error” – jak już wiemy, rozpoznać ‘faktycznego’ błędu w C++ nie idzie), potrafi też rozpoznać złe wywołanie funkcji (na podstawie niezgodności typów parametrów – chociaż tego w aktualnej wersji nie wyróżnia, raptem nie koloruje wywołania funkcji). MS VS pewnie też nie pozostaje w tyle pod względem wprowadzania w użytek tego typu bajerów? Choć z drugiej strony, z tego co wiem, nawet do semantycznego podświetlania składni potrzebuje płatnego plugina… zgroza! :/

    Sytuacja programistów C++ nie jest w kwestiach rozpoznawania i znajdywania błędów taka bajkowa, jak w przypadku np. Javy (najfajniejsze są te długie tramwaje błędów przy szablonach), ale jak widać, narzędzia idą do przodu, stary nasz język uparcie nie chce się wynieść z rynku, więc kto wie? Może kiedyś doczekamy takich ‘bajerów’ ułatwiających życie, jak ot choćby ta wspomniana Java?

  8. Reg:
    August 24th, 2008 o 21:33

    A zobaczcie jaki błąd pokazuje w tym kodzie C# !!!

    error CS0246: The type or namespace name ‘SomeType’ could not be found (are you missing a using directive or an assembly reference?)

    Czyli jak się chce to się da ! C++ jest po prostu do dupy :P

  9. CittEr:
    August 25th, 2008 o 14:02

    A ja myślałem, że Regedit to taki fajny gość…

  10. Developer:
    August 31st, 2008 o 12:54

    @Gynvael:
    To normalne, wtedy zaczynasz poszukiwania od pierwszego komunikatu z błędem, reszta jest najczęściej konsekwencją tego pierwszego.

    @Reg:
    Zgadzam się! Ale to nie C++ jest z tego powodu do dupy, tylko kompilator, no i w sumie nie aż tak o razu całkiem do dupy… ;)

    @CittEr:
    Regedit powiedział na głos to, co inni starannie przemilczają. +P

  11. noisy:
    September 4th, 2008 o 15:41

    Też pamiętam jak pierszy raz zmagałem się z brakiem średnika na końcu definicji klasy…to był koszmar! :)

    Po pewnym czasie zauważyłem, że w jednej książce/kursie (niepamiętam) ciekawie uczą programowania..

    wg nich instrukcje sterującą if powinno się pisać tak

    if(warunek)
    {
    instrukcja;
    }; //nie napisali tutaj nic o nadmiarowym sredniku, ale urzekl mnie on dosc znacząco

    podobnie posługiwali się

    if()
    {
    instrukcja;
    }
    else
    {
    instrukcja2;
    };

    for(;;)
    {};

    while()
    {};

    do{}
    while(); //no tutaj akurat zrozumiale! :)

    w sumie, jezeli ktos sie nauczy, ze tak trzeba wstawiac sredniki, to przy definicji klasy sie juz zastanawiac nie bedzie…

    jest w tym jakiś sposób…choć ja osobiście nie stawiam nadmiarowych średników.
    Swoją drogą…przychodzi komuś do głowy jakaś taka sytuacja gdzie nadmiarowy średnik za jakąś parą {} może bardzo zaszkodzić?

  12. Xion:
    September 4th, 2008 o 16:15

    Przynajmniej kilka:

    1) Średnik po } a przed ‘else’.
    2) Średnik po } a przed ‘while’ (w do-while).
    3) Średnik po } w konstrukcjach typu:

    1. struct TFoo
    2. {
    3.     // ...
    4. } /*;*/ SomeVerySpecialInstanceOfFoo;

    4) Średnik po } w przy inicjalizacji tablic wielowymiarowych, tj.:

    1. int array[][2] = { { 1, 2 } /*;*/, { 3, 4 }, { 5 , 6 } };
Comments are disabled.
 


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