Publicystyka: C mówimy nie!
Użytkownik: Znajomość C jest warunkiem koniecznym uczenia się C++, prawda?
Stroustrup: Nieprawda. Wspólny podzbiór C i C++ jest łatwiejszy do nauczenia się, niż C. Mniej typów błędów jest koniecznych do ręcznego wyłapywania (system typów C++ jest bardziej ścisły i wyrazisty), mniej sztuczek do uczenia się (C++ pozwala na wyrażanie wielu rzeczy bez stosowania obejść) i dostępne lepsze biblioteki. Najlepsza część wstępna do nauczenia się w C++ to nie jest "cały C". Bjarne Stroustrup FAQ
Jeśli kogoś interesuje, pozwolę sobie jako "praktyczny użytkownik" (choć niektórzy mówią "fanatyk" C++, na odrobinę publicystyki. Zrzuciłem to do osobnego rozdziału, jeśli ktoś nie chce być zanudzany zbyt długim wstępem. ;)
Rozdział ten zresztą zaczął się trochę niepostrzeżenie zbytnio rozrastać i stał się rodzajem "advocacy". Swoją drogą nie sądziłem, że to będzie potrzebne, ale ku mojemu zdziwieniu jest potrzebne, i co więcej, nie wygląda na to, żeby miało za jakiś czas przestać. Zaczęło się oczywiście od tego, że próbowałem wyjaśnić, co mnie zainspirowało do napisania tej, nazwijmy to, pracy.
Nie ukrywam oczywiście, że zainspirowały mnie do tego dwie rzeczy. Pierwsza, to sposób, w jaki język C++ jest wykładany po dziś dzień(!) na polskich uczelniach (ciekawe, czy kiedyś nadejdą takie czasy, żebym mógł wyciąć ten akapit:), a także we współcześnie dostępnych książkach. Może niektórym (zwłaszcza w US) trudno by było w to uwierzyć, ale na wielu polskich uczelniach nadal popularnym kompilatorem jest Borland C++ 3.1 (zresztą na maszynach klasy 386, wciąż je zalegających, nic standardowego uruchomić się nie da). Jest tylko kilka chlubnych wyjątków. Nadal też popularne są książki, które nie tylko wykładają C++ sprzed standardu ISO (1998 rok), ale również kolejność ułożenia materiału pozostawia wiele do życzenia (np. na początku operujemy wszystkim jak w języku C, a na końcu gdzieś są wzorce, a to jak wzorce, to może od razu std::string, listy, wektory, kolejki...). Druga rzecz z kolei, to mnóstwo mitów nt. języka C++, wciąż będących na topie w obiegowych opiniach.
Dlaczego zatem "bez cholesterolu"? Przede wszystkim – bo to jest ostatnio modne ;*). A tak na poważnie – ma to oznaczać przedstawienie C++ jako języka, który wcale nie jest wypasiony w nie wiadomo co i przez to naobrastał w tłuszcz. Również jako przeciwwagę dla wciąż przez wielu reklamowanego C, co do którego panuje kilka dość rozpowszechnionych opinii (nie mających nic wspólnego z prawdą), jak np. to, że język C jest łatwiejszy, "chudszy" i szybszy od C++. W wyniku tego wielu amatorów programowania zajęło sie C, a nie C++ (co widać po ilości oprogramowania OpenSource pisanego w C). Potem stosowali oni wyuczone praktyki z doświadczenia z językiem C. Efekt, jak można zaobserwować, jest taki że ci ludzie już się sensownego sposobu programowania w C++ nauczyć często nie potrafią – a wspomniane mity nt. języka C (dzięki również nim) nadal krążą.
Możesz mi zresztą wierzyć lub nie, ale uczenie się języka C zanim się pozna język C++ jest zwykłą stratą czasu. Po pierwsze, język C++ i tak przyda ci się bardziej, niż język C. Po drugie, jeśli nawet język C będzie ci potrzebny, zawsze można nauczyć się go jako "bardziej surową i okrojoną" wersję języka C++, co będzie znacznie łatwiejsze. Nikt przecie nie zaczyna nauki od asemblera, lecz od jakiegoś mniej wymagającego języka, a poza tym:
- w języku C, żeby operować napisami musisz się nieźle namęczyć z ręcznym klejeniem tablic znaków; uczenie się tego nie będzie ci potrzebne w C++, bo tam - jak we wszystkich normalnych językach - jest typ 'string'.
- w języku C jedynym standardowym zbiornikiem jest tablica surowa, która ma niezmienny rozmiar (jeśli ktoś chciałby oponować, że w najnowszym C są VLA, to przypomnę, że tam rozmiar tablicy można, owszem, dobrać, ale tylko podczas jej tworzenia); nie będziesz miał zatem możliwości nawet potrenowania z różnymi zaawansowanymi zbiornikami (typu lista, kolejka, zbiór, mapa), jeśli ich sobie sam nie napiszesz; w efekcie nie będziesz się w C uczyć programowania, tylko "wynajdywania koła na nowo", co również nie będzie ci potrzebne w C+
- w języku C jedynymi narzędziami generyzmu jest uniwersalny wskaźnik na kawałek pamięci, która "być może zawiera to, czego potrzebujemy" oraz preprocesor (na którym wszyscy od wieków wieszają psy); w C++, jeśli potrzebujesz generyzmu, masz do dyspozycji programowanie obiektowe i wzorce, które są zarówno wygodniejsze, jak i mniej wrażliwe na błędy
- w języku C przyzwyczaisz się, że aby wykorzystać jakąkolwiek zbiorczość klasyfikacji typów danych, musisz używać rzutowania wskaźników na strukturę (polecam np. opis biblioteki BSD sockets i obejrzenie, jak przekazuje się strukturę adresu komputera zdalnego z dziedziny internetowej w funkcjach connect i bind)
- no i - last but not least - w języku C nie będziesz miał możliwości zapoznania się z nowoczesnymi (logicznymi) technikami programowania, ani przede wszystkim nauczenia się odpowiedniego sposobu myślenia, który pozwoliłby ci na tworzenie systemów w sposób łatwiejszy i bardziej niezawodny (wiem, co mówię, bo spotkałem się już z niektórymi programistami, którzy nie potrafili wyjaśnić działania algorytmu inaczej, niż operując pojęciami typu wskaźnik i pamięć); kiedy zaś przyzwyczaisz się do takiego sposobu podejścia do programowania, będzie ci bardzo trudno się odzwyczaić (a odzwyczaić się będzie trzeba, jeśli naprawdę chcesz programować zawodowo).
Nie raz słyszałem przy propozycjach poważniejszego potraktowania C++ stwierdzenia, że "po co to C++, czy zamierzamy programować obiektowo?". Nawet nie chciałoby mi się machnąć na to ręką, bo po samej tej wypowiedzi można już się domyślić, że taki gościu o C++ co najwyżej słyszał i to też niezbyt wiele. Ale niestety takie mamy czasy, że tacy ciemniacy decydują dziś o wielu istotnych sprawach, więc przynajmniej trzeba umieć im pokazać, że się wie, o czym mówi. A na C++ przejść warto dlatego, że:
- C++ mamy znacznie bardziej rygorystyczny system typów, a co za tym idzie, pozwala na bardziej rygorystyczne zapewnienie formalnej poprawności programów. Bardzo dużo "potencjalnie niebezpiecznych" sposobów programowania w języku C, jest w C++ zabronione. A któż dzisiaj w produkcji oprogramowania nie potrzebuje w języku właściwości pozwalających na bezpieczne programowanie?
- System typów języka C++ da się nakierować tak, żeby go wykorzystać na nasze potrzeby - w odróżnieniu od systemu typów języka C, który trzeba często łamać, głównie rzutowaniem na void*. W C++ da się oprogramować daną koncepcję w ten sposób, żeby uniemożliwić lub maksymalnie utrudnić użytkownikom jej łamanie, także w przypadkowy sposób.
- W języku C++ mamy znacznie więcej możliwości oddzielania "interfejsu od implementacji", a główna zaleta tego polega na możliwości tworzenia wygodnych i bezpiecznych interfejsów do różnych elementów programu.
- Język C++ posiada wiele właściwości, które w języku C wymagają użycia obsobaczanego wciąż preprocesora.
- A jeśli w danym elemencie programu żadna z tych powyższych właściwości nie będzie miała żadnego znaczenia, możemy wciąż programować w dokładnie ten sam sposób, co w C.
Ale wśród obrońców języka C napotykamy wciąż mnóstwo "defensywnych" opinii, z którymi dość wielu ludzi się zgadza, np.:
1. Język C jest prostszy od C++, więc łatwiej się go nauczyć
Twierdzenie to popiera się sugestiami, że przecież skoro C++ zawiera tyle różnych właściwości, wspiera tyle różnych technik programowania, że jest znacznie bardziej skomplikowany, więc nauczyć się go będzie trudniej. Na upartego można by tu przyznać rację, chociaż trudno stwierdzić, czy taki ktoś rzeczywiście wie, o czym mówi. Skomplikowane techniki programowania są dziś tak wszechobecne, że w każdym języku, tak samo w C, jak i w C++, trzeba umieć się nimi posługiwać. Idiotyzm tego stwierdzenia zatem polega na tym, że sugeruje się, jakoby w C++ trzeba było opanować obsługę każdej możliwej techniki programowania, podczas gdy w C można programować w "starym dobrym stylu proceduralnym". Przecież to bzdura - jeśli dla danego programu wystarczają techniki proceduralne, to wystarczy znać C++ tylko na poziomie udostępniającym te właśnie techniki i już można programować. Skoro jest prawdą, że poznanie technik proceduralnych wystarczy do programowania w C, to dlaczego nie miałoby to wystarczyć do programowania w C++?
A jeśli z kolei ktoś odczuwa potrzebę stosowania bardziej skomplikowanych technik programowania, to może się okazać, że na naukę języka C niepotrzebnie stracił czas. Bo używanie tych technik w C jest, owszem, możliwe, tyle że to jest istna katorga. Zresztą, nie potrzeba tu nawet wchodzić w jakieś bardzo skomplikowane obszary, by pokazać, że niemal każdy program da się napisać w C++ łatwiej, niż w C. A co do uczenia się?
Zwolennikom twierdzenia, jakoby C można było się nauczyć łatwiej, proponowałbym nauczyć początkującego obsługi funkcji printf i zmiennych tekstowych (i niech on sobie to porówna choćby z write i stringiem z pascala). Istnieje przecież tyle "prostych" języków: Lisp, Smalltalk, czy nawet asembler. Asembler ma przecież tylko kilka rozkazów i wszystko robi się w podobny sposób – dlaczego zatem asembler nie jest popularniejszy od C (nb: kiedyś był ;)? Przede wszystkim dlatego, że nie da się w nim zrobić niczego w sposób wygodny i czytelny. Z C jest już lepiej, ale różnica pomiędzy C a C++ przedstawia się dość podobnie.
Mówi się też o C++, że nie da się go opanować w "rozsądnie krótkim czasie". Zarzuca się temu językowi, że jest bardzo skomplikowany i "przez stanowcze stwierdzenie" dowodzi się, że to jest dla języka programowania "bezwzględnie złe". Czy rzeczywiście? Programowanie niestety jest skomplikowaną dziedziną i proste języki programowania nie dostarczają niestety narzędzi do rozwiązywania wszystkich problemów. C++ jest skomplikowany? Możliwe, ale głównie dlatego, że udostępnia użytkownikowi bardzo wiele sposobów programowania. Jeśli ktoś uważa, że jest zbyt skomplikowany, to prosiłbym również o podanie jakiegokolwiek innego języka, który udostępnia tyle samo możliwości i jest mniej skomplikowany. C# i Java do tego aspirują, ale im to na razie specjalnie nie wychodzi; co więcej, w miarę "doganiania C++" w udostępnianych właściwościach, przeganiają go w stopniu komplikacji. Już obecnie ta "łatwa i przyjemna" Java znacznie przegoniła C++ jeśli chodzi o stopień skomplikowania.
Oczywiście, że język C++ jest o wiele bardziej skomplikowany, niż np. C, ale też z drugiej strony porównywanie z C pełnego C++, który posiada takie właściwości, jakich w C nie da się w sensowny sposób uzyskać, jest bezsensem. Żeby mieć jakiekolwiek miarodajne porównanie, należałoby porównywać w tym zakresie, w którym te języki się pokrywają, w zakresie który udostępnia obu tym językom podobne właściwości. Zatem w C++ należałoby dać sobie spokój zarówno z metodami wirtualnymi, dziedziczeniem i wzorcami (również przeciążaniem), a pozostawić przede wszystkim referencje, przesuwanie zmiennych lokalnych, słowo struct przed nazwą typu, funkcje inline. Dopiero wtedy sprawdzić, jaki jest faktyczny stopień skomplikowania tak okrojonego C++. I po zebraniu tego wszystkiego okazuje się niestety, że C++ wcale nie jest trudniejszy, a wręcz przeciwnie. Jeśli dodamy do tego jeszcze biblioteke standardową, dzięki której łatwiej się używa różnych podstawowych typów danych (jak choćby string) i porównując do tego sposoby uzyskiwania tego samego w C, proporcje te wychodzą jeszcze gorzej (wszystkim ewentualnym oponentom zaznaczam, że używanie typów polimorficznych, którym np. ostream niewątpliwie jest, bynajmniej nie oznacza używania jakichkolwiek bardziej skomplikowanych technik, niż przy printf). Dodatkowo, znajomość C++ jest użyteczna nawet w takim ograniczonym zakresie. Porównywać możemy jeszcze takie rzeczy, jak np. komplikacje projektów robionych w C i C++. Oczywiście trudno o tym mówić jednoznacznie, ale co do projektów robionych podobnymi metodami co do C z ostrożnym wsparciem zaawansowanych możliwości, również okazuje się, że w C++ taki projekt robi się prostszy. Owszem, bywało w C++ sporo projektów rozrośniętych ponad miarę, ale widywałem też projekty w C++, które były przenoszone z C i zawierały one co najmniej połowę mniej kodu przy tej samej funkcjonalności (podpowiem też, że używany w wielu firmach softwarowych przelicznik rozkazów asemblera na linijki kodu jest wyższy dla C++, niż dla C). Może to nie dowodzi, ale na pewno to sugeruje, że C++ jest jednak językiem wyższego poziomu, niż C. A w każdym razie, że można w nim pisać na wyższym poziomie.
Nie twierdzę zatem, że "całego" C++ można nauczyć się szybciej, niż C. Można jednak szybciej, niż C, nauczyć się podzbioru C++ dającego te same możliwości, co język C (tzw. "lepszego C"). Dzięki bardzo rozbudowanej bibliotece standardowej można przećwiczyć programowanie w C++ na przykładach bliższych codziennemu życiu i praktyce, bez potrzeby definiowania wszystkich drobnych szczegółów od zera, jak to jest w przypadku języka C. Takimi rzeczami jak np. wskaźniki i obiekty dynamiczne można się zająć znacznie później, mając już za sobą przećwiczone operacje na skomplikowanych typach danych. W języku C natomiast trzeba poznać wskaźniki i obiekty dynamiczne jak najszybciej, bo bez ich znajomości nie da się operować nawet czymś tak banalnym jak napisy, czy operacje wejścia-wyjścia, o skomplikowanych strukturach danych nie wspominając.
Przykładowo, kiedy nowicjuszowi mówi się o operowaniu na zmiennych tekstowych, można poprzestać na podstawowych wiadomościach o typie `string'. Opuszcza się w ten sposób spory kawał dość trudnego na początek tematu, a jednocześnie początkujący od razu może pobawić się zmiennymi tekstowymi. Podobnie, wysyłanie tekstów i liczb do strumienia jest łatwiejsze do objaśnienia, niż tłumaczenie, po co jest to całe %d, %s i dlaczego printf jest taka skomplikowana (formatowanie zawsze można odłożyć na później). W C bez wyłożenia dokładnych informacji o tym, co to są tablice, jak jest zbudowany łańcuch znaków,, jak się go kopiuje, po co w scanf przekazuje przez wskaźnik (co nie jest ani krótkim, ani łatwym tematem) zmiennych tekstowych nie da się używać nawet do najbanalniejszych operacji (a o bardziej skomplikowanych, jak wycinanie i sklejanie napisów to już nie wspomnę), podobnie jak poważniejsze zastosowania printf są niemożliwe bez denerwującego określania wprost, jakiego typu jest wypisywana wartość (o pomyłkę tu nietrudno, przy scanf jeszcze bardziej).
Argument, z którym się również zetknąłem, to taki, że dopuszczenie ludzi do C++ zaowocuje robieniem mnóstwa dziwnych i głupich rzeczy. Że np. możliwość przeciążania operatorów doprowadzi do tworzenia różnych, niejednoznacznych definicji. Że przeciążanie funkcji doprowadzi do zamieszania definicyjnego. Podobnie można mówić też o programowaniu generycznym i obiektowym. Moja praktyka jednak nie potwierdza tego. Okazuje się, że największe potworki robią ludzie, którzy nie potrafią z właściwości C++ zrobić sensownego użytku. Inaczej mówiąc, ci którzy dobrze znają C++ i umieją z niego korzystać, wykorzystują jego właściwości w sensowny i przemyślany sposób, podchodząc do wszelkich jego właściwości z ostrożnością. Ci, którzy nie potrafią, programują w C++ tak samo, jak w C i to ich twory są zazwyczaj ciężkie w utrzymaniu i ciężko skalowalne.
2. Język C++ ma obciążenia w stosunku do C, więc programy w nim będą wolniejsze
Zauważałem niejednokrotnie tego typu stwierdzenia, ale nikt, kto tak mówił, nie zdołał podać żadnych konkretów. Można by jednak spróbować zrobić małą analizę poszczególnych właściwości C++:
- Udogodnienia składniowe typu brak konieczności pisania struct przed nazwą typu, czy możliwość deklaracji zmiennej w dowolnym miejscu, nie wpływają na kod w najmniejszym stopniu.
- Klasy monomorficzne. Są definicyjnie identyczne ze strukturami.
- Metody. Metody to po prostu najzwyklejsze funkcje, takie same jakie są w C. Różnica jest tylko w składni wywołania. W szczególności, wywołanie metody na rzecz obiektu nie różni się od wywołania funkcji, gdzie ów obiekt przekaże się przez wskaźnik jako pierwszy argument.
- Przeciążanie funkcji i operatorów. Na ten temat można powiedzieć to samo, co nt. metod.
- Konstruktor i destruktor. Takoż są to najzwyklejsze funkcje. Ich szczególność polega tylko na kwestiach składniowych: konstruktor jest wywoływany w momencie deklaracji, a destruktor na zakończenie obiektu nadrzędnego (w tym również kontekstu). Destruktory mogą powodować obciążenie tylko jeśli używa się ich razem z wyjątkami (a o nich za chwilę).
- Klasy polimorficzne i metody wirtualne, owszem, stanowią drobne obciążenie. Z tym tylko, że ich stosowanie nie jest do niczego konieczne. Dostarczają one swoje właściwości, które mogą być zaimplementowane w inny sposób, np. przez popularną w C technikę switch/case. Która z kolei jest często wolniejsza od polimorfizmu. Ewentualnie zdarza się stosować w C dokładnie tą samą technikę, którą stosuje polimorfizm w C++ (np. kolejkowanie zadań z uwzględnieniem procesów czasu rzeczywistego w Solarisie), czy też techniki wywołań zwrotnych oparte na wołaniu funkcji przez wskaźnik. Wszelkie te alternatywne metody można również stosować w C++, jeśli ktoś uważa, że będą lepsze pod jakimś względem.
- Wzorce. Ich obciążenie można porównać z używaniem preprocesora w C.
- Funkcje inline, tak, powodują obciążenie objawiające się tym, że powiększa się segment kodu, bo te funkcje zamiast być wywoływane, jak na funkcje przystało, są wklejane w miejscach, w których się je powinno wywołać... :)
- Wyjątki. Podobnie jak polimorfizm, należą do grupy "opcjonalnych obciążeń". Tzn. powodują obciążenia tylko, jeśli się ich używa, przy czym można używać innych technik i osiągnąć mniej więcej to samo (z obciążeniem włącznie). Z wyjątkami jest akurat trochę nie do końca tak; to zależy, co przyjmiemy jako alternatywę. Jeśli przyjmiemy propagację ręczną przez wartości zwracane, to jest to technika znacznie trudniejsza w stosowaniu (trzeba zawsze wyznaczyć jakąś uniwersalną wartość zwracaną oznaczającą niepowodzenie), ale bez obciążeń i całkiem skuteczna. Jeśli przyjmiemy setjmp/longjmp, co jest najbardziej naturalną alternatywą, to niestety trzeba pamiętać, że do tego celu trzeba też zrezygnować z destruktorów, a w szczególności z używania typów, które nie są trywialnie-destruowalne, nie jest to zatem skuteczna alternatywa. Zaznaczam też, że w niemal żadnym języku wyjątki nie powodują takich obciążeń, jak w C++, ale te języki z kolei nie posiadają destruktorów.
Podsumowując: techniki konieczne do programowania w C++ nie powodują żadnych obciążeń podczas wykonania w stosunku do języka C. Niektóre techniki dodatkowe, mające swoje alternatywy w C, powodują pewne koszty. Zazwyczaj te alternatywne techniki jednak nie są mniej kosztowne, a często też są gorsze od strony projektanckiej. Jeśli ktoś chciałby dodać jeszcze coś do powyższych punktów – zapraszam.
3. Język C jest najszybszy, bo jest bliski asemblera
Ten mit jest dość trudno obalić. Równie trudno było obalić mit, że asembler jest najszybszym językiem programowania, co odstraszało początkowo ludzi od C (sam w to kiedyś wierzyłem). Ten mit jednak został już dawno obalony, mianowicie wydajne programowanie w asemblerze w dzisiejszych czasach wymaga znajomości tylu różnych kruczków w procesorze, że programista nie ma szans tego wszystkiego zapamiętać, natomiast kompilator pamięta i stosuje to bez problemów (słyszałem od niektórych, jak podjęli rywalizację z kompilatorem o optymalizację na poziomie asemblera i przegrali!). Z językiem C jest trochę inna sprawa, bo on przypomina bardzo język wysokiego poziomu, choć w rzeczywistości jest przenośnym asemblerem.
Osławione, a nie do końca rozumiane przez wielu wzorce, stały się niestety źródłem takiej wydajności (zarówno czasu tworzenia projektu jak i wydajności programu), jakiej w C uzyskać nie sposób. Pewien artykuł na Dr. Dobb's Journal prezentuje praktyczne przykłady na to, jak wzorce mogą zwiększać wydajność poprzez dostarczanie założeń, że wskaźniki nie mają aliasów (słowo restrict w C99 okazało się również półśrodkiem). Dzięki wzorcom i jednocześnie dzięki temu, że wskaźniki są do różnych typów, kompilator od razu może założyć ich niealiasowość jak również często niepokrywanie się zakresów, co mocno upraszcza (i uwydajnia) kod asemblerowy. Oczywiście, że wiele kompilatorów jeszcze nie optymalizuje aż tak dobrze, ale i tak kod jest nadal szybszy, niż jego odpowiedniki zależne od elementów czasu-wykonania.
Trochę bardziej skomplikowany jest argument związany z generyzmem. Ponieważ może być tego dość sporo, więc najważniejsze zdania będę zaznaczał wyraźnie; jak ktoś nie chce czytać całości, niech idzie tylko po nich. Pod koniec dałem też małe podsumowanie :).
Zanim rozważymy, jak istotne znaczenie dla szybkości oprogramowania ma fakt bliskości asemblera, może najpierw rozważmy, na co w dzisiejszej inżynierii oprogramowania przeznacza się najwięcej sił i środków. Szybkość działania programu nie jest wcale najistotniejsza, wbrew temu co się niektórym wydaje. Otóż,
najwięcej zasobów podczas tworzenia oprogramowania idzie na jego jakość.
I mówiąc "jakość" nie mam bynajmniej na myśli stosunku złożoności problemu do zużycia zasobów (to się nazywa "sprawność", ang. performance). Mówię tutaj o czymś bardziej przyziemnym, mianowicie stosunku założeń funkcjonalnych programu do ich faktycznej realizacji przez program. Innymi zatem słowy, programista przez większość swojego czasu (niektóre szacunki mówią o 70%, a nawet 80%) powiedzmy upewnia się, że program działa tak, jak powinien działać (w firmach o marnej organizacji pracy z kolei walczy z bugami).
Zastanówmy się zatem, skąd biorą się bugi. Większość na pewno zaraz zaproponuje, że "przez niedopatrzenie programisty". Niestety, jest to odpowiedź brzmiąca jak w pewnym dowcipie "w balonie" – jest stuprocentowo prawdziwa i stuprocentowo nikomu nieprzydatna. Jakość pracy pojedynczego programisty (poza pewnymi bardzo szczególnymi przypadkami) nie jest znaczącym czynnikiem, a w każdym razie na ten czynnik wpływać się specjalnie nie da (poza odpowiednią organizacją pracy), a przynajmniej nie za wiele. Dużo istotniejszy jest ten mniej widoczny, za to jedyny znaczący czynnik, mianowicie ilość POTENCJALNYCH pomyłek. Należy bowiem wiedzieć, że
ilość potencjalnych pomyłek jest równa ilości okazji do ich popełnienia.
Dlatego właśnie największe wysiłki w tworzeniu metod i języków programowania (i to począwszy od asemblera) były ukierunkowane właśnie na zminimalizowanie owych okazji do popełniania pomyłek. To ma oczywiście jeszcze inny cel, mianowicie upewnianie się (i tworzenie odpowiedniego oprogramowania testującego i wspomagającego testowanie) jest bardziej czasochłonne (a więc kosztowniejsze) w przypadku źródeł o dużej ilości potencjalnych pomyłek.
Żeby powiedzieć "nie-wprost", do czego tak naprawdę dążono w tych wysiłkach, zastanówmy się, jak zwiększyć ilość potencjalnych pomyłek. Jak wiadomo, kiedy powstaje program, mnóstwo jego elementów pracuje w dość podobny sposób lub posiada podobną konstrukcję. W asemblerze zatem dochodziło często do powtarzania tych samych kawałków kodu. Otóż jednym z najskuteczniejszych sposobów powiększania ilości okazji do popełniania pomyłek jest znana dobrze wszystkim programistom, zwłaszcza początkującym, tzw. metoda Copy'ego-Paste'a. Przede wszystkim za jej skutecznością we wspomnianej dziedzinie przemawia fakt, że
jeden błędny fragment kodu przekopiowany dziesięć razy daje dziesięć błędnych fragmentów kodu.
Jest to mniej więcej uproszczona definicja pierwszego prawa Copy'ego-Paste'a. Zastrzegam tylko od razu (może ktoś nie wie), co oznacza tak naprawdę metoda Copy'ego-Paste'a. Otóż nie oznacza ono wyłącznie skopiowania kodu. Oznacza ono skopiowanie kodu i dokonanie w nim drobnych zmian adoptujących do innego otoczenia (miejsca owych zmian nazwijmy punktami specjalizacji). Praw Copy'ego-Paste'a jest kilka, które może tu dla porządku wymienię:
Założenie: Niech będzie dany pewien fragment programu, którego funkcjonalność jest potrzebna w N nowych miejscach, w związku z czym dokonuje się skopiowania tego fragmentu kodu (zwanego źródłowym) N razy w inne miejsca (zwane kodem docelowym), zmieniając tylko zewnętrznie zależne szczegóły tego fragmentu.
- Jeśli kod źródłowy zawiera X defektów, to kod docelowy posiada N*X defektów.
- Jeśli do dokonania zmian funkcjonalności fragmentu źródłowego potrzeba X+C wysiłku, to do dokonania równoważnej zmiany w kodzie docelowym potrzeba N*X+C wysiłku. Stała C jest tutaj wysiłkiem włożonym w przygotowanie projektu zmian, która jest czynnikiem niewielkim w porównaniu z X.
- Jeśli do przetestowania jednostki zawierającej kod źródłowy potrzeba X "testkejsów" to do przetestowania jednostki zawierającej kod docelowy potrzeba N*X "testkejsów".
- Każdy punkt specjalizacji (tzn. miejsce, które należy zmodyfikować po dokonaniu kopiowania) jest dodatkowym miejscem okazji do pomyłki.
- Dla kopiowania "następny z poprzedniego", w każdym wzorze powyżej, X należy zastąpić 2X.
Metoda Copy'ego-Paste'a jest często (nadal!) stosowana i to często z uwagi na brak w językach programowania odpowiednich metod pozwalających na jego unikanie (w czym przoduje język C) – ale również z uwagi na brak wiedzy i umiejętności niektórych programistów.
Co jest zatem przeciwieństwem copy-paste? Musimy zdawać sobie sprawę, że nie wystarczy tutaj zrobić kod raz i użyć w wielu miejscach (podprogramy), ale również uzależnić ten kod od kilku elementów tak, żeby był stosowalny w wielu miejscach, również z uwagi na różnorodność struktur danych (często nazywa się to "reużywalnością", ang. reusability). Jednak często jest to przeróbka pod konkretne zastosowanie. A niestety
aby zmniejszyć ilość pomyłek, trzeba zmniejszyć ilość kodu, nie zmieniając jego funkcjonalności.
Wszystkim fanom języków o krótkiej składni leję od razu kubeł zimnej wody na głowę: ilość kodu to nie jest ilość linii, ani wielkość plików źródłowych w bajtach. Jest to ilość względna, pasująca do konkretnego języka programowania. Przydałoby się zatem pisać kod adoptowalny do szerokiej gamy zastosowań o których nie wiadomo w momencie jego pisania. Taka właściwość kodu nazywana jest GENERYZMEM (i zgodnie z zasadami słowotwórstwa, zwiększanie poziomu generyzmu kodu można krócej nazwać "generycyzacją" – ewentualnie "ugenerycznianiem"). Otóż,
najłatwiejszym możliwym sposobem generycyzacji kodu jest zastosowanie dynamizmu.
To znaczy: zamiast wstawiać identyczny kod ponownie, zorganizowano podprogramy z argumentami. Zamiast powtarzać dany kod po kilka razy dla kolejnych elementów, można zrobić pętlę, która zawrze w sobie kod iterowany zmienną sterującą. Zwracam tutaj też uwagę na kilka innych rzeczy. Przede wszystkim, podprogramy faktycznie służą zmniejszeniu wielkości kodu. Pętle już raczej nieznacznie. Następnie, jeśli chodzi o pętle i podprogramy to to są właściwości dynamiczne. Mianowicie przy pętlach operacje mogą być zależne nie od zahardkodowanych wartości, tylko od jakichś wartości powstałych w programie (wartości "dynamicznych"). Ale tu się z tych własności nie korzysta; dynamizm jest wykorzystywany tylko w celu "skrócenia zapisu", czyli generycyzacji (współczesne kompilatory mają rozwijanie pętli jako opcję optymalizacji). W C często idzie się dalej. Tutaj zamiast kopiowania kodu, można go uzależnić od różnych dynamicznych właściwości. Stosuje się do tego funkcje. Zatem zamiast hardkodować wartości i kopiować używające je fragmenty, wpisuje się je do odpowiednich struktur (lub podaje jako argumenty funkcji), a funkcja to już odpowiednio przetworzy. Zwracam tutaj uwagę na różnicę w szybkości, którą daje się już zaobserwować. Przy większej komplikacji danych, wszystkie obliczenia muszą zostać przeprowadzone na bieżąco przez proces. Gdyby zamiast tych obliczeń stosowano copy-paste, to oczywiście kod przyspieszyłby nawet sporo, bo wtedy nie program by je obliczał, tylko obliczyłby je kompilator i do programu powstawiał tylko wyniki (ewentualnie policzyłby sobie użytkownik podczas dokonywania specjalizacji). Jednak przez copy-paste utracilibyśmy generyzm. Coś za coś, czyli
dynamizm kosztuje.
O tym przekonało się wielu twórców języków programowania. Choćby Smalltalk, który miał być niby językiem w pełni dynamicznym, stosuje statyczne właściwości wszędzie, gdzie to tylko możliwe (tzn. jeśli uzyskany efekt będzie ten sam). A przynajmniej tak robią dobre, dopracowane kompilatory (GNU Smalltalk do nich nie należy i efektywnie nadaje się tylko do nauki tego języka). Co nam zatem oferuje lepszego C++? Dwie bardzo istotne właściwości: funkcje inline (argumentu, że większość kompilatorów C je implementuje, nie przyjmuję!) oraz wzorce. Właśnie dzięki wzorcom można zrobić coś pośredniego między hardkodowaniem a kodem zbiorczym. Tym samym proponowałbym więc porównać szybkość funkcji z zahardkodowanymi (za pośrednictwem wzorców) wartościami i to samo napisane w C, gdzie taka wartość jest argumentem funkcji. Dzięki wzorcom zatem
C++ umożliwia hardkodowanie bez utraty generyzmu,
Czyli pozwala na uzyskanie tego samego efektu w kodzie wynikowym, co copy-paste, ale równocześnie ze zmniejszeniem ilości linii kodu i powtarzających się fragmentów. Wypadałoby zatem przypomnieć wszystkim zwolennikom języka C, że parametryzowany kod (poza używaniem preprocesora, co jest niezalecane przez wszelkie standardy kodowania) robi się tam za pomocą funkcji (często uzależniając wybór fragmentu kodu do wykonania za pomocą switch), a parametryzowane dane przez używanie wskaźnika void*. Wszystko z wyborem fragmentów do wykonania lub interpretowaniem danych przez "odpowiednie" wyciąganie ich bezpośrednio z pamięci, w czasie działania programu (w tej dziedzinie biją go na głowę nawet języki z kompilatorami JIT, gdzie obliczenia wykonuje się co prawda podczas wykonywania programu, ale raz, a nie za każdym dostępem). Efektywnie zatem
W języku C generyzm uzyskuje się za pośrednictwem dynamizmu, który stanowi narzut podczas wykonania, gdy w C++ używając wzorców można uzyskać to samo, bez narzutu podczas wykonania.
Podsumowując zatem to wszystko, co napisałem wyżej: od generyzmu się nie ucieknie. Programować metodą copy-paste można, owszem, ale tylko do pewnego momentu. Zatem istotne jest raczej to, jakie możliwości w uzyskaniu generyzmu ma dany język, a dokładnie, jak w nim trzeba programować, żeby programować generycznie. W języku C aby uzyskać generyzm musimy stosować metody dynamiczne, które powodują obciążenia podczas wykonywania i są niepomiernie bardziej zawodne. W języku C++, dzięki istnieniu wzorców, mamy możliwość uzyskania generyzmu nie pozbywając się zalet statyzmu (brak obciążeń podczas wykonywania i większa niezawodność).
Jak zatem widać, C++ o wiele lepiej niż C spełnia zasadę "nie płać za to, czego nie używasz" (co było zresztą podstawową zasadą obowiązującą podczas jego tworzenia).
4. Język C jest lepszy dla małych projektów i programowania na niskim poziomie
Żeby ostatecznie się rozprawić z mitem, jakoby ktoś C++ w ogóle nie potrzebował (jeśli ma C), spróbuję się też odnieść do wypowiedzi, jak to próbowano bronić używania języka C w bardzo specyficznych środowiskach (oczywiście takich, dla których istnieje kompilator C++). Spróbuję tu przytoczyć spotykane wypowiedzi już nawet takie bardziej stonowane i sugerujące wręcz skłonność do kompromisu:
Język C++ zawiera, owszem, wiele wypasionych możliwości, więc na pewno bardzo się przyda w bardzo skomplikowanych projektach, gdzie korzysta się z rozbudowanych danych, również jeśli musimy zrobić jakieś skomplikowane GUI. Natomiast w tej małej aplikacji nie ma potrzeby angażowania aż C++, można to również napisać w C.
Naprawdę? W takim razie oto lista przykładów, które pokazują, jak to język C "dobrze się nadaje do małych projektów" (w tym do w ogóle jakichkolwiek projektów):
- W języku C w bardzo problematyczny sposób obsługuje się napisy (a jest to jeden z najczęściej, po liczbach całkowitych, używany typ danych). Jest to już od dłuższego czasu znany problem i od dłuższego czasu praktycznie nikt z nim niczego nie zrobił (a w międzyczasie wyszedł niby nowy standard!). Żeby operować napisami w języku C, trzeba albo używać tablic statycznych i uważnie operować zakresami, albo używać tablic dynamicznych i skazywać się na koszmary ręcznego zarządzania zasobami. Zresztą napisy powinno się móc sklejać, czy dzielić – a robienie tego z napisami w C to naprawdę istny koszmar (o marnej wydajności, związanej z implementacją strcat i zerem terminującym, już nie wspomnę). Zresztą, nawet zrobienie jakiejś biblioteki do obsługi napisów też nie rozwiąże wielu problemów. W C++ mając do dyspozycji takie proste narzędzie jak klasa (ułatwienie w tym wypadku wyłącznie składniowe), możemy dać użytkownikowi coś, co wygląda z zewnątrz na nie bardziej skomplikowane, niż zwykła liczba całkowita, a wewnątrz można wykonywać skomplikowane operacje i korzystać z wielu szczególnych warunków. Czy takie coś da się zrobić w języku C mając do dyspozycji tylko wywoływanie funkcji? Nie bądźmy śmieszni.
- W języku C nie istnieje, poza wbudowaną tablicą, żaden standardowy zbiornik. Jeśli potrzebujesz tablicy dynamicznej (samorozrastającej się!), listy, stosu, kolejki, tablicy mieszanej – nikt ci tu nie zaoferuje najmniejszej pomocy (no, chyba że sobie użyjesz gliba), musisz ręcznie dłubać się ze strukturami ze wskaźnikami (głównie void*). Jakikolwiek dynamiczny zbiornik z kolei powoduje kolejne koszmary ręcznego zarządzania zasobami. Ta rzecz ma szczególnie bardzo istotne znaczenie w małych projektach, bo nikt zwykle nie zamierza definiować wszystkich szczegółów jakiegoś prymitywnego typu danych, żeby sobie go użyć potem w jednym miejscu. Może i C posiada jakieś-tam drobne możliwości stworzenia takiego w miarę generycznego typu danych, tyle że byłby on niezwykle toporny w użyciu i ciężki w utrzymaniu (no, chyba że założymy, że twórcy np. systemów operacyjnych są skończonymi idiotami i nie pomyśleli, że tak można, tylko zrobili "listowatą" każdą niemal strukturę, ale to wyjaśnienie raczej z przyczyn zasadniczych odpada). Jeśli nawet skorzystasz z takowego, jesteś wciąż skazany na rzutowanie, bo jedyny typ podległy, którego można użyć w przypadku bibliotecznej listy w C jest void*. Proponowałbym przyjrzeć się dobrze bibliotece GTK+, żeby sobie uświadomić, jakie to "łatwe i proste" jest stosowanie programowania obiektowego w języku C (w szczególności te ciągłe rzutowania pomiędzy wskaźnikami na "klasy bazowe", co w C++ robi się bez żadnego rzutowania).
- Formatowanie wejścia i wyjścia jest zrobione w sposób idealnie nadający się do zrobienia wielu błędów.
- No i to wspomniane ręczne zarządzanie zasobami. Ja wiem, może w C++ nie ma odśmiecania, ale przynajmniej istnieją możliwości zautomatyzowania w pewnym zakresie zarządzania zasobami, żeby jak najmniejszą ilością zasobów trzeba było zarządzać ręcznie. W C, owszem, jesteśmy w stanie zrobić parę new/free funkcji dla każdego typu danej, żeby go tam odpowiednio konstruować i odpowiednio niszczyć (niezły kawał roboty, ale to się opłaca), tyle że procesu tworzenia i niszczenia obiektów nie da się już zautomatyzować, bo do tego konieczne są destruktory. W C++ żeby np. zaznaczyć, że na danym obszarze korzystamy z pliku, wystarczy to zrobić w jednym miejscu, podczas gdy w C jak się otworzyło plik, to trzeba pamiętać o jego zamknięciu (w tym również przed każdym return!). Podobnie wygląda kwestia zwalniania zasobów w przypadku wystąpienia sytuacji wyjątkowej, czy też zaznaczenia, że obiekt skądś-tam dostajemy na własność i zamierzamy go usunąć, jak nam nie będzie potrzebny, co w C++ można zaznaczyć w miejscu uzyskania tego obiektu (auto_ptr).
Inaczej mówiąc, w języku C trzeba bawić się w każdy najdrobniejszy szczegół, za każdym razem trzeba tak czy siak ponownie to samo ręcznie wystukać, nawet jeśli dokładnie takie same rzeczy robiło się już wiele razy. Owszem, jesteśmy w stanie zrobić sobie to jakoś "skrótowo", ale to, jak możemy krótszym zapisem opisać bardzo złożone operacje (to jest zresztą podstawowa właściwość każdego języka wysokiego poziomu!), zależy wprost od tego, jakie możliwości dany język nam daje w tej dziedzinie. W przypadku gdy język C daje nam jedynie prosty model proceduralno-strukturalny, możliwości nie mamy zbyt wiele. Każdą czynność musimy opisać funkcją, każdy obiekt wskaźnikiem, polimorfizm i funkcjonalność wskaźnikami do funkcji, wszystko ładnie okraszone nieodstępnym rzutowaniem, a lokalnego kontekstu dla obiektów i stanów (takich jak np. sekcje krytyczne) nie możemy opisać niczym. Namiastkę metaprogramowania możemy mieć, powiedzmy, za pomocą preprocesora, ale co większość poważnych programistów myśli o używaniu preprocesora, to chyba nie trzeba przypominać.
Jakoś to się tak dzieje, że jak na razie każdy program przepisany z C do C++ zrobił się mniejszy. Nie widziałem jeszcze, żeby się zdarzyło inaczej (chyba że rozszerzano funkcjonalność, ale wtedy to już nie jest równoważne). Używany zresztą przelicznik równoważności linii kodu na linie kodu asemblera jest dla języka C niższy, niż dla C++ – widać więc jasno, że zakłada się, iż każdy kod pisany w C można w C++ napisać w mniejszej liczbie linii.
Zresztą prawda jest też taka, że faktycznie nikt nie wybiera języka C do małych projektów (bo i faktycznie nie ma to za wiele sensu). Ludzie nie znający C++ zazwyczaj do tych celów wybierają perla :)
Język C++ jest językiem wyższego poziomu, niż język C, więc więcej rzeczy dzieje się tam poza kontrolą programisty. W języku C nic się nam nie wymknie i wszystko mamy pod kontrolą.
Programista, który twierdzi, że po to, aby mieć wszystko pod kontrolą, potrzebuje języka, który na wszelki wypadek nie ma nic poza tym, co można sobie w myślach przełożyć na asembler, nie nadaje się do programowania w ogóle (i niech spada testować :). Można się w to bawić, ale po pierwsze, jeśli ma się akurat takie hobby (a nie pisze się oprogramowanie, za które żąda się potem twardej gotówki), a po drugie, jeśli pisze się aplikację niewielkich rozmiarów (przy większych rozmiarach stopień komplikacji powoduje większe kłopoty z zapanowaniem nad tym wszystkim). Pomijam już fakt, że małą aplikację z kolei to wprawny programista napisze w C++ znacznie mniejszym kosztem. Można by nawet zadać pytanie, dlaczego w C, a nie w asemblerze, albo Fortranie. I obawiam się, że odpowiedź potwierdzi moje najgorsze przypuszczenia: nie w asemblerze dlatego, że nie jest przenośny i nie w Fortranie, bo mało kto potrafi w tym cokolwiek napisać. Jeśli faktycznie bardzo potrzebne jest w danym miejscu programowanie na niskim poziomie, charakterystycznym dla języka C, to przecież można to w C++ robić również, tyle tylko, że w C++ można do tego czegoś jeszcze dorobić wygodny wysokopoziomowy interfejs.
I powtarzam nie wiem już po raz który – nie ma niczego w C++, co by się działo poza kontrolą programisty. Trzeba tylko po prostu wiedzieć, jak takie czy inne mechanizmy działają, a przede wszystkim, jak działają różne elementy biblioteczne. Dodatkowo, w C++ można pisać w dokładnie tym samym stylu, co w C i w takiej sytuacji nie używamy żadnej konstrukcji która "potencjalnie byłaby poza kontrolą programisty". Jeśli w języku C++ dzieje się coś poza kontrolą programisty, to chyba tylko takiego programisty, który po prostu nie zna C++. No, to chyba oczywiste – gdyby za sterami samolotu siadł gościu, który potrafi najwyżej prowadzić samochód, to można by z całą odpowiedzialnością stwierdzić, że taki samolot robiłby wiele rzeczy poza kontrolą pilota :).
W programowaniu na wysokim poziomie, gdy mamy do czynienia z rozbudowanymi, skomplikowanymi aplikacjami komputerowymi, które mają złożoną obsługę, na pewno jest miejsce dla C++. Nie ma natomiast miejsca dla C++ w oprogramowaniu na bardzo niskim poziomie – takim jak sterowniki urządzeń, jądro systemu operacyjnego, czy jego moduły, oprogramowanie dla kas fiskalnych, czy dekoderów telewizji satelitarnej.
He he he :) Może to niektórych zdziwi, ale właśnie C++ był w szczególności dedykowany do takich rzeczy, jak pisanie sterowników, czy jądra systemów operacyjnych. Nie rozumiem zresztą, dlaczego cała kupa najróżniejszych mędrków uważa, że do pisania niskopoziomowego oprogramowania potrzebny jest niskopoziomowy język programowania (to taki sam "aksjomat idiotów", jak np. to, że GUI musi być programowane obiektowo i wymaga odśmiecacza). Język C++ nie wniósł do inżynierii oprogramowania żadnego istotnego odkrycia, ani nie zaproponował żadnej nowej technologii – sam jest bazowany głównie na języku C, a wszystkie jego właściwości zostały zapożyczone z innych języków (Simula, Ada, ML, Clu). Może więc przypomnę parę faktów z początków języka C++.
Bjarne Stroustrup, kiedy pisał swoją pracę dyplomową, był zafascynowany językiem Simula, i zamierzał w nim ową pracę ukończyć. Niestety, wyszedł tu później pewien drobny problem. Otóż implementacja Simuli, jaką Stroustrup wówczas dysponował, okazała się marnej jakości i w efekcie Stroustrup musiał swoją pracę zrobić w BCPL, gdyż w przeciwnym razie groziło mu niedotrzymanie terminu. Stanął zatem przed wyborem: albo będzie kontynuował w Simuli i miał do dyspozycji wspaniały język wysokopoziomowy (ale będzie miał problem z ukończeniem doktoratu), albo poświęci wygodę pisania w języku wysokopoziomowym na rzecz wydajności i pełnej przewidywalności niskopoziomowego języka BCPL.
Później, poza innymi językami programowania wysokiego poziomu, interesował go również język C, w którym pisało się – jak na owe czasy (okolice 1980 roku) – dość prosto, a na dodatek język ten jako jeden z niewielu nie wnosił żadnych obciążeń nie dających programiście korzyści (no i, co nie było takie częste w tych czasach, był niemal całkowicie niezależny od platformy). Ale, podobnie jak BCPL, łączył w sobie pełne panowanie nad wszystkim, co się dzieje, z niemożliwością programowania na wysokim poziomie. I dlatego zapewne Stroustrupowi wpadła do głowy taka myśl: Czy naprawdę aby móc korzystać z zaawansowanych właściwości języków wysokiego poziomu musimy godzić się na poważne obciążenia, z których praktycznie nie korzystamy? Dlaczego te zaawansowane właściwości, które występują w różnych bardzo wysokopoziomowych językach (jak np. programowanie obiektowe), nie mogłyby być także dostępne w języku takim jak C?
I Bjarne Stroustrup – najwyraźniej – postawił sobie za zadanie udowodnienie, że jest to możliwe – i w moim przekonaniu udało mu się to. Zapoczątkował język, któremu jako bodaj pierwszemu i nawet bodaj jedynemu udało się połączyć niskopoziomowość i pełną kontrolę języka C z wysokopoziomowymi właściwościami języków takich jak Ada, czy Smalltalk.
Faktem jest, owszem, że zwolennicy języków wysokiego poziomu będą czuli pewien niedosyt. Język C++ jest szczególny; doświadczenia w wysokopoziomowym programowaniu w innych językach z reguły do niczego się w C++ nie przydadzą. Właściwości wysokiego poziomu są w nim ostrożne, wręcz można powiedzieć, ubogie. Ale nie są wcale na tyle ubogie, żeby nie dało się w nim programować na wysokim poziomie. Oczywiście, jeśli ktoś potrzebuje pełnej swobody programowania wysokopoziomowego, wiele innych języków czeka na niego. Ale niech wtedy zapomni o pełnej kontroli i przewidywalności wydajności, jaką zapewni mu język C++ (nie wspominając już o tym, że trudno wśród nich znaleźć taki język, który udostępnia użytkownikowi tyle paradygmatów programowania, co C++). Jak pisał Bjarne Stroustrup, konkurentem dla C++ miał być wyłącznie język C; konfrontacja C++ z językami wysokiego poziomu nie ma za wiele sensu.
Natomiast nie rozumiem, co mają do niego zwolennicy języków niskiego poziomu. Przecież jeśli ktoś nie chce korzystać z zaawansowanych właściwości C++, to nic prostszego, niż z nich po prostu nie korzystać. Jeśli ktoś nie chce koniecznie używać std::string, to wciąż może korzystać z funkcji z string.h i statycznych tablic. Jeśli ktoś nie chce korzystać z wzorców, tylko woli do każdego typu danej zrobić osobną funkcję (a może zrobisz to makrodefinicjami, misiu?), to niech sobie to zrobi nawet i tak, jeśli koniecznie musi. Wolisz dynamiczne tablice przydzielane przez calloc – to nie korzystaj z std::vector. Wolisz sam się dłubać w struktury ze wskaźnikiem na samą siebie – nie korzystaj z std::list. Wolisz wszystkie dane zbiorcze obsługiwać pętlami for – nie korzystaj ze standardowych algorytmów (napisz jeszcze, misiu, implementację quicksorta tak, żeby chodziła "od strzału"). Wolisz piętrzące się sterty instrukcji switch/case i ręczne budowanie obiektów przez wskaźniki na mniejsze obiekty – nie korzystaj z programowania obiektowego. Wolisz pełną swobodę operowania fragmentami pamięci, zamiast ścisłego systemu typów – rzutuj. Czy język C++ narzuca ci sposób tworzenia oprogramowania?
A dodatkowo, jeśli chodzi o oprogramowanie w takich dziedzinach, jak wspomniane w wypowiedzi, to owo oprogramowanie jest zazwyczaj jednak dość skomplikowane, składa się z wielu niezależnych części, często tworzonych przez oddzielne zespoły. W takim czymś koszmarem jest np. nazewnictwo, bo w C nie ma np. przestrzeni nazw. Tak samo koszmarem jest wieczne ręczne zarządzanie zasobami wszelkiej maści, koszmarem są możliwości organizowania projektu ograniczone do prostego, proceduralno-strukturalnego modelu. Tak samo koszmarem jest konieczność definiowania własnych tzw. "prymitywnych" typów danych (np. zbiorników). Oczywiście, że takie oprogramowanie może być mimo wszystko tworzone w C, ale jakim kosztem!
Język C posiada również bardzo słaby system typów i nie posiada żadnego wsparcia dla typów złożonych (poza zawieraniem pól w strukturach), o ochronie przed przypadkowym niepoprawnym użyciem nie wspominając. Jeśli się zatem chce mieć jakiekolwiek bardziej złożone typy danych, najczęściej stosuje się rzutowanie. Rzutowanie jest absolutnym i całkowicie bezrestrykcyjnym złamaniem (bo naruszenie to jeszcze za słabe słowo) statycznego systemu typów. Jeśli już mieć statyczny system typów, to taki, żeby był z niego jakiś pożytek; żeby rzeczywiście mógł być użyty do tego, do czego jest on faktycznie przeznaczony – do zapewnienia formalnej poprawności programów. W C ze statycznego systemu typów pożytek jest praktycznie żaden, jeśli do wykonania jakiejkolwiek bardziej złożonej operacji trzeba użyć rzutowania. Rzutowanie, poza tym, że udostępnia właściwości, na jakie nie pozwala statyczny system typów, dodatkowo "zabezpiecza" przed wykrywaniem wielu potencjalnych błędów, co jest tym bardziej istotne w złożonym oprogramowaniu, a jeszcze bardziej istotne w programowaniu samodzielnych urządzeń, czy elementów krytycznych, takich jak jądra systemów operacyjnych.
Język C zatem nie nadaje się ani do małych projektów (brak w nim podstawowych składników, używanych w większości małych programów), ani do oprogramowania niskopoziomowego, czy o dużych wymogach co do niezawodności (bo jest w nim bardzo łatwo przeoczyć błąd, w porównaniu z C++), a już na pewno nie nadaje się do oprogramowania złożonego, skomplikowanego, tworzonego przez kilka niezależnych zespołów (np. dlatego, że nie ma żadnych właściwości pozwalających na tworzenie komponentów, ani nawet przestrzeni nazw). Kwestie tworzenia oprogramowania ze skomplikowanym interfejsem to już nawet odrzucają ci "skłonni do kompromisów". Jeśli poza tymi rodzajami oprogramowania są jeszcze jakieś rodzaje współcześnie tworzonego oprogramowania, to być może C się jeszcze do czegoś nadaje.
No dobrze, zgodzę się w ostateczności co do jednej rzeczy – kompilatory języka C wymagają znacznie mniejszej biblioteki runtime, niż w przypadku C++. Nie dyskwalifikuje to jednak bynajmniej C++ z zastosowań w takich rzeczach, jak jądra systemów operacyjnych i "embedded systems". Inteligentne systemy kompilacji potrafią wyciągnąć z biblioteki standardowej tylko te rzeczy, które rzeczywiście są używane, a dodatkowo w bibliotece standardowej C++ większość znajduje się w plikach nagłówkowych. Na dodatek zawierają rzeczy, które w przypadku języka C trzeba by napisać samemu i otrzymać wcale nie mniej kodu do łączenia.
Tak na marginesie wspomnę jeszcze o drobnym zdarzeniu, które może nie "wstrząsnęło opinią publiczną", ale narobiło trochę zamieszania. Na pewnej stronie www (aktualnie polskie tłumaczenie jest dostepne na stronie Anubis [kopia lokalna] – nawiasem mówiąc, ten tekst ciągle zmienia lokalizację, ale da się go znaleźć googlami bez większych problemów) ukazał się wywiad, którego (podobno) udzielił Bjarne Stroustrup pewnemu pismu komputerowemu. Już na samym wstępie dowiadujemy się tam, że IBM swego czasu zrobił głupotę i wykształcił sporą rzeszę programistów, przez co stracili oni na wartości i wielu z nich musiało sobie poszukać innego zajęcia (a jednym z nich jest redaktor prowadzący ten wywiad). Bjarne Stroustrup zaś przekazuje szokujące informacje o tym, że wymyślił C++ jako język w założeniu trudny, zamieszany, skomplikowany i nie dający się opanować (właśnie po to, żeby programiści mieli pracę). Na dodatek przytacza kilka przykładów firm, które przenosiły projekty do C++ i omal nie zbankrutowały; rzuca też tekst w stylu "Widział pan żeby kiedykolwiek jakaś firma ponownie używała swego kodu?". C++ służy zatem tylko zwiedzeniu programistów tak, aby ci "prawdziwi" (czyli piszący tylko w C) pozostali, a ich wartość została doceniona. Na końcu jest wzmianka też o referencjach: pewien jego znajomy miał problemy z pamiętaniem, co przekazywał do funkcji, wartość czy zmienną, i że ten operator zawsze mu o tym przypominał (czyli: referencje, choć wydają się fajne, to w gruncie rzeczy też są do dupy, jak i cały C++). Niefortunnie niestety wspomniał o jednym jedynym przypadku użycia referencji, którego w programowaniu w C++ akurat się unika (ale żeby wiedzieć o tym, to też trzeba by trochę w C++ popisać).
Podobnych "wpadek" w całym tym tekście jest od groma (i na szczęście, jest już teraz – jak w przytoczonym linku – zamieszczana na stronach typu "śmieszne teksty") – jak np. że używanie wyjątków do zgłaszania braku pamięci może powodować mnóstwo problemów i lepiej polegać na wartości zwracanej przez malloc. Autorem tego tekstu musiał być jakiś sfrustrowany fan języka C, bo owe wpadki dobitnie świadczą o tym, że autor sam nie wie, o czym pisze. Z tego co pamiętam na pl.comp.lang.c wielu ludzi było skłonnych uwierzyć w prawdziwość tego wywiadu, niestety tak naprawdę poza drobną prowokacją (oraz samokompromitacją) autor niczego sensownego nie wniósł. Na końcu wywiadu Bjarne Stroustrup rzekomo stwierdza, że nie jest wcale tak źle, C++ aktualnie wymiera, a programiści, którzy twardo trzymali się C i nie dali się nabrać na C++, przetrwają.
Niestety, wbrew temu twierdzeniu, jak na razie C++ ma się całkiem dobrze, a właśnie język C sprowadzono do roli języka o wąskiej specjalizacji (jako przenośny asembler o łatwym do napisania kompilatorze; częściej się go wykorzystuje do unikalnych platform, niż do programowania komputerów). Właściwości C++ dzięki swej elastyczności pozwalają na stosowanie coraz większej ilości stylów programowania, zatem wybór języka programowania powoli przestaje mieć znaczenie (niewiele jest takich języków, w których coś można zrobić "lepiej" niż w C++ – pomijając oczywiście języki skryptowe). Jedynie języki wąsko wyspecjalizowane w niektórych konkretnych zastosowaniach sprawdzają się lepiej (aczkolwiek takich "wąskich" specjalizacji też jest tyle co kot napłakał). Z kolei biblioteka STL oraz wprowadzone przez nią funkcjonały dały początek programowaniu funkcjonalnemu w C++, co zostało znacznie rozwinięte przez bibliotekę BOOST, w szczególności słynne "bind", "function", a na dodatek jeszcze lambda (o dość ograniczonym zastosowaniu, ale dość przydatne) oraz spirit (generator parserów definiowany wprost w C++). Powoduje to, że C++ nadaje się już do takich zastosowań, które były wcześniej zajęte przez języki w rodzaju klonów ML-a. W C++ powstało również wiele innych niezależnych bibliotek, wśród nich jedną z bardziej wartych uwagi jest Qt produkowana przez TrollTech – wieloplatformowa biblioteka aplikacji okienkowych (przypominam tylko, że przez długi czas palmę pierwszeństwa w tych zastosowaniach trzymał Smalltalk, a "najszybciej" okienka robiło się w Tk). Mimo entuzjastycznego nastawienia różnych wielbicieli innych języków programowania (w tym również C), jak na razie C++ jest bezwzględnie na pierwszym miejscu jeśli chodzi o ilość produkowanego komercyjnego oprogramowania (w OpenSource i GNU podobno nieznacznie więcej powstaje w C, ale w obu tych językach powstaje ok. 95% tego oprogramowania). Jak na razie również nie istnieje i nie szykuje się w najbliższym czasie żadna alternatywa dla C++, ani żaden język, który mógłby go zastąpić (Java nie miała żadnych szans konkurować z C++ ze względu na swoją powolność; również osławiony C# miał być w założeniu językiem do pokonania Javy, a bynajmniej nie do konkurencji z C++; zresztą Micro$oft najwyraźniej ma zamiar inwestować w C++, skoro zatrudnił Stana Lippmana).
A co ze wspomnianymi programistami? No cóż; to fakt, że C++ nie jest językiem łatwym. Ale jak na razie uczenie się tego języka (pod warunkiem oczywiście, że kogoś programowanie naprawdę interesuje) jest najlepszą inwestycją w naukę i to przynoszącą całkiem pokaźne zyski. I paradoksalnie to właśnie dobrzy programiści C++ są dziś bardzo poszukiwani (a doświadczeni programiści C to ludzie, którym najtrudniej dostosować się do C++). Może właśnie to zainspirowało autora wspomnianego wywiadu? A język, choć popularny, to jednak żeby był nawet najwyśmienitszy, to nie zastąpi nikomu smykałki do programowania, pojęcia o regułach programowania, algorytmice, a także umiejętności utrzymywania czytelności i przejrzystości kodu oraz jego efektywności. I tacy programiści raczej nie potrzebują żadnych specjalnych zabiegów, żeby łatwiej dostać pracę.