Po przeczytaniu tej słynnej wypowiedzi Linusa Torvaldsa zastanawiałem się, jakie właściwie są wszystkie pułapki dla programistów w C ++. Nie mówię wyraźnie o literówkach lub błędnym przepływie programu, o których mowa w tym pytaniu i odpowiedziach , ale o więcej błędów wysokiego poziomu, które nie są wykrywane przez kompilator i nie powodują oczywistych błędów przy pierwszym uruchomieniu, kompletnych błędów projektowych, rzeczy, które są nieprawdopodobne w C, ale prawdopodobnie zostaną wykonane w C ++ przez początkujących, którzy nie rozumieją pełnych implikacji swojego kodu.
Z zadowoleniem przyjmuję również odpowiedzi wskazujące na ogromny spadek wydajności, w przypadku którego zwykle nie można tego oczekiwać. Przykład tego, co jeden z moich profesorów powiedział mi kiedyś o generatorze analizatora składni LR (1), który napisałem:
Użyłeś nieco zbyt wielu przypadków niepotrzebnego dziedziczenia i wirtualności. Dziedziczenie sprawia, że projekt jest znacznie bardziej skomplikowany (i nieefektywny z powodu podsystemu RTTI (wnioskowanie typu w czasie wykonywania)), dlatego powinien być używany tylko tam, gdzie ma to sens, np. W przypadku działań w tabeli analizy. Ponieważ intensywnie korzystasz z szablonów, praktycznie nie potrzebujesz dziedziczenia ”.
virtual
funkcji, prawda?dynamic_cast
powinno się udać, czy też nie, i kilka innych rzeczy, ale refleksja obejmuje znacznie więcej, w tym możliwość pobierania informacji o atrybutach lub funkcjach członka, które nie są obecny w C ++.Odpowiedzi:
Torvalds mówi tutaj ze swojego tyłka.
OK, dlaczego mówi ze swojego tyłka:
Po pierwsze, jego rant to tak naprawdę nic, ALE rant. Tutaj jest bardzo mało rzeczywistej treści. Jedynym powodem, dla którego jest naprawdę sławny lub nawet szanowany, jest to, że został stworzony przez boga Linuxa. Jego głównym argumentem jest to, że C ++ to bzdura i lubi wkurzać ludzi C ++. Oczywiście nie ma żadnego powodu, aby na to odpowiadać, a każdy, kto uważa to za rozsądny argument, i tak jest poza rozmową.
Co do tego, co można zaliczyć jako jego najbardziej obiektywne punkty:
Zasadniczo Torvalds mówi ze swojego tyłka. Nie powstaje żaden zrozumiały argument. Oczekiwanie poważnego odparcia takich bzdur jest po prostu głupie. Mówi mi się, żebym „rozwijał się” w odpowiedzi na coś, co miałbym rozwinąć, gdyby to było to, co powiedziałem. Jeśli naprawdę, szczerze spojrzysz na to, co powiedział Torvalds, zobaczysz, że tak naprawdę nic nie powiedział.
To, że Bóg mówi, że to nie znaczy, że ma to jakiś sens lub powinno być traktowane poważniej niż gdyby to powiedział jakiś przypadkowy bozo. Prawda jest taka, że Bóg jest kolejnym przypadkowym bozo.
Odpowiadając na aktualne pytanie:
Prawdopodobnie najgorsza i najczęstsza zła praktyka C ++ polega na traktowaniu jej jak C. Dalsze korzystanie z funkcji C API, takich jak printf, dostaje (również uważane za złe w C), strtok itp. Nie tylko nie udaje się wykorzystać dostarczonej mocy dzięki ciasnemu systemowi typów nieuchronnie prowadzą do dalszych komplikacji podczas próby interakcji z „prawdziwym” kodem C ++. Zasadniczo więc rób dokładnie odwrotność tego, co radzi Torvalds.
Naucz się korzystać z STL i Boost, aby uzyskać dalsze wykrywanie błędów w czasie kompilacji i ułatwić Ci życie na inne, ogólne sposoby (na przykład tokenizer doładowania jest zarówno bezpieczny dla typu ORAZ lepszy interfejs). To prawda, że musisz nauczyć się czytać błędy szablonu, co na początku jest zniechęcające, ale (z mojego doświadczenia i tak) jest to o wiele łatwiejsze niż próba debugowania czegoś, co generuje niezdefiniowane zachowanie w czasie wykonywania, co powoduje interfejs API dość łatwe do zrobienia.
Nie mówię, że C nie jest tak dobry. Oczywiście bardziej lubię C ++. Programiści C lubią C lepiej. W grze występują kompromisy i subiektywne upodobania. Istnieje również wiele dezinformacji i FUD. Powiedziałbym, że jest więcej FUD i dezinformacji krążących wokół C ++, ale jestem stronniczy w tym względzie. Na przykład problemy z „wzdęciem” i „wydajnością” w C ++ prawdopodobnie nie są tak naprawdę większymi problemami i na pewno są wyrzucone z proporcji rzeczywistości.
Jeśli chodzi o problemy, o których mówi profesor, nie są one unikalne dla C ++. W OOP (i w programowaniu ogólnym) wolisz kompozycję niż dziedziczenie. Dziedziczenie jest najsilniejszą możliwą relacją sprzężenia, która istnieje we wszystkich językach OO. C ++ dodaje jeszcze jedną, silniejszą, przyjaźń. Dziedziczenie polimorficzne powinno być stosowane do reprezentowania abstrakcji i relacji „jest-a”, nigdy nie powinno być wykorzystywane do ponownego użycia. Jest to drugi największy błąd, jaki możesz popełnić w C ++, i jest to dość duży błąd, ale nie jest on unikalny dla języka. Możesz również tworzyć zbyt złożone relacje dziedziczenia w języku C # lub Java, a będą one miały dokładnie takie same problemy.
źródło
Zawsze uważałem, że niebezpieczeństwa związane z C ++ były bardzo przesadzone przez niedoświadczonych programistów C z Classes.
Tak, C ++ jest trudniejszy do pobrania niż coś takiego jak Java, ale jeśli programujesz przy użyciu nowoczesnych technik, dość łatwo jest napisać solidne programy. Szczerze mówiąc, nie mam o wiele trudniejszego programowania w C ++ niż w językach takich jak Java i często brakuje mi niektórych abstrakcji C ++, takich jak szablony i RAII, kiedy projektuję w innych językach.
To powiedziawszy, nawet po latach programowania w C ++, co jakiś czas popełniam naprawdę głupi błąd, który nie byłby możliwy w języku wyższego poziomu. Częstą pułapką w C ++ jest ignorowanie czasu życia obiektu: w Javie i C # na ogół nie musisz się martwić o czas życia obiektu *, ponieważ wszystkie obiekty istnieją na stercie i są zarządzane dla ciebie przez magiczny śmieciarz.
Teraz, we współczesnym C ++, zwykle nie musisz również przejmować się czasem życia obiektu. Masz destruktory i inteligentne wskaźniki, które zarządzają dla ciebie czasem życia obiektów. W 99% przypadków działa to wspaniale. Ale od czasu do czasu zostaniesz wkręcony przez wiszący wskaźnik (lub odniesienie). Na przykład niedawno miałem obiekt (nazwijmy go
Foo
), który zawierał wewnętrzną zmienną odniesienia do innego obiektu (nazwijmy goBar
). W pewnym momencie głupio zaaranżowałem rzeczy tak, że wcześniej nieBar
wchodziły w zakresFoo
, aleFoo
destruktor ostatecznie wywołał funkcję członkaBar
. Nie trzeba dodawać, że sprawy nie potoczyły się dobrze.Teraz nie mogę winić za to C ++. To był mój zły projekt, ale chodzi o to, że tego rodzaju rzeczy nie miałyby miejsca w zarządzanym języku wyższego poziomu. Nawet z inteligentnymi wskaźnikami i tym podobnymi czasami trzeba mieć świadomość istnienia obiektu przez cały czas.
* Jeśli zarządzanym zasobem jest pamięć, to znaczy.
źródło
Różnica w kodzie jest zwykle bardziej związana z programistą niż z językiem. W szczególności dobry programista C ++ i programista C dojdą do podobnie dobrych (nawet jeśli różnych) rozwiązań. Teraz C jest prostszym językiem (jako językiem), co oznacza, że jest mniej abstrakcji i lepsza widoczność tego, co faktycznie robi kod.
Część jego rantowania (jest znany ze swoich rantów przeciwko C ++) opiera się na fakcie, że więcej osób przejmie C ++ i napisze kod, nie rozumiejąc, co kryją niektóre abstrakcje i nie przyjmują błędnych założeń.
źródło
std::vector<bool>
zmianie każdej wartości?for ( std::vector<bool>::iterator it = v.begin(), end = v.end(); it != end; ++it ) { *it = !*it; }
? Co jest abstrakcyjne*it = !*it;
?std::vector<bool>
jest znanym błędem, ale jest to naprawdę dobry przykład tego, co jest omawiane: abstrakcje są dobre, ale musisz uważać na to, co kryją. To samo może i stanie się w kodzie użytkownika. Na początek widziałem zarówno w C ++, jak i Javie, którzy używają wyjątków do kontroli przepływu, i kod, który wygląda jak wywołanie funkcji zagnieżdżenia, które jest tak naprawdę programem uruchamiającym wyjątki ratunkowe:void endOperation();
zaimplementowane jakothrow EndOperation;
. Dobry programista uniknie tych zaskakujących konstrukcji, ale faktem jest, że można je znaleźć.Nadużywanie
try/catch
bloków.Zwykle wynika to z języków takich jak Java i ludzie będą argumentować, że w C ++ brakuje
finalize
klauzuli.Ale ten kod zawiera dwa problemy:
file
przedtry/catch
, ponieważ nie można tak naprawdęclose
pliku, który nie istniejecatch
. Prowadzi to do „przecieku lunety”, któryfile
jest widoczny po zamknięciu. Możesz dodać blok, ale ...: /return
pośrodkutry
zakresu, plik nie zostanie zamknięty (dlatego ludzie dziwią się z powodu brakufinalize
klauzuli)Jednak w C ++ mamy znacznie wydajniejsze sposoby radzenia sobie z tym problemem, które:
finalize
using
defer
Mamy RAII, którego naprawdę interesującą właściwość najlepiej podsumować jako
SBRM
(Scoped Bound Resources Management).Tworząc klasę w taki sposób, aby jej destruktor oczyścił zasoby, które posiada, nie nakładamy na siebie obowiązku zarządzania zasobami na każdego użytkownika!
To cecha tęsknię w każdym innym języku, i prawdopodobnie ten, który jest najbardziej zapomniane.
Prawda jest taka, że rzadko trzeba nawet pisać
try/catch
blok w C ++, z wyjątkiem najwyższego poziomu, aby uniknąć zakończenia bez logowania.źródło
fopen
ifclose
tutaj.) RAII to „właściwy” sposób robienia rzeczy tutaj, ale jest niewygodny dla osób, które chcą korzystać z bibliotek C z C ++ .File file("some.txt");
i to wszystko (nieopen
, nieclose
, nietry
...)Częstym błędem, który spełnia twoje kryteria, jest niezrozumienie działania konstruktorów kopiowania w przypadku przydzielonej pamięci w klasie. Straciłem rachubę czasu, który spędziłem naprawiając awarie lub wycieki pamięci, ponieważ „noob” umieścił swoje obiekty na mapie lub wektorze i nie napisał poprawnie konstruktorów kopii i destrukterów.
Niestety C ++ jest pełne takich „ukrytych” błędów. Ale narzekanie na to jest jak narzekanie, że pojechałeś do Francji i nie mogłeś zrozumieć, co mówią ludzie. Jeśli tam pojedziesz, naucz się języka.
źródło
C ++ pozwala na wiele różnych funkcji i stylów programowania, ale to nie znaczy, że są to naprawdę dobre sposoby na używanie C ++. W rzeczywistości niepoprawnie łatwo używać C ++.
Trzeba się go nauczyć i właściwie zrozumieć , po prostu uczenie się przez działanie (lub używanie go w inny sposób) doprowadzi do nieefektywnego i podatnego na błędy kodu.
źródło
Cóż ... Na początek możesz przeczytać C ++ FAQ Lite
Następnie kilka osób zbudowało kariery, pisząc książki o zawiłościach C ++:
Herb Sutter i Scott Meyers mianowicie.
Jeśli chodzi o rant Torvaldsa, który nie ma treści ... chodźcie na poważnie: żaden inny język nie miał tyle atramentu na rozlanie niuansów tego języka. Twoje książki w języku Python, Ruby i Java skupiają się na pisaniu aplikacji ... Twoje książki w C ++ skupiają się na głupich funkcjach / wskazówkach / pułapkach.
źródło
Zbyt ciężkie szablony mogą początkowo nie powodować błędów. Z biegiem czasu ludzie będą musieli zmodyfikować ten kod i będzie im trudno zrozumieć ogromny szablon. Wtedy pojawiają się błędy - nieporozumienie powoduje komentarze „Kompiluje i uruchamia”, co często prowadzi do prawie, ale niezupełnie poprawnego kodu.
Ogólnie, jeśli widzę, że robię trzypoziomowy głęboki szablon ogólny, zatrzymuję się i myślę, jak można go zredukować do jednego. Często problem rozwiązuje się przez wyodrębnienie funkcji lub klas.
źródło
Ostrzeżenie: nie jest to tak duża odpowiedź, jak krytyka rozmowy, do której „użytkownik niewiedzy” odsyła w swojej odpowiedzi.
Jego pierwszym głównym punktem jest (podobno) „ciągle zmieniający się standard”. W rzeczywistości przykłady, które podaje, dotyczą zmian w C ++, zanim powstał standard. Od 1998 r. (Kiedy sfinalizowano pierwszy standard C ++) zmiany w języku były dość minimalne - w rzeczywistości wielu twierdziło, że prawdziwym problemem jest to, że należy wprowadzić więcej zmian. Jestem dość pewien, że cały kod zgodny z oryginalnym standardem C ++ nadal jest zgodny z bieżącym standardem. Chociaż jest to nieco mniej pewne, chyba że coś zmieni się szybko (i dość nieoczekiwanie) to samo będzie w zasadzie również w nadchodzącym standardzie C ++ (teoretycznie cały kod, który użył
export
pęknie, ale praktycznie nie istnieje; z praktycznego punktu widzenia nie stanowi to problemu). Mogę wymyślić kilka innych języków, systemów operacyjnych (lub wielu innych elementów związanych z komputerem), które mogą wysuwać takie roszczenia.Następnie przechodzi w „ciągle zmieniające się style”. Ponownie większość jego punktów jest bardzo bliska nonsensów. Stara się scharakteryzować
for (int i=0; i<n;i++)
jako „stary i zepsuty” orazfor (int i(0); i!=n;++i)
„nowy upał”. Rzeczywistość jest taka, że chociaż istnieją typy, dla których takie zmiany mogą mieć sens, ponieważint
nie ma to żadnej różnicy - a nawet gdy można coś zyskać, rzadko jest to konieczne do napisania dobrego lub poprawnego kodu. Nawet w najlepszym razie robi górę z kretowiska.Kolejnym jego twierdzeniem jest to, że C ++ „optymalizuje w złym kierunku” - w szczególności, chociaż przyznaje, że korzystanie z dobrych bibliotek jest łatwe, twierdzi, że C ++ „sprawia, że pisanie dobrych bibliotek jest prawie niemożliwe”. Tutaj uważam, że jest to jeden z jego najbardziej podstawowych błędów. W rzeczywistości pisanie dobrych bibliotek dla prawie każdego języka jest niezwykle trudne. Niemal minimum napisanie dobrej biblioteki wymaga tak dobrego zrozumienia problematycznej domeny, że Twój kod działa dla wielu możliwych aplikacji w tej domenie (lub z nią związanych). Większość tego, co naprawdę robi C ++ , to „podnieść poprzeczkę” - po zobaczeniu, o ile lepsza może być biblioteka , ludzie rzadko są skłonni wrócić do pisania tego rodzaju bzdur, jakie mieliby w przeciwnym razie.naprawdę dobrzy koderzy piszą sporo bibliotek, z których następnie (jak sam przyznaje) mogą korzystać (reszta). Tak naprawdę jest to przypadek, w którym „to nie jest błąd, to funkcja”.
Nie będę próbował trafić w każdy punkt w kolejności (zabierałoby to strony), ale przechodzę bezpośrednio do jego punktu końcowego. Cytuje Bjarne'a mówiąc: „Optymalizacja całego programu może być wykorzystana do wyeliminowania nieużywanych tabel funkcji wirtualnych i danych RTTI. Taka analiza jest szczególnie odpowiednia dla stosunkowo małych programów, które nie używają dynamicznego łączenia”.
Krytykuje to, podnosząc nieuzasadnione twierdzenie, że „To naprawdę trudny problem”, nawet posuwając się do porównania z problemem zatrzymania. W rzeczywistości nie ma nic takiego - w rzeczywistości zrobił to linker zawarty w Zortech C ++ (właściwie pierwszy kompilator C ++ dla MS-DOS w latach 80-tych). To prawda, że trudno jest mieć pewność, że wyeliminowano wszystkie możliwe dane, ale nadal jest całkiem rozsądne, aby wykonać całkiem uczciwą robotę.
Niezależnie od tego jednak znacznie ważniejsze jest to, że jest to absolutnie nieistotne dla większości programistów. Jak wiedzą ci z nas, którzy zdemontowali sporo kodu, chyba że piszesz asembler bez bibliotek, Twoje pliki wykonywalne prawie na pewno zawierają sporo „rzeczy” (zarówno kodu, jak i danych, w typowych przypadkach), które prawdopodobnie nawet o tym nie wiem, nie wspominając o tym, że kiedykolwiek faktycznie używałem. Dla większości ludzi przez większość czasu to nie ma znaczenia - chyba że pracujesz dla najmniejszych systemów wbudowanych, dodatkowe zużycie pamięci jest po prostu nieistotne.
Ostatecznie prawdą jest, że ten rant ma nieco więcej substancji niż idiotyzm Linusa - ale to daje mu dokładnie to cholerstwo z lekką pochwałą, na którą zasługuje.
źródło
Jako programista C, który musiał pisać w C ++ z powodu nieuniknionych okoliczności, oto moje doświadczenie. Jest bardzo mało rzeczy, których używam, czyli C ++ i głównie trzymam się C. Głównym powodem jest to, że nie rozumiem C ++ aż tak dobrze. Nie miałem / nie miałem mentora, który pokazałby mi zawiłości C ++ i jak napisać w nim dobry kod. I bez wskazówek bardzo dobrego kodu C ++ bardzo trudno jest napisać dobry kod w C ++. IMHO jest to największą wadą C ++, ponieważ trudno jest znaleźć dobrych programistów C ++ chętnych do obsługi początkujących.
Niektóre z osiągnięć wydajności, które widziałem, zwykle wynikają z magicznej alokacji pamięci STL (tak, możesz zmienić alokator, ale kto to robi, kiedy zaczyna od C ++?). Zwykle słyszysz argumenty ekspertów C ++, że wektory i tablice oferują podobną wydajność, ponieważ wektory używają tablic wewnętrznie, a abstrakcja jest super wydajna. Stwierdziłem, że jest to prawdą w praktyce w przypadku dostępu do wektora i modyfikacji istniejących wartości. Ale nie jest to prawdą w przypadku dodania nowego wpisu, budowy i zniszczenia wektorów. gprof wykazał, że łącznie 25% czasu na aplikację poświęcono konstruktorom wektorów, destruktorom, memmove (do przeniesienia całego wektora w celu dodania nowego elementu) i innym przeciążonym operatorom wektorowym (jak ++).
W tej samej aplikacji wektor czegośSmall został użyty do przedstawienia czegośBig. Nie było potrzeby losowego dostępu do czegoś Małego w czymś Dużym. Nadal użyto wektora zamiast listy. Powód, dla którego zastosowano wektor? Ponieważ oryginalny koder był zaznajomiony z tablicową składnią wektorów i niezbyt dobrze z iteratorami potrzebnymi do list (tak, pochodzi on z języka C). Następnie udowadnia, że do poprawnego posługiwania się C ++ potrzeba wielu wskazówek ekspertów. C oferuje tak mało podstawowych konstrukcji bez absolutnie żadnej abstrakcji, że możesz to zrobić znacznie łatwiej niż C ++.
źródło
Chociaż lubię Linusa Thorvaldsa, ta reguła jest pozbawiona treści - tylko reguła.
Jeśli chcesz zobaczyć uzasadnione twierdzenie, oto jedno: „Dlaczego C ++ jest szkodliwy dla środowiska, powoduje globalne ocieplenie i zabija szczenięta” http://chaosradio.ccc.de/camp2007_m4v_1951.html Dodatkowy materiał: http: // www .fefe.de / c ++ /
Zabawna rozmowa, imho
źródło
STL i boost są przenośne na poziomie kodu źródłowego. Myślę, że Linus mówi o tym, że C ++ nie ma ABI (binarnego interfejsu aplikacji). Musisz więc skompilować wszystkie biblioteki, z którymi się łączysz, z tą samą wersją kompilatora i tymi samymi przełącznikami, albo ograniczyć się do C ABI na granicach dll. Uważam też to za anonimowe ... ale jeśli nie tworzysz bibliotek stron trzecich, powinieneś być w stanie przejąć kontrolę nad środowiskiem kompilacji. Uważam, że ograniczenie się do C ABI nie jest warte kłopotów. Wygoda przesyłania ciągów, wektorów i inteligentnych wskaźników z jednej biblioteki DLL do drugiej jest warta kłopotów z koniecznością odbudowania wszystkich bibliotek podczas aktualizacji kompilatorów lub zmiany przełączników kompilatora. Złote zasady, których przestrzegam to:
- Dziedzicz, aby ponownie użyć interfejsu, a nie implementacji
-Preferowanie agregacji nad dziedziczeniem
-Preferuj, tam gdzie to możliwe, bezpłatne funkcje dla metod członkowskich
-Zawsze używaj idiomu RAII, aby twój kod był wyjątkowo bezpieczny. Unikaj próby złapania.
-Używaj inteligentnych wskaźników, unikaj nagich (nie posiadanych) wskaźników
-Preferowanie semantyki wartości do semantyki odniesienia
-Nie wymyślaj ponownie koła, użyj stl i doładowania
-Użyj idiomu Pimpl, aby ukryć prywatny i / lub zapewnić zaporę kompilatora
źródło
Nie umieszczanie finału
;
na końcu deklaracji klauzuli, przynajmniej w niektórych wersjach VC.źródło