W stronę rewolucji w C++?
Od 2017 roku C++ stara się coraz bardziej zbliżyć do semantyki nowoczesnych języków. Pojawienie się Rusta w ekosystemie około 2015 roku i popularność, jaką zdobył, uświadomiły potrzebę bezpieczniejszego języka, który zachowa przy tym wydajność. Co więcej, na początku 2024 roku Biały Dom opublikował oświadczenie wzywające programistów do rezygnacji z języków C i C++, ponieważ nie są wystarczająco bezpieczne.
W tym kontekście pojawia się ta aktualizacja C++:
- Dogonienie innych języków.
- Zwiększenie bezpieczeństwa języka w odpowiedzi na ostrzeżenia Białego Domu i położenie pierwszych cegiełek pod prawdziwe „Memory Safety”.
Zawartość aktualizacji
Oto najważniejsze punkty tej aktualizacji języka:
- Dodanie refleksji statycznej
- Dodanie kontraktów
- Dodanie
std::execution - Wzmocnienie STL i wprowadzenie pojęcia „Erroneous behaviour”, które zastępuje niektóre przypadki „Undefined behaviour”
- Rozszerzenie
constexprna cały język
Dodanie refleksji statycznej
Refleksja w programowaniu to zdolność języka do obserwowania samego siebie (jak w lustrze). Oferuje ją wiele języków, na przykład Rust czy C#. Refleksja może działać w czasie wykonania i wtedy mówimy o refleksji dynamicznej. Taki wybór zrobił na przykład Python i częściowo C#. Daje to więcej możliwości, ale kosztem wydajności. Drugie rozwiązanie polega na rozstrzyganiu refleksji w czasie kompilacji. To refleksja statyczna, na którą postawił Rust, a także C++. Komplikuje to implementację, ale całkowicie eliminuje koszt wydajnościowy.
Oto bardzo krótkie podsumowanie działania tego mechanizmu w C++. Aby uzyskać refleksję encji w kodzie, stosuje się operator refleksji ^^ i otrzymuje obiekt constexpr typu std::meta::info. Obiekt typu std::meta::info może reprezentować dowolny element, w tym: przestrzeń nazw, klasę, typ, funkcję, zmienną itd. Następnie nagłówek <meta> udostępnia funkcje consteval pozwalające pobrać informacje z obiektu std::meta::info. Po zakończeniu przetwarzania operator splice [: :] pozwala uzyskać rzeczywistą encję. Na przykład:
Oczywiście ten przykład jest bardzo prosty i nie pokazuje wszystkich możliwości. Oto lista niektórych zastosowań refleksji:
- Automatyczna serializacja i deserializacja obiektów
- Dogłębna inspekcja typów na potrzeby coraz bardziej zaawansowanych konceptów (na przykład koncept
has_only_public_membersi podobne) - Bezpieczne mechanizmy rzutowania
- Nowy sposób pisania kodu generycznego
Wykorzystałem refleksję do stworzenia małej biblioteki, która pokazuje część jej możliwości. Oto link do GitHuba: https://github.com/NoliaXeh/metaref
Dodanie kontraktów
W ramach zwiększania bezpieczeństwa C++ komitet przyjął dodanie kontraktów do języka. To postulat zgłaszany od lat: kontrakty mogły pojawić się już w C++20, ale zostały przyjęte dopiero niedawno.
Chodzi o umożliwienie programowania kontraktowego (design by contract) w C++. Programowanie kontraktowe to paradygmat polegający na zapewnieniu wokół funkcji jasnych i weryfikowalnych gwarancji, tak by wyeliminować klasyczne błędy programistyczne. Zwykle obejmuje trzy rodzaje sprawdzeń i właśnie one trafiły do C++:
- Warunek wstępny: sprawdza warunek w chwili wywołania funkcji. Jeśli warunek jest fałszywy, program się zatrzymuje i wyświetla błąd.
- Warunek końcowy: sprawdza warunek w chwili, gdy funkcja zwraca wynik. Jeśli warunek jest fałszywy, program się zatrzymuje i wyświetla błąd.
- Asercja: podobnie jak makro assert sprawdza warunek wewnątrz funkcji. Jeśli warunek jest fałszywy, program się zatrzymuje i wyświetla błąd.
Oto prosty przykład:
double square_root(double x) pre(x >= 0) // If x is negative, we cannot calculate its square root post(r: r * r == x) // We ensure that the return value squared equals X (here, r is the return value) { /// Code here } int main() { double value = square_root(9); contract_assert(value == 3); // Contract checked in a function body std::println("sqrt(9) = {}", value); }
To ogromny krok naprzód dla bezpieczeństwa języka. Niektórzy mogli się już domyślić: sprawdzanie tych warunków w czasie wykonania ma swój koszt wydajnościowy. To prawda i dlatego istnieje kilka polityk sprawdzania kontraktów:
- Ignore: kontrakty są całkowicie ignorowane, nie są kompilowane, więc nic nie kosztują
- Observe: kontrakty są wykonywane, ale w razie błędu program nie przerywa działania, tylko zapisuje log
- Enforce: kontrakty są wykonywane, a w razie błędu zgłaszany jest wyjątek
- Quick-enforce: kontrakty są wykonywane, a w razie błędu program zostaje zatrzymany (bez wyjątku)
Kontrakty w obecnej postaci spotykają się z ostrą krytyką, zwłaszcza dlatego, że można zdecydować o ich ignorowaniu, oraz dlatego, że w systemach krytycznych niosą ryzyko awarii programu.
Rewolucja asynchroniczna: std::execution (Senders & Receivers)
To prawdopodobnie jeden z najważniejszych elementów C++26. Programowaniu asynchronicznemu i wielowątkowemu w C++ zawsze brakowało jednolitego, standardowego modelu. Mieliśmy std::thread (często zbyt niskopoziomowe) albo parę std::async / std::future (która powoduje poważne problemy z wydajnością, czasem nieoczekiwanie blokując wątki).
Wraz z przyjęciem propozycji P2300 C++26 wprowadza std::execution, standardowy framework oparty na paradygmacie Senders i Receivers.
Główna idea polega na ścisłym oddzieleniu opisu pracy do wykonania od miejsca, w którym jest ona wykonywana. Model opiera się na trzech kluczowych pojęciach:
std::execution::sender: opisują operację asynchroniczną. Z natury są „leniwe”: zdefiniowanie Sendera nie uruchamia żadnego wykonania, dopóki nie zostanie on jawnie podłączony.std::execution::receiver: są odbiorcami wyniku Sendera. Udostępniają trzy kanały (sukces z wartością, porażka z błędem lub po prostu anulowanie).std::execution::scheduler: definiują kontekst wykonania (pulę wątków, pętlę zdarzeń, zadanie systemowe, a nawet GPU).
Główną zaletą tego modelu jest kompozycyjność. Możesz łączyć złożone operacje w łańcuchy za pomocą potoków (operator |) i nigdy nie wpaść w piekło callbacków obsługi błędów. Co więcej, architektura została zaprojektowana z myślą o zerowym narzucie: kompilator potrafi spłaszczyć cały łańcuch wykonania podczas kompilacji i wygenerować wyjątkowo zoptymalizowany kod asemblera bez zbędnych alokacji pamięci.
Wzmocnienie STL i zachowanie błędne (erroneous behaviour)
Aby zwiększyć bezpieczeństwo języka, trzeba było też przyjrzeć się STL, w której przy wielu decyzjach projektowych przedłożono wydajność nad bezpieczeństwo. Na przykład kontenery nie sprawdzają zakresu przy dostępie przez operator[]. Przed C++26 taki dostęp był klasyfikowany jako „Undefined behaviour”.
Od C++26 te niezdefiniowane zachowania, które prowadzą do błędów bezpieczeństwa, są przeklasyfikowane jako „Erroneous behaviour”. Pozwala to kompilatorom wybrać inną politykę obsługi w zależności od konfiguracji, na przykład stosować kontrakty wobec tych zachowań przy kompilacji w trybie debug.
Na marginesie: czy C++26 stał się „Memory Safe” jak Rust?
Trzeba odróżnić ogólny wzrost bezpieczeństwa, jaki przynosi C++26, od ścisłego Memory Safety, którego wymagają NSA czy Biały Dom.
Kontrakty i erroneous behaviour w C++26 rozwiązują ogromne problemy (takie jak odczyt niezainicjowanych zmiennych czy przepełnienie tablic, które dotyczą bezpieczeństwa przestrzennego). Nie rozwiązują jednak w magiczny sposób problemów bezpieczeństwa czasowego związanych z zarządzaniem czasem życia obiektów w pamięci, takich jak słynny Use-After-Free (użycie wskaźnika po zwolnieniu pamięci) czy wiszące wskaźniki (Dangling Pointers).
C++26 robi milowy krok w stronę bardziej niezawodnego kodu, ale na poziom gwarancji porównywalny z borrow checkerem Rusta w czasie kompilacji trzeba będzie poczekać do kolejnych wersji języka (zwłaszcza do bardzo wyczekiwanej koncepcji C++ Profiles, popieranej przez Bjarne’a Stroustrupa).
Rozszerzenie constexpr na cały język
Od teraz cały język może być ewaluowany w czasie kompilacji. Już w C++23 duża część języka stała się zgodna z constexpr, ale C++26 dodaje m.in. placement new oraz konwersję wskaźników void* na inne typy wskaźników.
Kilka drobiazgów, które nie zasłużyły na osobny akapit
- Dodanie symbolu wieloznacznego
_do oznaczania ignorowanej zmiennej. - Dodanie symboli
$,@i „` do podstawowego zestawu znaków języka. - Dodanie dyrektywy
#embed "filename"pozwalającej dołączyć plik do kodu jakochar[]. = deletemoże teraz zawierać komunikat o błędzie (np.:=delete("Message")).std::hive, zupełnie nowy kontener zgodny z semantyką bucket array.
Podsumowanie: prawdziwa rewolucja w C++?
Czy C++26 naprawdę oznacza rewolucję zapowiedzianą we wstępie? Biorąc pod uwagę nowe funkcje, trudno twierdzić inaczej.
Przez dekady język stawiał na czystą wydajność i elastyczność, zostawiając programiście ciężką odpowiedzialność za unikanie słynnych niezdefiniowanych zachowań. Dziś, wraz z wprowadzeniem kontraktów i pojęcia erroneous behaviour, komitet standaryzacyjny świadomie zwraca się w stronę bezpieczeństwa. To bezpośrednia i konkretna odpowiedź na wymagania współczesnego przemysłu i ostrzeżenia instytucji takich jak Biały Dom.
Równolegle historyczne wprowadzenie refleksji statycznej w połączeniu z ogromnym rozszerzeniem constexpr radykalnie zmieni sposób projektowania bibliotek. Serializacja staje się trywialna, kod zyskuje na ekspresywności, a metaprogramowanie wreszcie staje się czytelne. Złożone makra i kryptyczne templates odchodzą do przeszłości.
Co więcej, standaryzując wreszcie solidny model asynchroniczności za pomocą std::execution (paradygmat Senders i Receivers jest jego fundamentem), język wypełnia jedną ze swoich największych historycznych luk. Standardowy C++ oferuje teraz jednolity i kompozycyjny framework do orkiestracji złożonej współbieżności, spychając na drugi plan stare narzędzia, często ograniczone lub kosztowne pod względem wydajności.
C++26 nie tylko dogania nowsze języki, takie jak Rust czy C#. Dowodzi, że potrafi się modernizować, pozostając wiernym swojemu DNA: abstrakcjom o zerowym koszcie. To coś więcej niż kolejna wersja standardu: to prawdziwe odnowienie, które daje C++ wszystko, czego potrzebuje, by przez kolejne dekady pozostać niekwestionowanym liderem.
Chcesz zgłębić tematy związane z C++26 lub przeprowadzić audyt swojej bazy kodu?
Skontaktuj się z ekspertem MARGO