Najboleśniejszy strzał w stopę

2011-03-13 20:14

Znanych jest mnóstwo kruczków w języku C++, o których trzeba pamiętać, jeśli chcemy efektywnie w nim programować i nie tracić zbyt dużo czasu na odczytywanie niespecjalnie zrozumiałych komunikatów kompilatora czy – gorzej – drapanie się po głowie podczas debugowania naszego programu. O wielu z nich miałem okazję pisać, ale oczywiście ten temat-rzeka daleki jest od wyczerpania :) W jego nurcie wyróżnia się jednak jeden wybitnie złośliwy przypadek, który przez długi czas uważałem aczkolwiek za ciekawostkę, na którą w praktyce raczej nikt się nie natknie…
Rzecz jasna, myliłem się. Okazuje się, że okaz ten jak najbardziej występuje w rzeczywistym świecie. Nie objawia się wprawdzie zbyt często, ale dzięki temu jest jeszcze bardziej podstępny, posiadając tym większą siłę rażenia, gdy w końcu na kogoś trafi. Dobrze więc wiedzieć, co robić, aby się przed nim bronić :] I tym właśnie chciałbym się dzisiaj zająć.

Pewną pomocą jest tutaj fakt, iż opisywany błąd zdaje się zazwyczaj pojawiać w pewnym konkretnym scenariuszu. Jego wystąpienie w owym kontekście jest przy tym tak zaskakujące, że zetknięcie się z nim skutecznie “uodparnia” na wszelkie inne, podobne okoliczności w których problem może wystąpić. Rzeczony scenariusz jest przy tym bardzo typowy: chodzi o wczytanie zawartości pliku (otwartego jako strumień std::ifstream) do kolekcji albo zmiennej, na przykład łańcucha znaków typu std::string.
Są naturalnie tacy, co bawiliby się tutaj w bufor pomocniczy, pętlę i funkcję getline lub coś w tym guście. Programiści lubiący operować na nieco wyższym poziomie wiedzą jednak, że std::string możemy inicjalizować zakresem znaków określonym – jak każdy zakres w C++ – parą iteratorów. Trochę mniej znanym faktem jest z kolei to, że iterować można również po strumieniach. Mamy na przykład coś takiego jak std::istream_iterator, który potrafi automatycznie dekodować ze strumienia egzemplarze ustalonego typu danych:

  1. // odczytanie 10 liczb int z stdio
  2. vector<int> numbers(10);
  3. copy (istream_iterator< int >(cin),
  4.       istream_iterator< int >(), numbers);

Jeśli nam to nie odpowiada i wolimy dostęp do “surowych” bajtów, wtedy z pomocą przychodzi bardziej wewnętrzny std::istreambuf_iterator. To właśnie przy jego pomocy możemy szybko przejść po zawartości strumienia plikowego i umieścić ją w stringu:

  1. ifstream file("plik.txt");
  2. string fileContents(istreambuf_iterator< char >(file),
  3.                     istreambuf_iterator< char >());

Jak pewnie można się domyślić, zakres zdefiniowany przez te iteratory w obu przypadkach zaczyna się na początku strumienia. Kończy się zaś w miejscu, gdzie odczyt następnej porcji danych nie jest już możliwy. W naszym “rozwiązaniu” problemu wczytywania całego pliku będzie to więc koniec tego pliku, czyli to co nam chodzi.

Nieprzypadkowo jednak słowo ‘rozwiązanie’ ująłem w cudzysłów. Powyższe dwie instrukcje zawierają bowiem ów wyjątkowo perfidny błąd, o którym wspominałem na początku. Dość powiedzieć, że jeśli spowoduje on wygenerowanie przez kompilator bardzo tajemniczego komunikatu, będzie to lepszy z jego dwóch możliwych rezultatów. Drugim jest kompilacja zakończona powodzeniem i… kod, który robi dokładnie nic. Nie tylko nic nie wczytuje, ale nawet nie tworzy zmiennej typu string! To raczej zaskakujące, nieprawdaż? ;)

Fenomen ten jest znany jako “najbardziej kłopotliwa interpretacja”, co daje pewną mglistą wskazówkę co do natury zjawiska, lecz nie brzmi nawet w połowie tak dobrze jak jego angielska nazwa: most vexing parse. Problem – w dużym skrócie – polega na tym, że to co według nas jest deklaracją i inicjalizacją zmiennej (nazwanej tutaj fileContents) dla kompilatora jest, owszem, deklaracją – tyle że funkcji. Funkcji, którą normalnie zapisalibyśmy raczej tak:

  1. string fileContents(istreambuf_iterator< char > file,
  2.                     istreambuf_iterator< char > (*fun)());

Jak widać, różnica jest dość duża, ale mimo to obie instrukcje są interpretowane dokładnie tak samo. A to wszystko dzięki możliwości wstawienia dodatkowych nawiasów, pominięcia nazw parametrów… krótko mówiąc, “dzięki” wyjątkowo niefortunnej kombinacji mało znanych reguł składni C++.

Żeby było jeszcze ciekawiej, opisany tutaj błąd jest tylko skomplikowanym wariantem znacznie prostszych i powszechniejszych pomyłek w rodzaju poniższej:

  1. Object obj(); // miała być zmienna, a jest funkcja

o których zresztą pisałem na marginesie innych zagadnień. Wszystkie one są konsekwencją poniższej reguły, którą warto sobie wbić do głowy, jeżeli programujemy cokolwiek poważniejszego w C++:

Jeśli dana instrukcja kodu C++ może być zinterpretowana jako deklaracja funkcji, to właśnie tak zostanie ona zinterpretowana.

Wiedząc o tym, możemy uniknąć wielu przypadków strzelania sobie w stopę. Łącznie z tym, który pokazałem powyżej.

Dokładny opis most vexing parse można znaleźć w punkcie 6. słynnej książki Scotta Meyersa Effective STL (po polsku: STL w praktyce. (…), wyd. Helion). Polecam go, jeśli ktoś zastanawia się, skąd u licha wziął się tutaj wskaźnik na funkcję ;)

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


5 comments for post “Najboleśniejszy strzał w stopę”.
  1. Kamil Szatkowski:
    March 13th, 2011 o 20:29

    Ja się zastanawiam bo jakiego grzyba ktoś to tam wsadził. Deklaracje funkcji w funkcji są bardziej niż zbędne.

  2. Kos:
    March 13th, 2011 o 22:34

    Niezły przykład. :)
    Also, nie masz nadliczbowej gwiazdki w “zapisalibyśmy raczej tak”?

  3. Xion:
    March 13th, 2011 o 23:07

    Gwiazdka tam jest opcjonalna. Tak naprawdę wystarczy tylko para nawiasów na końcu, żeby parametr był zinterpretowany jako wskaźnik na funkcję.

  4. Asmodeusz:
    March 16th, 2011 o 12:54

    @Kamil: każdy w miarę sensownie działający plugin/program do SCA wykrywa “niepotrzebne deklaracje funkcji” i zazwyczaj traktuje je jako error/critical. Błąd wyjątkowo złośliwy – jednak do szybkiego zauważenia przy dobrym toolsecie.

    Nieco gorzej gdy próbujemy zadeklarować zmienną poprzez kod typu: Typ nazwa(); i gdzieś w projekcie jest funkcja o takiej samej nazwie ;)

  5. Xirdus:
    March 16th, 2011 o 17:24

    A mogliby zrobić keyworda “function”…

Comments are disabled.
 


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