TABLE OF CONTENTS:
Gdy powstawał ten artykuł, nie wiedzieliśmy jeszcze, że w styczniu 2023 TIOBE wybierze C++ językiem roku 2022! Mieliśmy nosa!
W pierwszej części tej serii artykułów o najpopularniejszych językach programowania skupiliśmy się na języku C#, który w indeksie TIOBE z lipca 2022 zajmował piąte miejsce. Minęły 4 miesiące, a pięć pierwszych pozycji indeksu pozostało bez zmian – każdy z języków utrzymał swoje zaszczytne miejsce na podium, co nie jest rzeczą bardzo zaskakującą1.
Dzisiaj skupimy się na dwóch kolejnych pozycjach od dołu wyróżnionej przez TIOBE piątki, z wyłączeniem języka Java2. Języki C i C++ są w czołówce indeksu od 2001 roku, a omawiamy je w jednym artykule, by pokazać istotne różnice pomiędzy nimi.
Niestety, często kandydaci, z którymi spotykamy się podczas rozmów kwalifikacyjnych, łączą w swoich CV te dwa języki w jedną pozycję – C/C++. Dla mnie, jako osoby, która sprawdza techniczną wiedzę podczas spotkań, jest to sygnał, że kandydat może do końca nie znać różnic między tymi językami – zwykle podejrzewam, że chodzi jedynie o język C++ i zawsze to weryfikuję. Programista C nie zgrupowałby tego języka z jego następcą3. Istotnie różni je nie tylko sposób pracy, ale i projekty, w których je wykorzystujemy.
Język C jest najbardziej wyróżniającym się językiem ze wspomnianej piątki. Jako jedyny jest językiem niskopoziomowym, co znacząco wpływa na obszary, w których ma sensowne zastosowanie. Jest językiem zorientowanym na programowanie proceduralne, co nie oznacza oczywiście, że nie można w nim programować funkcyjnie, czy z użyciem praktyk OOP (ang. Object-Oriented Programming). Można, ale sam język programowania C nie zawiera konstrukcji gramatycznych, które by w tym pomagały.
W języku C pisze się wszędzie tam, gdzie liczy się wydajność. Nie posiada on mechanizmów takich jak polimorfizm, zarządzanie pamięcią, zabezpieczenie dostępu do pamięci, czy kontrola typów 4. Brak tych mechanizmów znacząco przyspiesza działanie programu, ale nakłada przy tym sporą odpowiedzialność na programistę – to on musi kontrolować absolutnie wszystko.
Drugim istotnym czynnikiem przemawiającym za użyciem tego języka jest bezpośredni dostęp do zasobów. Brak mechanizmów, które ograniczają swobodę programisty sprawia, że w języku C można łatwo i efektywnie manipulować flagami w rejestrach procesora, dokonywać bezpośrednich wpisów do różnych obszarów pamięci lub manipulować komunikacją z podzespołami urządzenia, na którym pracujemy.
Ta swoboda i wydajność to powody, dla których w języku programowania C powstają sterowniki, jądra systemów operacyjnych, jądra przeglądarek internetowych, fundamenty baz danych, interfejsów graficznych czy silników gier, a także znaczna większość oprogramowania wbudowanego. Gdy myślimy o najniższej warstwie oprogramowania, często jest ona napisana właśnie w języku C.
Gramatyka języka C jest bardzo prosta. Całkiem łatwo napisać w C kod, który realizuje skomplikowaną mechanikę, a skompiluje się bez błędów – nawet jeśli takie się pojawią z reguły są do naprawienia w kilka chwil. Niestety pozorna prostota oznacza też, że niewiele rzeczy jest z góry zagwarantowanych lub sprawdzanych na etapie kompilacji – bardzo łatwo popełnić w tym języku trudne do wykrycia błędy, które są zwykle nie do pomyślenia we współczesnych językach programowania. Przechodząc z pracy w języku wysokopoziomowym do C, trzeba odrzucić wiele nawyków, a wytworzyć sporo zupełnie nowych.
Chociaż składni języka C można nauczyć się bardzo szybko, jest to jeden z najtrudniejszych języków do operowania w praktyce. Swoboda implementacji rozwiązań oznacza, że ciężko o standaryzację jednego, konkretnego podejścia do rozwiązywania problemów. W projektach pisanych w C programiści wielokrotnie wynajdują koło na nowo5, sprzecznie z praktyką ponownego użycia kodu (ang. code reuse). Takie koło zawsze wygląda nieco inaczej niż poprzednie i wymaga osobnych testów, a bywa że ten proces zachodzi nawet w obrębie jednego produktu, jeśli pracuje nad nim kilka zespołów6.
Dodatkowo, mając narzędzie-wytrych w postaci makr, które pozwalają generować w locie nawet całe bloki kodu, bardzo łatwo stworzyć skomplikowany i trudny w zrozumieniu kod. Nic dziwnego zatem, że jeden zespół może nie chcieć użyć kodu drugiego, współpracującego z nim zespołu.
Warto zauważyć, że duża część kodu napisanego w C, ze względu na optymalizację i dostosowanie go do wyznaczonej platformy, jest przeznaczona tylko pod jedno konkretne zastosowanie. Jednak dobry podział na warstwy i komponenty mógłby sprawić, że część tego kodu dałoby się wykorzystać ponownie. Sam wielokrotnie stosowałem to podejście w projektach, w których brałem udział. Niestety, z mojego doświadczenia, takie podejście jest rzadkością.
Gdy pracuje się na najniższej warstwie oprogramowania, każdy błąd może okazać się krytycznym, a gdy w grę wchodzą kwestie bezpieczeństwa, jedna dziura może spowodować globalne konsekwencje7.
Z kolei w oprogramowaniu wbudowanym, którego aktualizacje rozsyłane są po setkach tysięcy lub milionach egzemplarzy sprzętu, jeden krytyczny błąd może nawet sprawić, że urządzenia zupełnie przestaną działać8. Ktoś, kto choć raz zaimplementował system OTAU (ang. Over-The-Air Update) wie, z jak wielu czynników się składa, jak delikatny jest to proces i jak łatwo popełnić błąd. Stworzenie systemu OTAU wymaga implementacji szeregu zabezpieczeń i złożonej obsługi scenariuszy krytycznych, co powoduje, że bardzo łatwo się pomylić lub zapomnieć o jakimś scenariuszu. Konsekwencją takiej pomyłki może być nawet permanentne uszkodzenie sprzętu kończące się serwisem albo wymianą.
Swoistym paradoksem jest fakt, że do tak krytycznych zadań wykorzystuje się tak niebezpieczny język jakim jest C. Nie dziwi mnie za to rosnąca wśród programistów popularność języka Rust9, którego konstrukcja zdaje się odwrotnością gramatyki języka C – znacząco ogranicza on swobodę programisty, zabezpieczając kod przed wieloma bardzo istotnymi problemami, dając w zamian prawie identyczną wydajność i możliwość niskopoziomowej manipulacji podzespołami i pamięcią, co język programowania C.
Zdecydowanie język C to najtrudniejszy język z jakim przyszło mi pracować, ale nie ze względu na złożoność samego języka, a problemów, które trzeba przy jego pomocy rozwiązywać oraz konieczność ręcznego zabezpieczania dużej ilości kodu przed wieloma błędami. C jest bardzo podstawowym narzędziem, co oznacza, że szczególnie trudno rozwiązywać z jego pomocą złożone problemy. Największa trudność w jego opanowaniu polega na nauczeniu się tworzenia bezpiecznych rozwiązań i unikaniu błędów, co wymaga ogromnej dyscypliny w projekcie i bardzo uważnej analizy kodu, zarówno swojego, jak i innych członków zespołu.
Język C++ charakteryzuje zupełnie odmienny zestaw cech niż jego poprzednika. C++ jest obiektowym językiem wysokiego poziomu, choć pozwala na niskopoziomowy dostęp do zasobów tak jak język C. Historyczny bagaż języka C++, w postaci konstruktów z języka C, jest jednym z jego silnych punktów, jak również źródłem największych problemów10. To nie oznacza jednak, że pisze się, czy powinno się pisać, w C++ tak samo jak w C. Wręcz przeciwnie.
Biblioteki takie jak standardowa C++ oraz STL (ang. Standard Template Library) dostarczają masę gotowych rozwiązań, które w C trzeba było tworzyć od zera. Nie bez powodu na rozmowach kwalifikacyjnych często padają pytania związane z STL – jej dobra znajomość pozwala szybko i efektywnie rozwiązać wiele podstawowych problemów. Adresuje ona, między innymi, problemy związane z zarządzaniem pamięcią.
Od czasu wejścia na rynek standardu C++11 coraz bardziej odchodzi się już od stosowania „czystych wskaźników” (ang. raw pointer) znanych z C, na rzecz szeregu dostępnych mechanizmów zarządzania pamięcią, takich jak inteligentne wskaźniki (ang. smart pointer). W kwestii refleksji C++ wciąż kuleje za swoimi nowszymi rywalami, ale najbardziej użyteczne mechanizmy, takie jak RTTI (ang. Run Time Type Information)11 i cechy typów, są w nim dostępne. Ma również obsługę wyjątków12.
Język C++, podobnie jak C, używany jest wszędzie tam, gdzie liczy się wydajność, ponieważ większość z mechanizmów spowalniających jego pracę jest opcjonalna13. Można zatem korzystać z wielu udogodnień, które oferuje – bez, lub z niewielkim dodatkowym narzutem. To sprawia, że jego zastosowania w dużej mierze pokrywają się z zastosowaniami języka C. W wielu nowszych projektach (również w systemach wbudowanych) dotykających najniższej warstwy sprzętu korzysta się zarówno z C jak i C++. Jest to popularny język programowania, zwłaszcza w urządzeniach z wbudowanym Linuxem.
Możliwości tego języka pozwalają skutecznie wykorzystać go w prawie każdej dziedzinie. Dzięki użyciu platform takich jak Qt z powodzeniem można w nim rozwijać aplikacje desktopowe. Współczesny C++ posiada szereg mechanizmów programowania asynchronicznego, wliczając mechanizmy tworzenia zadań wykorzystywane w EAP (ang. Event-based Asynchronous Pattern) czy mechanizmy realizujące koncepty „future” i „promise”, wykorzystane w TAP (ang. Task-based Asynchronous Pattern). Jest też powszechnie stosowany jako backend wielu serwisów internetowych. C++ to naprawdę uniwersalny język, który sprawdzi się w każdej roli, jednak nie do każdej oczywiście jest najlepszym możliwym wyborem.
Istotną bolączką pracy w projektach napisanych w języku C++ jest zarządzanie procesem kompilacji oraz dodatkowymi pakietami. Są to problemy praktycznie nieistniejące w Pythonie i marginalnie znaczące w C#. Do zarządzania kompilacją można użyć wielu zewnętrznych narzędzi – zależnie od tego, w którym roku powstał projekt, mogą to być make, autotools lub współcześnie, najczęściej CMake. Z kolei projekty, które powstały w Visual Studio, kompilowane są zwykle przez narzędzia dostarczone wraz z IDE. Zarządzanie zewnętrznymi bibliotekami wymaga użycia managera pakietów, takiego jak Conan14, który dobrze współgra z narzędziem CMake. Nie są to jednak narzędzia działające niezawodnie i dające ten sam efekt w każdym projekcie czy na każdej platformie. Praca z nimi wymaga trochę obycia i doświadczenia, zwłaszcza jeśli chcemy stworzyć własny pakiet do użycia w innych projektach, czy to własnych czy open-source. Pracując z kodem w C++, nie można zapomnieć o procesie kompilacji, a nawet trzeba poświęcić mu całkiem sporo uwagi, zwłaszcza w kluczowych momentach rozwoju projektu, takich jak rozszerzenie projektu o nowe moduły, dodawanie zależności, wdrożenie lub zmiana procesu CI, itp.
Mimo wielu udogodnień, które pomagają programistom w pracy, C++ to nadal trudny język. Jego konstrukcje gramatyczne zdają się często nienaturalne i wymagają znajomości skomplikowanych mechanizmów, które działają w tle, by w pełni wykorzystać jego moc15. Niezrozumiałe błędy kompilacji, zajmujące dziesiątki stron tekstu, to w pracy z szablonami w C++ normalność. Przykład poniżej należy do jednego z najprostszych. W złożonych projektach szukanie właściwego komunikatu błędu zajmuje sporo czasu i nie zawsze IDE jest w stanie z tym pomóc.
Jednak błędy kompilacji to najprostsze z możliwych, bo wychodzą na powierzchnię od razu. Dużo trudniejsze błędy to te, które na przykład wykluczają RVO (ang. Return-Value Optimization), ignorują lub nieprawidłowo wykorzystują semantyki przenoszenia danych (ang. move semantics), czy jeden z wielu zaawansowanych mechanizmów optymalizacji dostępnych w C++. Potrzeba dużo doświadczenia, by rozpoznać, że dany mechanizm nie działa poprawnie i zorientować się dlaczego nie działa. Co więcej, mnogość ukrytych mechanizmów, które w różnych sytuacjach zachodzą „w tle” bez informowania o tym programisty, znacząco utrudnia efektywne wykorzystanie tego języka początkującym. To język programowania, w którym mam ponad dziesięć lat doświadczenia, a i tak niejednokrotnie frustrują mnie związane z nim problemy. Bezapelacyjnie, C++ ma największy próg wejścia ze wszystkich współcześnie stosowanych na rynku języków programowania i wymaga wieloletniego doświadczenia, by umiejętnie się nim posługiwać.
Sprawy nie ułatwia fakt, że każda nowa edycja16 wprowadza szereg mechanizmów, które wzbogacają język, ale często zmieniają podejście do niektórych konceptów i wymagają przyzwyczajenia. Z tego powodu wiele projektów decyduje się zatrzymać na określonej wersji standardu C++ i nie aktualizować go do nowszej17 – to jest również powód, dla którego istotne jest wyróżnienie z którą wersją języka dany programista miał jedynie styczność, a z którą spędził więcej czasu.
Języki C i C++, mimo ogromu swoich wad, nie muszą się bać o to, że w nadchodzących czasach stracą swoją popularność – zbyt dużo kodu w nich powstało i wciąż wymaga utrzymania, ale też cały czas powstają produkty, do wytworzenia których nadają się najlepiej. Niemniej C i C++ to rzadko wybory początkujących programistów, głównie dlatego, że istnieją interesujące oferty pracy w konkurencyjnych językach, których dużo łatwiej się nauczyć i efektywnie pracować w krótszym czasie. A jednak, popularność tych języków jest niezmienna, a zapotrzebowanie na specjalistów C i C++ nieustające. Dla tych, którzy nie boją się wyzwań związanych z nauką tych języków, pracy nie zabraknie.
Omówiliśmy już języki programowania C#, C++ oraz C. Serię zakończymy językiem Python, który niewiele ma wspólnego z powyższą trójką, a którego popularność w bardzo krótkim czasie pchnęła go na pierwsze miejsce indeksu. W 2018 roku zajmował jedynie 3% rynku, aktualnie jest to około 17% – co czyni go tak popularnym wyborem wśród programistów? Dowiecie się z trzeciej części serii.