printf - źródło błędów? [Zamknięte]

9

Używam dużo do printfcelów śledzenia / logowania w moim kodzie, okazało się, że jest to źródło błędu programistycznego. Zawsze uważałem operator wstawiania ( <<) za coś dziwnego, ale zaczynam myśleć, że używając go zamiast tego mogę uniknąć niektórych z tych błędów.

Czy ktoś miał kiedyś podobne objawienie, czy po prostu chwytam się tutaj słomek?

Niektórzy odbierają punkty

  • Mój obecny sposób myślenia jest taki, że bezpieczeństwo typu przewyższa wszelkie korzyści wynikające z używania printf. Prawdziwym problemem jest łańcuch formatu i użycie funkcji variadic, które nie są bezpieczne dla typu.
  • Może nie będę używać <<i wariantów strumienia wyjściowego stl, ale z pewnością przyjrzę się zastosowaniu mechanizmu bezpiecznego dla typu, który jest bardzo podobny.
  • Dużo śledzenia / rejestrowania jest warunkowe, ale chciałbym zawsze uruchamiać kod, aby nie pomijać błędów w testach tylko dlatego, że jest to rzadko brana gałąź.
John Leidegren
źródło
4
printfw świecie C ++? Coś tu brakuje?
user827992,
10
@ user827992: Czy brakuje ci faktu, że standard C ++ zawiera bibliotekę standardu C przez odniesienie? Używanie printfw C ++ jest całkowicie legalne . (Czy to dobry pomysł, to kolejne pytanie.)
Keith Thompson
2
@ user827992: printfma pewne zalety; zobacz moją odpowiedź.
Keith Thompson,
1
To pytanie jest dość graniczne. Pytania „Co myślicie” są często zamknięte.
dbracey,
1
@vitaut Chyba (dzięki za wskazówkę). Jestem trochę zakłopotany agresywną moderacją. Tak naprawdę nie sprzyja to interesującym dyskusjom na temat programowania sytuacji, o których chciałbym mieć więcej.
John Leidegren

Odpowiedzi:

2

printf, szczególnie w przypadkach, w których możesz dbać o wydajność (takich jak sprintf i fprintf), to naprawdę dziwny hack. Ciągle mnie zadziwia, że ​​ludzie, którzy używają C ++ z powodu niewielkiego narzutu związanego z wydajnością funkcji wirtualnych, będą następnie bronić C's io.

Tak, aby obliczyć format naszego wyjścia, coś, co możemy poznać w 100% w czasie kompilacji, przeanalizujmy ciąg formatu fricken w czasie wykonywania wewnątrz ogromnie dziwnej tabeli skoków, używając niepoprawnych kodów formatu!

Oczywiście tych kodów formatu nie można dopasować do reprezentowanych przez nich typów, byłoby to zbyt łatwe ... i za każdym razem, gdy sprawdzasz, czy to% llg czy% lg, że ten (silnie napisany) język sprawia, że ​​jesteś wymyśl typy ręcznie, aby coś wydrukować / zeskanować, ORAZ został zaprojektowany dla procesorów starszych niż 32-bitowe.

Przyznaję, że obsługa szerokości i precyzji formatu przez C ++ jest nieporęczna i mogłaby zużywać trochę cukru syntaktycznego, ale to nie znaczy, że musisz bronić dziwnego hacka, który jest głównym systemem io. Absolutne podstawy są dość łatwe w każdym języku (chociaż prawdopodobnie powinieneś używać czegoś takiego jak niestandardowa funkcja błędu / strumień błędów w kodzie debugowania), umiarkowane przypadki są podobne do wyrażeń regularnych w C (łatwe do napisania, trudne do analizy / debugowania ), a złożone przypadki niemożliwe w C.

(Jeśli w ogóle korzystasz ze standardowych kontenerów, napisz sobie kilka przeciążeń szybkiego operatora <<, które pozwalają ci robić takie rzeczy jak std::cout << my_list << "\n";debugowanie, gdzie moja_lista jest typu list<vector<pair<int,string> > >).

Jkerian
źródło
1
Problemem standardowej biblioteki C ++ jest to, że większość inkarnacji implementuje się operator<<(ostream&, T)przez wywołanie ... no cóż sprintf! Wydajność sprintfnie jest optymalna, ale z tego powodu wydajność iostreamów jest na ogół jeszcze gorsza.
Jan Hudec
@JanHudec: W tym momencie nie było to prawdą przez około dekadę. Rzeczywiste drukowanie odbywa się przy użyciu tych samych podstawowych wywołań systemowych, a implementacje C ++ często wywołują w tym celu biblioteki C ... ale to nie to samo, co routowanie std :: cout przez printf.
jkerian
16

Mieszanie danych wyjściowych w stylu C printf()(lub puts()lub putchar()...) z danymi std::cout << ...wyjściowymi w stylu C ++ może być niebezpieczne. Jeśli dobrze pamiętam, mogą mieć osobne mechanizmy buforowania, więc dane wyjściowe mogą nie pojawiać się w zamierzonej kolejności. (Jak wspomina AProgrammer w komentarzu, sync_with_stdiorozwiązuje ten problem).

printf()jest zasadniczo niebezpieczny dla typu. Typ oczekiwany dla argumentu jest określony przez ciąg formatu ( "%d"wymaga intpromowania lub czegoś, co promuje int, "%s"wymaga, char*który musi wskazywać na poprawnie zakończony ciąg w stylu C itp.), Ale przekazanie niewłaściwego typu argumentu powoduje niezdefiniowane zachowanie , nie jest to błąd diagnostyczny. Niektóre kompilatory, takie jak gcc, wykonują dość dobrą robotę, ostrzegając przed niedopasowaniem typów, ale mogą to zrobić tylko wtedy, gdy ciąg formatu jest literałem lub jest znany w czasie kompilacji (co jest najczęstszym przypadkiem) - i takie ostrzeżenia nie są wymagane przez język. Jeśli podasz niewłaściwy typ argumentu, mogą się zdarzyć dowolne złe rzeczy.

Z drugiej strony, strumień I / O C ++ jest znacznie bardziej bezpieczny dla typu, ponieważ <<operator jest przeciążony wieloma różnymi typami. std::cout << xnie musi określać typu x; kompilator wygeneruje odpowiedni kod dla dowolnego typu x.

Z drugiej strony printfopcje formatowania są znacznie wygodniejsze. Jeśli chcę wydrukować wartość zmiennoprzecinkową z 3 cyframi po przecinku, mogę użyć "%.3f"- i nie ma to wpływu na inne argumenty, nawet w ramach tego samego printfwywołania. setprecisionZ drugiej strony C ++ wpływa na stan strumienia i może zepsuć później dane wyjściowe, jeśli nie będziesz bardzo ostrożny, aby przywrócić strumień do poprzedniego stanu. (To moje osobiste wkurzenie; jeśli brakuje mi jakiegoś czystego sposobu, aby tego uniknąć, proszę o komentarz.)

Oba mają zalety i wady. Dostępność printfjest szczególnie przydatna, jeśli masz tło C i znasz go bardziej lub importujesz kod źródłowy C do programu C ++. std::cout << ...jest bardziej idiomatyczny dla C ++ i nie wymaga tyle uwagi, aby uniknąć niedopasowania typów. Oba są poprawnymi językami C ++ (standard C ++ obejmuje większość biblioteki standardowej C przez odniesienie).

To prawdopodobnie najlepiej użyć std::cout << ...dla dobra innych programistów C ++, który może pracować na kodzie, ale można użyć jednego albo - zwłaszcza w kodzie śledzenia, że masz zamiar wyrzucić.

Oczywiście warto poświęcić trochę czasu na naukę korzystania z debuggerów (ale w niektórych środowiskach może to nie być możliwe).

Keith Thompson
źródło
Brak wzmianki o mieszaniu w pierwotnym pytaniu.
dbracey,
1
@dbracey: Nie, ale pomyślałem, że warto o tym wspomnieć jako możliwą wadę printf.
Keith Thompson,
6
Jeśli chodzi o problem z synchronizacją, zobacz std::ios_base::sync_with_stdio.
AProgrammer
1
+1 Użycie std :: cout do wydrukowania informacji debugowania w aplikacji wielowątkowej jest w 100% bezużyteczne. Przynajmniej w printf rzeczy nie są tak często przeplatane i nieprzenikalne przez człowieka lub maszynę.
James
@James: Czy to dlatego, że std::coutużywa osobnego wywołania dla każdego drukowanego elementu? Możesz obejść ten problem, zbierając wiersz wyniku w łańcuch przed wydrukowaniem go. I oczywiście możesz również wydrukować jeden element na raz printf; wygodniej jest wydrukować linię (lub więcej) w jednym połączeniu.
Keith Thompson,
2

Twój problem najprawdopodobniej pochodzi z mieszania dwóch bardzo różnych standardowych menedżerów produkcji, z których każdy ma swój własny program dla tego biednego małego STDOUT. Nie otrzymujesz żadnych gwarancji dotyczących sposobu ich implementacji, i jest całkowicie możliwe, że ustawiają one opcje deskryptora powodującego konflikt, obie próbują zrobić z nim różne rzeczy itp. Ponadto operatorzy wstawiania mają jedną poważną nad printf: printfpozwolą ci to zrobić:

printf("%d", SomeObject);

Podczas gdy <<nie będzie.

Uwaga: Do debugowania nie używasz printfani cout. Używasz fprintf(stderr, ...)i cerr.

Linuxios
źródło
Brak wzmianki o mieszaniu w pierwotnym pytaniu.
dbracey,
Oczywiście możesz wydrukować adres obiektu, ale duża różnica polega na tym, że printfnie jest on bezpieczny dla typu, a obecnie myślę, że bezpieczeństwo typu przewyższa wszelkie korzyści wynikające z używania printf. Problemem jest tak naprawdę ciąg formatujący i funkcja variadic, która nie jest bezpieczna dla typu.
John Leidegren,
@JohnLeidegren: Ale co, jeśli nie SomeObjectbędę wskaźnikiem? Otrzymasz dowolne dane binarne reprezentowane przez kompilator SomeObject.
Linuxios,
Myślę, że przeczytałem twoją odpowiedź wstecz ... nvm.
John Leidegren,
1

Istnieje wiele grup - na przykład Google - które nie lubią strumieni.

http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Streams

(Pop otwórz trójkąt, żebyś mógł zobaczyć dyskusję.) Myślę, że przewodnik po stylu Google C ++ zawiera DUŻO bardzo rozsądnych porad.

Myślę, że kompromis polega na tym, że strumienie są bezpieczniejsze, ale printf jest bardziej czytelny (i łatwiejszy do uzyskania pożądanego formatowania).

dbracey
źródło
2
Przewodnik po stylu Google jest fajny, ALE zawiera sporo elementów, które nie nadają się do przewodnika ogólnego przeznaczenia . (co jest w porządku, bo przecież jest to przewodnik Google dotyczący kodu działającego w / dla Google.)
Martin Ba
1

printfmoże powodować błędy z powodu braku bezpieczeństwa typu. Istnieje kilka sposobów adresowania, że bez przełączania iostream„s <<operatora i bardziej skomplikowane formatowanie:

  • Niektóre kompilatory (takie jak GCC i Clang) mogą opcjonalnie sprawdzać printfciągi formatu względem printfargumentów i mogą wyświetlać ostrzeżenia, takie jak poniższe, jeśli się nie zgadzają.
    ostrzeżenie: konwersja określa typ „int”, ale argument ma typ „char *”
  • Typesafeprintf skrypt może Preprocesuj swoje printf-Style połączenia, aby je wpisać bezpieczny.
  • Biblioteki takie jak Boost.Format i FastFormat pozwalają na korzystanieprintf ciągów formatu podobnych (w szczególności Boost.Format są prawie identyczne printf), przy jednoczesnym zachowaniu iostreamsbezpieczeństwa i rozszerzalności typu.
Josh Kelley
źródło
1

Składnia Printf jest w zasadzie w porządku, pomijając pewne niejasne pisanie. Jeśli uważasz, że to źle, dlaczego C #, Python i inne języki używają bardzo podobnej konstrukcji? Problem w C lub C ++: nie jest częścią języka i dlatego nie jest sprawdzany przez kompilator pod kątem poprawnej składni (*) i nie jest rozkładany na serię wywołań natywnych, jeśli optymalizuje się pod kątem szybkości. Pamiętaj, że jeśli zoptymalizujesz rozmiar, wywołania printf mogą okazać się bardziej wydajne! Składnia przesyłania strumieniowego w C ++ jest imho niezbyt dobra. Działa, bezpieczeństwo typu istnieje, ale pełna składnia ... bleh. Mam na myśli, że go używam, ale bez radości.

(*) niektóre kompilatory wykonują to sprawdzanie oraz prawie wszystkie narzędzia analizy statycznej (używam Lint i od tamtej pory nigdy nie miałem problemów z printf).

Zniszczyć
źródło
1
Istnieje Boost.Format, który łączy wygodną składnię ( format("fmt") % arg1 % arg2 ...;) z bezpieczeństwem typu. Kosztem większej wydajności, ponieważ generuje wywołania typu stringstream, które wewnętrznie generują wywołania sprintf w wielu implementacjach.
Jan Hudec
0

printfjest, moim zdaniem, znacznie bardziej elastycznym narzędziem wyjściowym do radzenia sobie ze zmiennymi niż którekolwiek wyjście strumienia CPP. Na przykład:

printf ( "%d in ANSI = %c\n", j, j ); /* Perfectly valid... if a char ISN'T printing right, I'd just check the integer value to make sure it was okay. */

Jednak możesz użyć <<operatora CPP , gdy przeciążasz go dla określonej metody ... na przykład, aby uzyskać zrzut obiektu, który przechowuje dane konkretnej osoby, PersonData....

ostream &operator<<(ostream &stream, PersonData obj)
{
 stream << "\nName: " << name << endl;
 stream << " Number: " << phoneNumber << endl;
 stream << " Age: " << age << endl;
 return stream;
}

W związku z tym bardziej efektywne byłoby stwierdzenie (zakładając, że ajest to obiekt PersonData)

std::cout << a;

niż:

printf ( "Name: %s\n Number: %s\n Age: %d\n", a.name, a.number, a.age );

Ten pierwszy jest o wiele bardziej zgodny z zasadą enkapsulacji (nie trzeba znać szczegółów, prywatnych zmiennych członkowskich), a także jest łatwiejszy do odczytania.

Aviator45003
źródło
0

Nie powinieneś używać printfw C ++. Zawsze. Powodem jest, jak słusznie zauważyłeś, że jest to źródło błędów, a fakt, że drukowanie typów niestandardowych, aw C ++ prawie wszystko powinno być typami niestandardowymi, jest bólem. Rozwiązaniem w C ++ są strumienie.

Istnieje jednak poważny problem, który sprawia, że ​​strumienie nie są odpowiednie dla żadnego wyjścia widocznego dla użytkownika! Problemem są tłumaczenia. Przykład zapożyczenia z podręcznika gettext mówi, że chcesz napisać:

cout << "String '" << str << "' has " << str.size() << " characters\n";

Teraz przychodzi niemiecki tłumacz i mówi: Ok, po niemiecku, wiadomość powinna być

n Zeichen lang ist die Zeichenkette ' s '

A teraz masz kłopoty, ponieważ on potrzebuje porozrzucanych elementów. Należy powiedzieć, że nawet wiele wdrożeń printfma z tym problem. Chyba że obsługują rozszerzenie, więc możesz użyć

printf("%2$d Zeichen lang ist die Zeichenkette '%1$s'", ...);

W Boost.Format obsługuje formaty printf stylu i ma tę funkcję. Więc piszesz:

cout << format("String '%1' has %2 characters\n") % str % str.size();

Niestety, wiąże się to z pewnym obniżeniem wydajności, ponieważ wewnętrznie tworzy ciąg znaków i używa <<operatora do formatowania każdego bitu, aw wielu implementacjach <<operator wewnętrznie wywołuje sprintf. Podejrzewam, że bardziej skuteczne wdrożenie byłoby możliwe, gdyby było to naprawdę pożądane.

Jan Hudec
źródło
-1

Wykonujesz wiele bezużytecznej pracy, oprócz tego, że stljest zły lub nie, debuguj swój kod za pomocą serii printftylko 1 dodatkowego poziomu możliwych awarii.

Wystarczy użyć debuggera i przeczytać coś o wyjątkach oraz o tym, jak je złapać i wyrzucić; staraj się nie być bardziej gadatliwym niż w rzeczywistości.

PS

printf jest używany w C, dla C ++ masz std::cout

użytkownik827992
źródło
Nie używasz śledzenia / rejestrowania zamiast debuggera.
John Leidegren,