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:
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:
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:
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:
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ę ;)
Ja się zastanawiam bo jakiego grzyba ktoś to tam wsadził. Deklaracje funkcji w funkcji są bardziej niż zbędne.
Niezły przykład. :)
Also, nie masz nadliczbowej gwiazdki w “zapisalibyśmy raczej tak”?
Gwiazdka tam jest opcjonalna. Tak naprawdę wystarczy tylko para nawiasów na końcu, żeby parametr był zinterpretowany jako wskaźnik na funkcję.
@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 ;)
A mogliby zrobić keyworda “function”…