A C++23 nyelvi változásainak jelentős része nem új absztrakció, hanem
a meglévő eszközök „élesítése”: olyan helyzetek, ahol a C++20-ban a
programozó kénytelen volt kerülőutat választani. A preprocessor
feltételes fordítása, a literálok típusa, a karakterescape-ek írásmódja
és a kis pontosságú lebegőpontos típusok mind ilyenek. A hiányzó elem
nem új paradigmát, hanem pontosan azonosítható kellemetlenséget okozott:
felesleges defined()-zárójeleket,
static_cast<size_t>-öt a literálokon, „mohó”
\x escape-eket, illetve azt, hogy a
float16_t-hez implementációs neveket
(_Float16, __fp16) kellett használni. Ez az
alfejezet ezeket a forrásszintű újdonságokat veszi sorra, mindegyiknél a
C++20-as megoldásból kiindulva.
A rész alapprofilja innentől ISO C++23, explicit
-std=c++23 kapcsolóval, GCC vagy Clang mellett. A fordítási
mód neve és a támogatás mértéke fordítóverzió-függő: a laborban rögzítsd
a g++ --version / clang++ --version kimenetét,
és először egy üres „probe” fájllal ellenőrizd, hogy a
-std=c++23 egyáltalán elfogadott (egyes implementációk a
-std=c++2b aliast ismerik). Az ebben az alfejezetben
szereplő képességek támogatottságát nem állítom ellenőrzöttnek;
mindenhol megadom a feature-test makrót és/vagy a nyelvi elemet
ténylegesen kipróbáló compile probe-ot, valamint a C++20-as
fallbackot.
A <version> header a szabványos feature-test
makrók központi helye; a könyvtári képességnél __cpp_lib_*,
a nyelvi elemnél __cpp_*, a <stdfloat>
típusainál pedig a fordító által definiált __STDCPP_*_T__
makrók a mérvadók. A preprocesszor-direktívákhoz tartozó makrónevek és
-értékek verziófüggők; ezeket a <version> tartalmából
és a fordító dokumentációjából kell kiolvasni, ezért ahol lehet, nyelvi
compile probe-ot használunk helyettük.
A C++20-ban egy többlépcsős platform- vagy feature-választó láncot így kellett írni:
#if defined(__linux__)
# define PLATFORM "linux"
#elif defined(__APPLE__)
# define PLATFORM "darwin"
#else
# define PLATFORM "unknown"
#endifA P2334 bevezette a #elifdef és #elifndef
direktívákat. Szemantikailag #elifdef AZONOSITO pontosan
ekvivalens a #elif defined(AZONOSITO) kifejezéssel,
#elifndef pedig a
#elif !defined(AZONOSITO)-val. Két feltétel fontos.
Egyrészt a direktíva operandusa egyetlen azonosító, nem
kifejezés, ezért a #elifdef defined(X) alak érvénytelen —
ez a leggyakoribb hiba, mert a szerkezet #if-re emlékeztet.
Másrészt a #elifdef csak a feltételes fordítás láncának
folytatásaként állhat: #if, #ifdef,
#ifndef vagy egy korábbi #elif* után; önállóan
nem.
A P2437 a #warning direktívát szabványosította. Hatása:
az implementációnak egy diagnosztikai üzenetet kell kibocsátania, amely
tartalmazza a direktíva szövegét, de a program nem lesz
ill-formed, és a fordítás alapértelmezés szerint folytatódik.
Ez a lényegi különbség a #error-hoz képest, és ezért a
-Werror sem feltétlenül állítja meg a buildet: a
#warning nem warning, hanem szabványos diagnosztika. A
C++20-as fallback #pragma message("..."), amely viszont
implementációfüggő, és nem minden fordító ismeri.
Kidolgozott példa — platformválasztó lánc, #warning-gal
a „nem várt” ágon:
// pp23.cpp
#include <cstdio>
#if defined(__linux__)
# define PLATFORM "linux"
#elifdef __APPLE__
# define PLATFORM "darwin"
#elifdef _WIN32
# define PLATFORM "windows"
#else
# warning "ismeretlen platform: a platform-specifikus kod talan nem fordul"
# define PLATFORM "unknown"
#endif
int main() {
std::printf("platform: %s\n", PLATFORM);
}g++ -std=c++23 -Wall -Wextra -Wpedantic -o pp23 pp23.cppA __linux__, __APPLE__, _WIN32
makrók nem szabványosak, hanem fordító/OS specifikusak; itt csak a
direktívalánc illusztrálására szolgálnak. A várt viselkedés leírás, nem
mért eredmény: Linuxon a linux szöveg jelenik meg, a
#warning pedig csak az #else ágon fut.
A P0330 két új egész literálsuffixumot vezetett be. A
z/Z suffixszel jelölt literál típusa a
std::size_t előjeles párja, azaz
std::make_signed_t<std::size_t> (LP64-en
long); az uz/UZ suffixszel jelölt
literál típusa maga a std::size_t (LP64-en
unsigned long). A literál értékének ábrázolhatónak kell
lennie a megfelelő típusban; ha nem, a program ill-formed, tehát itt
nincs csendes túlcsordulás.
A gyakorlati nyereség a signed/unsigned keveredés és a felesleges castok eltüntetése. C++20-ban például:
template <class C>
constexpr auto last_index(C const& c) {
return c.size() == 0 ? std::size_t{0} : c.size() - 1; // std::size_t
}C++23-ban ugyanez 0uz-vel olvashatóbb, és a
size() eredményével való összehasonlítás nem kever
előjelet:
template <class C>
constexpr auto last_index(C const& c) {
return c.size() == 0uz ? 0uz : c.size() - 1uz; // mindenhol std::size_t
}A signed változat akkor hasznos, ha a ciklusváltozó negatív is lehet,
vagy ha int-tel kell összevetni:
for (auto i = 0z; i < static_cast<decltype(i)>(v.size()); ++i).
Itt a 0z típusa long, így a
v.size()-t egyszer kell konvertálni, és a
-Wsign-compare csendben marad. Figyelem: a z
nem azt jelenti, hogy „size_t”, hanem azt, hogy „a size_t előjeles
párja” — a két suffix tehát nem ugyanannak a típusnak a két neve.
A P2071 a \N{KARAKTERNÉV} univerzális
karakternév-escape-et vezette be: az \u/\U
kódszám helyett az Unicode Character Database-ben definiált
név alapján azonosítja a karaktert. A P2290 ehhez
kapcsolódva a \x, \u, \U
escape-eknek kapcsos zárójeles, határolt alakját adta:
\x{...}, \u{...}, \U{...}.
Mindkettő ugyanazt a problémát oldja meg, csak más oldalról.
A \x escape hagyományosan „mohó”: a fordító a lehető
leghosszabb hexadecimális számjegy-sorozatot fogyasztja, ezért a
"\xff" "f" trükk kellett ahhoz, hogy egy 0xFF
byte után f betű következzen. A "\x{ff}f" alak
ezt explicit módon határolja, és a kapcsos alakban a számjegyek száma is
korlátlan, ami a \u/\U esetében a 4, illetve 8
jegyű korlátot szünteti meg (\u{1F600}).
A \N{...} és a \x{...} közötti különbség
tanulságos. A \x{e9} egyetlen kódpontot jelöl, amelynek a
karakterliterál típusába kell illeszkednie; char literálban
ez a 0xE9 byte. A
\N{LATIN SMALL LETTER E WITH ACUTE} ugyanezt a kódpontot
nevezi meg, de a stringliterál kódolása dönti el, hány
byte lesz belőle: UTF-8 végrehajtási karakterkészlet esetén kettő
(0xC3 0xA9). A kettő tehát nem ekvivalens:
#include <cstdio>
#include <cstring>
int main() {
const char* hexed = "\x{e9}"; // 1 byte: 0xE9
const char* named = "\N{LATIN SMALL LETTER E WITH ACUTE}"; // UTF-8: 2 byte
std::printf("hexed=%zu named=%zu\n", std::strlen(hexed), std::strlen(named));
}g++ -std=c++23 -Wall -Wextra -o esc esc.cppA névfeloldás támogatása nem kötelező minden implementációban, ezért
itt különösen indokolt a compile probe: ha a \N{...}-t
tartalmazó fájl nem fordul, a fallback a \u00E9 alak. A
pontos egyeztetési szabályt (a név és a kódpont megfeleltetését) a
szabvány [lex.charset] része rögzíti; erre itt nem térek ki
részletesen.
A P2295 kimondja, hogy a C++23 implementációnak a forrásfájlt UTF-8
kódolásúként is el kell tudnia olvasni. Ez fontos különbség a korábbi
helyzethez képest, ahol a forráskód karakterkészlete implementációfüggő
volt. Két dolgot viszont nem jelent. Egyrészt nem jelenti azt, hogy a
char stringliterál automatikusan UTF-8 lesz: azt a
végrehajtási karakterkészlet (execution character set)
dönti el, amely GCC/Clang esetén külön kapcsolóval
(-fexec-charset=UTF-8) állítható. Másrészt a
u8"..." literál típusa const char8_t[] (C++20
óta), és a char8_t konverziója char-rá nem
implicit.
Tipikus hiba a gyakorlatban a nem UTF-8 kódolású forrásfájl
(Windows-1250, Latin-1), amelyet a fordító UTF-8-ként olvas, és „invalid
UTF-8 sequence” hibát ad, vagy rossz karaktert tesz a literálba. A
diagnózis: file -i forras.cpp, illetve xxd az
érintett soron. A javítás nem a \x-szel való kézi kódolás,
hanem a fájl konvertálása (iconv) és a build során a
kódolás deklarálása. A BOM kezelése szintén implementációfüggő; a
laborban érdemes BOM nélküli UTF-8-at használni.
A P1467 hozta létre a <stdfloat> headert és az ott
definiált típusokat: std::float16_t,
std::float32_t, std::float64_t,
std::float128_t és std::bfloat16_t. A
std::floatN_t azt a típust jelöli, amely az ISO/IEC/IEEE
60559 binaryN formátum értékkészletét legalább lefedi (N =
16, 32, 64, 128); a std::bfloat16_t a bfloat16
értékkészletét. A típus létezése nem kötelező: a
szabvány akkor írja elő, ha az implementáció rendelkezik ilyen típussal,
és ezt a __STDCPP_FLOAT16_T__,
__STDCPP_FLOAT32_T__, __STDCPP_FLOAT64_T__,
__STDCPP_FLOAT128_T__, illetve
__STDCPP_BFLOAT16_T__ makró definiálásával jelzi. A
<stdfloat> header tartalmára a
__cpp_lib_stdfloat feature-test makró utal; a konkrét
értéket a <version>-ből kell kiolvasni.
A típusokhoz literálsuffixumok is tartoznak: f16,
f32, f64, f128,
bf16. A 1.0f16 típusa
std::float16_t, a 1.0bf16 típusa
std::bfloat16_t. A std::float32_t gyakori
targeten azonos a float-tal, de ezt a szabvány nem
garantálja; ABI-határon ezért nem szabad a kettőt felcserélhetőnek
tekinteni.
A konverziós szabályok gyakorlati magja: a float16_t és
bfloat16_t a szokásos aritmetikai konverziók során a
float felé promoválódik, így a velük végzett aritmetika
legalább float pontossággal történik, és az eredmény
tároláskor veszít pontosságot. A float16_t ↔︎
float és float16_t ↔︎ double
konverzió implicit, de szűkítő lehet; {} inicializálásnál
konstans kifejezés esetén ez fordítási hiba, változó esetén
-Wconversion figyelmeztet. A binary16 kb. 3
tizedesjegy pontosságú, legnagyobb véges értéke 65504; a
bfloat16 kevesebb mantisszabit ad (kb. 2–3 tizedesjegy), de
a float exponenstartományát tartja meg — ezért gépi
tanulásban népszerű.
Kidolgozott példa feature-testtel és fallbackkal:
// stdfloat_probe.cpp
#include <cstdio>
#include <version>
#if defined(__STDCPP_FLOAT16_T__)
# include <stdfloat>
#endif
int main() {
#if defined(__STDCPP_FLOAT16_T__)
std::float16_t h = 1.0f16 / 3.0f16;
std::float32_t f = 1.0f32 / 3.0f32;
std::printf("float16=%.6f float32=%.6f sizeof=%zu/%zu\n",
static_cast<double>(h), static_cast<double>(f),
sizeof h, sizeof f);
#else
std::puts("std::float16_t nem elerheto ezen a targeten (fallback: float)");
#endif
}g++ -std=c++23 -Wall -Wextra -Wconversion -o stdfloat_probe stdfloat_probe.cppA várt kimenet leírás, nem mért eredmény: float16 esetén
a binary16 kerekítése miatt kb. 0.333252,
float32 esetén kb. 0.333333 jelenne meg. A
{} inicializálás szűkítését érdemes külön kipróbálni
tanítási hibaként:
std::float16_t h{1.0 / 3.0}; // hiba: double -> float16_t szukito, nem ábrázolható pontosan
std::float16_t h2 = 1.0 / 3.0; // implicit konverzió, -Wconversion jelziA preprocesszor terén a C23 is átvette a #elifdef,
#elifndef és #warning direktívákat, így a két
nyelv között ezek hordozhatók — a pontos C23 paragrafusokat és az
esetleges eltéréseket a C23-as részek tárgyalják (C23 könyvtár és preprocessor). A literálsuffixumok
és escape-ek terén viszont nincs átfedés: a
z/uz, az f16/bf16 és
a \N{...}/\x{...} C++-specifikusak, ezért
kétnyelvű fejlécben #ifdef __cplusplus védelem vagy makró
szükséges. A <stdfloat> C++ header; a C
_Float16/_Float32 típusai és a
std::float16_t között nincs garantált megfeleltetés, ezért
extern "C" határon ezeket nem szabad binárisan átadni az
ABI ellenőrzése nélkül. A char8_t és a u8
literál mindkét nyelvben létezik (C++20, illetve C23), de a
char8_t C++-ban erősebb típus; a C23-as változat részletei
szintén a C23-as fejezetben szerepelnek.
A leggyakoribb hiba a #elifdef defined(X) írásmód: az
operandus azonosító kell, nem kifejezés; a javítás
#elifdef X. A második, hogy a #warning-ot
hibának hiszik: nem állítja meg a fordítást, hibához #error
kell. A harmadik, hogy a z suffixet size_t-nek
válik: az uz a size_t, a z a
párja. A negyedik, hogy a "\x{e9}"-et UTF-8
é-nek tekintik: az egyetlen byte, a helyes UTF-8
"\N{LATIN SMALL LETTER E WITH ACUTE}" vagy
u8"\u00E9". Az ötödik, hogy a
#ifdef __STDCPP_FLOAT16_T__ és a
__cpp_lib_stdfloat közé egyenlőséget tesznek: az előbbi a
konkrét típusra, az utóbbi a header tartalmára utal. Végül a hatodik: a
std::float32_t-t ABI-határon float-ként
kezelik, holott ez nem garantált.
platform.h
fejlécet, amely
#if/#elifdef/#elifndef lánccal
három platformot különböztet meg, és az ismeretlen ágon
#warning-ot ad. Magyarázd el, miért hiba a
#elifdef defined(__linux__).std::vector<int> v és egy int i. Írj egy
bool in_range(int, std::size_t) segédfüggvényt, amely
0z/0uz használatával elkerüli a
signed/unsigned keveredést, és indokold, miért nem elég a puszta
static_cast.const char* s = "\x{e9}";. Add meg a strlen(s)
értékét és az okát, majd javítsd a kódot UTF-8 é-re. Add
meg az ellenőrzési feltételt is.std::float16_t-t használ, ha elérhető, különben
float-ra esik vissza, és a visszaesést
#warning-gal jelzi. Sorold fel a szükséges feature-test
makrókat.1. megoldás. A lánc:
// platform.h
#if defined(__linux__)
# define PLATFORM_ID 1
#elifdef _WIN32
# define PLATFORM_ID 2
#elifndef __unix__
# warning "nem unix-szeru platform: a POSIX kodutak nem fordulnak"
# define PLATFORM_ID 0
#else
# define PLATFORM_ID 3
#endifA #elifdef defined(__linux__) azért hiba, mert a
direktíva operandusa egyetlen azonosító; a defined operátor
csak #if/#elif kifejezésben értelmezhető.
Ellenőrzés: a fájl fordul -std=c++23-mal, és az
#else ágra kényszerítve
(-U__linux__ -U__unix__) megjelenik a #warning
üzenet, de a fordítás nem áll meg.
2. megoldás.
#include <cstddef>
#include <vector>
bool in_range(int i, std::size_t n) {
return i >= 0z && static_cast<std::size_t>(i) < n;
}Itt a 0z típusa long (LP64-en), így az
i >= 0z összehasonlítás előjeles marad, és nem kell a
negatív i-t unsigned-dá alakítani — ez a
static_cast-tal nem oldható meg, mert a cast után a
-1 nagy unsigned értékké válik. A második feltételben a
cast csak a nemnegatív ágon fut, ahol értéke megőrződik. Ellenőrzés:
i = -1, n = 10 esetén false;
i = 3, n = 10 esetén true;
-Wsign-compare nem jelez.
3. megoldás. A "\x{e9}" egyetlen
char, értéke 0xE9, ezért
strlen(s) == 1. UTF-8-ban az é (U+00E9) két
byte: 0xC3 0xA9. Javítás:
const char* s = "\N{LATIN SMALL LETTER E WITH ACUTE}"; // vagy u8"\u00E9"Ellenőrzési feltétel: strlen(s) == 2, és
xxd-vel a byte-ok c3 a9. Ha a
\N{...} nem fordul, a fallback "\u00E9",
illetve u8"\u00E9".
4. megoldás.
// f16_compat.h
#include <version>
#if defined(__STDCPP_FLOAT16_T__)
# include <stdfloat>
# define F16 std::float16_t
#else
# warning "std::float16_t nem elerheto: float fallback"
# define F16 float
#endifA szükséges makrók: __STDCPP_FLOAT16_T__ a típus
létezésére, __cpp_lib_stdfloat a
<stdfloat> header tartalmára. Ellenőrzés: a fejléc
fordul mindkét ágon; a static_assert(sizeof(F16) >= 2)
nem garantált, ezért a méretre ne építsünk szerződést.
A C++23 forrásszintű újdonságai jól elkülöníthető csoportokba esnek.
A preprocessor
#elifdef/#elifndef/#warning
direktívái a feltételes fordítás zaját csökkentik, de operandszerződésük
szigorú: azonosító, nem kifejezés. A z/uz
literálsuffixum a size_t és előjeles párja típusát rögzíti
a literál szintjén, megszüntetve a castokat. A \N{...} és a
\x{...}/\u{...}/\U{...} escape-ek
a karakterazonosítást teszik olvashatóvá, illetve határolttá; a
char és a stringliterál kódolása továbbra is két külön
kérdés. Az UTF-8 forrás támogatása kötelező, de a végrehajtási
karakterkészlet nem automatikusan UTF-8. A <stdfloat>
típusai feltételesek, ezért feature-test és compile probe kötelező; a
float16_t/bfloat16_t aritmetikája
float felé promoválódik, és a tárolás pontosságvesztéssel
jár. A C23-mal való párhuzam a preprocesszorra korlátozódik; a literál-
és típuskészlet eltér, ezért a kétnyelvű határon minden ilyen elemet
ellenőrizni kell.