actu-image
Software engineering PL - 26 mar 2026

Co nowego w C++

Thibault Ricord-Marchal's photo

Autor artykułu

Thibault Ricord-Marchal

Konsultant ds. inżynierii oprogramowania

Jestem programistą C++ i systemów wbudowanych w MARGO, obecnie pracuję dla BNP CIB. Z natury jestem ciekawy świata, dlatego dzielę się tu swoimi obserwacjami i wiedzą na temat ewolucji języków programowania.

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 constexpr na 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:

int main() { constexpr std::meta::info int_type_info = ^^int; // Here, int_type_info contains all information of the int type. // For example, the name of the type as a std::string_view static_assert(std::meta::identifier_of(int_type_info) == „int”); // We can then go back to the „real” type with the splice operator [: :] [:int_type_info:] my_int = 42; // „[:int_type_info:]” is replaced by „int” return my_int; }

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_members i 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).
Architektura zero-overhead

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 jako char[].
  • = delete może teraz zawierać komunikat o błędzie (np.: =delete("Message")).
  • std::hive, zupełnie nowy kontener zgodny z semantyką bucket array.
Ekspertyza C++

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