Dlaczego komunikaty o błędach szablonu C ++ są tak przerażające?

28

Szablony C ++ są znane z generowania długich, nieczytelnych komunikatów o błędach. Mam ogólne pojęcie, dlaczego komunikaty o błędach szablonów w C ++ są tak złe. Zasadniczo problem polega na tym, że błąd nie jest wyzwalany, dopóki kompilator nie napotka składni, która nie jest obsługiwana przez określony typ w szablonie. Na przykład:

template <class T>
void dosomething(T& x) { x += 5; }

Jeśli Tnie obsługuje +=operatora, kompilator wygeneruje komunikat o błędzie. A jeśli dzieje się to gdzieś głęboko w bibliotece, komunikat o błędzie może mieć tysiące linii.

Ale szablony C ++ są w zasadzie tylko mechanizmem do pisania kaczego w czasie kompilacji. Błąd szablonu C ++ jest koncepcyjnie bardzo podobny do błędu typu środowiska wykonawczego, który może wystąpić w dynamicznym języku, takim jak Python. Rozważmy na przykład następujący kod Python:

def dosomething(x):
   x.foo()

Tutaj, jeśli xnie ma foo()metody, interpreter Pythona zgłasza wyjątek i wyświetla ślad stosu wraz z dość wyraźnym komunikatem o błędzie wskazującym problem. Nawet jeśli błąd nie zostanie wyzwolony, dopóki interpreter nie znajdzie się głęboko w jakiejś funkcji bibliotecznej, komunikat o błędzie środowiska wykonawczego nadal nie jest tak zły jak nieczytelne wymioty wyrzucane przez typowy kompilator C ++. Dlaczego więc kompilator C ++ nie może lepiej wyjaśnić, co poszło nie tak? Dlaczego niektóre komunikaty o błędach szablonu C ++ dosłownie powodują przewijanie okna konsoli przez ponad 5 sekund?

Channel72
źródło
6
Niektóre kompilatory mają przerażające komunikaty o błędach, ale inne są naprawdę dobre ( clang++mrugnięcie, mrugnięcie).
Benjamin Bannier
2
Wolałbyś więc, żeby twoje programy zawodziły w czasie wykonywania, wysyłane z rąk klienta, zamiast zawieść w czasie kompilacji?
P Shved
13
@Pavel, no. To pytanie nie dotyczy zalet / wad środowiska wykonawczego w porównaniu do sprawdzania błędów podczas kompilacji.
Channel72
1
Jako przykład dużych błędów szablonu C ++, FWIW: codegolf.stackexchange.com/a/10470/7174
kebs

Odpowiedzi:

28

Komunikaty o błędach szablonu mogą być znane, ale w żadnym wypadku nie zawsze są długie i nieczytelne. W takim przypadku cały komunikat o błędzie (z gcc) to:

test.cpp: In function void dosomething(T&) [with T = X]’:
test.cpp:11:   instantiated from here
test.cpp:6: error: no match for operator+=’ in x += 5

Podobnie jak w przykładzie w Pythonie, otrzymujesz „ślad stosu” punktów tworzenia szablonów i jasny komunikat o błędzie wskazujący na problem.

Czasami komunikaty o błędach związane z szablonami mogą być znacznie dłuższe z różnych powodów:

  • „Ślad stosu” może być znacznie głębszy
  • Nazwy typów mogą być znacznie dłuższe, ponieważ szablony są tworzone z innymi instancjami szablonów jako argumentami i wyświetlane ze wszystkimi kwalifikatorami przestrzeni nazw
  • Gdy rozwiązanie problemu z przeciążeniem nie powiedzie się, komunikat o błędzie może zawierać listę potencjalnych przeciążeń (z których każda może zawierać bardzo długie nazwy typów)
  • Ten sam błąd może zostać zgłoszony wiele razy, jeśli w wielu miejscach zostanie utworzony niepoprawny szablon

Główną różnicą w stosunku do Pythona jest statyczny system typów, co prowadzi do konieczności dołączania (czasem długich) nazw typów w komunikacie o błędzie. Bez nich czasami bardzo trudno zdiagnozować, dlaczego nie udało się rozwiązać problemu przeciążenia. Dzięki nim Twoim wyzwaniem nie jest już odgadnięcie, gdzie jest problem, ale rozszyfrowanie hieroglifów, które mówią ci, gdzie to jest.

Ponadto sprawdzenie w czasie wykonywania oznacza, że ​​program zatrzyma się przy pierwszym napotkanym błędzie, wyświetlając tylko jeden komunikat. Kompilator może wyświetlać wszystkie napotkane błędy, dopóki się nie poddaje; przynajmniej w C ++ nie powinien kończyć się na pierwszym błędzie w pliku, ponieważ może to być konsekwencją późniejszego błędu.

Mike Seymour
źródło
4
Czy możesz podać przykład błędu będącego konsekwencją późniejszego błędu?
Ruslan
12

Kilka oczywistych powodów to:

  1. Historia. Gdy gcc, MSVC itp. Były nowe, nie było ich stać na wykorzystanie dużej ilości miejsca do przechowywania danych w celu generowania lepszych komunikatów o błędach. Pamięć była na tyle mała, że ​​po prostu nie mogli.
  2. Przez lata konsumenci ignorowali jakość komunikatów o błędach, więc dostawcy również to robili.
  3. W przypadku niektórych kodów kompilator może ponownie zsynchronizować i zdiagnozować prawdziwe błędy w dalszej części kodu. Błędy w szablonach kaskadowo spadają tak bardzo, że cokolwiek sprzed pierwszego jest prawie zawsze bezużyteczne.
  4. Ogólna elastyczność szablonów utrudnia odgadnięcie, co prawdopodobnie miałeś na myśli, gdy kod zawiera błąd.
  5. Wewnątrz szablonu znaczenie nazwy zależy zarówno od kontekstu szablonu, jak i kontekstu tworzenia, a wyszukiwanie zależne od argumentów może dodać jeszcze więcej możliwości.
  6. Przeciążenie funkcji może dostarczyć wielu kandydatów do tego, do czego może odwoływać się określone wywołanie funkcji , a niektóre kompilatory (np. Gcc) obowiązkowo wyświetlają je wszystkie, gdy występuje dwuznaczność.
  7. Wielu programistów, którzy nigdy nie rozważyliby użycia normalnych parametrów bez upewnienia się, że przekazane wartości spełniają wymagania, nawet nie próbują w ogóle sprawdzać parametrów szablonu (i muszę wyznać, że sam to robię).

Nie jest to wyczerpujące, ale masz ogólny pomysł. Nawet jeśli nie jest to łatwe, większość można wyleczyć. Przez lata mówiłem ludziom, aby otrzymywali kopię Comeau C ++ do regularnego użytku; Prawdopodobnie raz zaoszczędziłem na jednym komunikacie o błędzie, aby zapłacić za kompilator. Teraz Clang dochodzi do tego samego punktu (i jest jeszcze tańszy).

Zakończę ogólną obserwacją, która brzmi jak żart, ale tak naprawdę nie jest. Przez większość czasu prawdziwym zadaniem kompilatora jest przekształcanie kodu źródłowego w komunikaty o błędach. Najwyższy czas, aby dostawcy skoncentrowali się na wykonywaniu tej pracy nieco lepiej - chociaż otwarcie przyznam, że kiedy pisałem kompilatory, miałem silną tendencję do traktowania jej jako drugorzędnej (w najlepszym wypadku), aw niektórych przypadkach prawie ją ignorowałem całkowicie.

Jerry Coffin
źródło
9

Prosta odpowiedź brzmi, ponieważ Python został zaprojektowany w taki sposób, podczas gdy wiele rzeczy związanych z szablonami powstało przez przypadek. Na przykład nigdy nie zamierzano stać się systemem kompletnym dla Turinga. A jeśli nie możesz celowo zaplanować i uzasadnić tego, co dzieje się, gdy twój system działa , dlaczego ktoś miałby oczekiwać ostrożnego, przemyślanego planowania tego, co dzieje się, gdy coś pójdzie nie tak?

Ponadto, jak wskazałeś, interpreter języka Python może ci znacznie ułatwić, wyświetlając ślad stosu, ponieważ interpretuje kod języka Python. Jeśli kompilator C ++ napotka błąd szablonu i poda ślad stosu, byłoby to tak samo nieprzydatne jak „szablon wymiotuje”, prawda?

Mason Wheeler
źródło