A C++20 bevezette a constexpr és consteval
alapokat, valamint a std::is_constant_evaluated() függvényt
annak eldöntésére, hogy az aktuális végrehajtás konstans kiértékelésben
történik-e. Ez utóbbi azonban hamar kiderült, hogy csak félmegoldás:
futásidejű függvényhívásként viselkedik, ezért a nem kívánt ág is
ugyanolyan szemantikával rendelkezik, és főleg nem tette lehetővé
consteval (immediate function) hívását az ágban. A C++23
két irányból javított ezen: egyrészt bevezette az
if consteval és if !consteval szerkezeteket,
amelyek a futásidejű és a konstans ágat valóban szétválasztják, másrészt
lazította a constexpr függvénytörzs megengedett
szerkezeteit (P2242, P2647). Jelen rész a constexpr,
consteval és constant evaluation (33.1) alfejezetben bevezetett
alapokra támaszkodik, és azt mélyíti el; az itt tárgyalt
consteval-propagálás (P2564) státusza csak korlátozottan
áll rendelkezésre, ezt külön jelzem. A fejezet feltételezi a Compile-time adatok és típusos konfiguráció (33.2)
alfejezetben bemutatott compile-time adatszerkezetek ismeretét (pl.
típusos konfiguráció constexpr táblákkal).
if consteval
és if !consteval szemantikájaAz if consteval egy új utasításforma, nem könyvtári
függvény. Pontos feltétel: a feltétel akkor teljesül, amikor az aktuális
függvényhívását manifestly constant-evaluated kontextusban értékeljük
(azaz olyan kifejezésben, amelynek a kiértékelése kötelezően konstans,
vagy egy consteval függvény törzsében vagyunk). Két
szintaktikai alak létezik:
if consteval { T; } else { E; } – a T ág a
konstans, az E ág a futásidejű ág.if !consteval { E; } else { T; } – ugyanez fordított
sorrendben; a ! alak arra kényelmes, amikor a futásidejű
ágat írjuk előre.Fontos elhatárolás: mindkét ágat szintaktikailag
ellenőrzi a fordító; a névfeloldás és az overload-resolúció mindkettőre
lefut. A különbség szemantikai: a megtett ág az adott
kontextusban érvényes szabályok szerint értékelődik. Konkrétan a
T (konstans) ág immediate function kontextusban fut, ezért
ott consteval függvény hívható; az E ág
viszont közönséges futásidejű kontextus, ahol consteval
hívás hiba.
Ebből következik, hogy az if consteval nem ekvivalens a
std::is_constant_evaluated() feltétellel. A
bool visszatérésű függvényhívás után a fordító mindkét ágat
közönséges utasításnak tekinti: ha bármelyik ágban
consteval hívást írunk, már fordítási hibát kapunk, mert az
is_constant_evaluated() által megkülönböztetett ágak egyike
sem immediate kontextus. Emiatt a régi minta csak akkor használható, ha
mindkét ág közönséges, konstans kiértékelésre is és futásidőre is
alkalmas kifejezéseket tartalmaz. További gond, hogy
std::is_constant_evaluated() a constexpr
lambda és if constexpr halmozott szerkezeteiben nehezen
áttekinthető állapotokat hozott létre, mert a standard “manifestly
constant-evaluated” kategóriáját nem jeleníti meg explicit módon. Az
if consteval ezt kikényszeríti: a szerzőnek nyilatkoznia
kell, melyik ág melyik kontextushoz tartozik.
Első kidolgozott példa: egy runtime és egy konstans ág
consteval segédfüggvénnyel.
// if_consteval_basic.cpp
// Fordítás: g++ -std=c++23 -O2 -Wall -Wextra -pedantic if_consteval_basic.cpp -o if_consteval_basic
// vagy clang++ -std=c++23 -O2 -Wall -Wextra -pedantic ...
#include <cstdio>
#if !defined(__cpp_if_consteval) || __cpp_if_consteval < 202106L
# error "if consteval nem elerheto; ellenorizd a -std=c++23 tamogatast"
#endif
consteval int twice_immediate(int x) {
return x * 2; // csak konstans kontextusbol hivhato
}
constexpr int compute(int x) {
if consteval {
return twice_immediate(x); // OK: ez az ag immediate kontextus
} else {
return x * 2; // kozonseges futasideju ut
}
}
int main() {
static_assert(compute(21) == 42); // konstans ag
int r = compute(21); // futasideju ag
std::printf("compute(21) = %d\n", r);
}A példa nem tesztelt a jelen dokumentumban; a várt viselkedés az,
hogy a static_assert a fordítás során igazolódik, a
futásidejű hívás pedig a 42 értéket írja ki. A
__cpp_if_consteval makró a szabvány szerinti feature-test;
hiányában a fordító nem támogatja ezt a formát, és a fallback ágra kell
váltani. C++20 fallbackként csak
std::is_constant_evaluated() használható, és az immediate
hívás elhagyásával kell élni.
A ! alak ugyanezt kínálja, csak a futásidejű ág kerül
előre:
constexpr int compute_alt(int x) {
if !consteval {
return x * 2;
} else {
return twice_immediate(x);
}
}goto és címkeC++20-ban a constexpr függvény törzse nem
tartalmazhatott nem literál típusú változót, goto utasítást
vagy címkét. A P2242 ezt a szintaktikai tilalmat megszünteti: a függvény
definíciója ezekkel együtt is érvényes
constexpr függvény. A megengedés azonban nem korlátlan: ha
a konstans kiértékelés során a végrehajtás eléri a nem literál típusú
változó létrehozását, illetve a tiltott konstrukciót, akkor az adott
kiértékelés nem lesz konstans kifejezés (a program futásidőben
változatlanul működik). A lazítás gyakorlati értéke vegyes kód esetén
nagy: #ifdef alapú ágak, goto cleanup stílusú
hibakezelés, valamint futásidejű segédobjektumok, amelyeket
if consteval zár ki a konstans ágból.
// p2242_goto.cpp
// Fordítás: g++ -std=c++23 -O2 -Wall -Wextra -pedantic p2242_goto.cpp -o p2242_goto
#include <cstdio>
constexpr int abs_like(int x) {
if (x >= 0) goto done; // P2242: goto engedelyezett
x = -x;
done: // P2242: cimke engedelyezett
return x;
}
static_assert(abs_like(-7) == 7); // C++23-ban mukodik
#include <vector>
constexpr int mixed(int x) {
if consteval {
return x * 7;
}
// Kovetkezo resz csak futasidoben erheto el; P2242 elengedte a
// definicio szintu tiltast, igy a std::vector deklaracio itt mar OK.
std::vector<int> scratch(10, x);
return static_cast<int>(scratch.size());
}
int main() {
std::printf("abs_like(-7)=%d, mixed(3) futasidoben=%d\n",
abs_like(-7), mixed(3));
}A constexpr int mixed(int x) konstans kiértékeléssel
hívva a scratch deklarációjához nem ér el, ezért a konstans
hívás sikeres; futásidőben viszont a std::vector létrejön.
C++20-ban ez a függvény fordítási hiba lett volna, mert
a törzs std::vector-t deklarál.
static constexpr változó constexpr
függvénybenC++23 előtt bármilyen static lokális változó tilos volt
constexpr függvényben. A P2647 csak a
static constexpr esetet engedi meg, mert az ilyen változó
konstans-inicializált és megváltoztathatatlan, így nem visz be hívásközi
állapotot, és nem akadályozza a konstans kiértékelést. Gyakorlati haszna
előre számolt táblák egyszeri definiálása a függvényen belül, anélkül,
hogy a tábla a névtérbe szivárogna.
// p2647_static.cpp
// Fordítás: g++ -std=c++23 -O2 -Wall -Wextra -pedantic p2647_static.cpp -o p2647_static
#include <cstdio>
constexpr int fib(int n) {
static constexpr int t[] = {0,1,1,2,3,5,8,13,21,34};
if (n < 0 || n > 9) return -1;
return t[n];
}
static_assert(fib(8) == 21);
static_assert(fib(-1) == -1);
int main() {
std::printf("fib(8)=%d\n", fib(8));
}C++20 fallbackként a táblát a függvényen kívül, detail
névtérben kell definiálni, és a függvény paraméterként kapja meg, vagy
constexpr referenciát használ. A két változat viselkedése
azonos; a feature-test (__cpp_constexpr >= 202207L)
döntése a fordító támogatásán múlik, és a konkrét értéket érdemes a
fordító dokumementációjából ellenőrizni.
constexpr könyvtári elemekA C++23 könyvtári oldalon is bővült a konstans kiértékelhető
eszköztár. A std::unique_ptr (P2273) konstans
kiértékelésben is használható, amennyiben a new és
delete hívások a C++20 óta constexpr
allokációt jelentenek, tehát a mutatott objektum a konstans kiértékelés
élettartamán belül keletkezik és szűnik meg. A
<cmath> és <cstdlib> részben
constexpr (P0533 és kapcsolódó javaslatok): a függvények
egy része feltételesen constexpr, a
math_errhandling konfigurációjától függően. Ez azt jelenti,
hogy adott célplatformon és adott standard library-vel a függvény lehet,
hogy nem constexpr; kizárólag feature-test makró és compile
probe dönthet. A std::abs egész változata régóta
constexpr, a lebegőpontos <cmath>
függvények (pl. std::sqrt, std::pow) C++23-tól
feltételesen konstans kiértékelhetők.
// libcx23.cpp
// Fordítás: g++ -std=c++23 -O2 -Wall -Wextra -pedantic libcx23.cpp -o libcx23
#include <cstdio>
#include <memory>
constexpr int unique_demo() {
auto p = std::make_unique<int>(42);
return *p;
}
static_assert(unique_demo() == 42); // C++23 + tamogato libstdc++/libc++ eseten
int main() {
std::printf("unique_demo()=%d\n", unique_demo());
}A pontos feature-test makró neve (tipikusan a
__cpp_lib_constexpr_* családból) és aktuális értéke függ a
standard library kiadásától; a kód előtt compile probe-bal kell
ellenőrizni. A <cmath> feltételes
constexpr jellege miatt
static_assert(std::sqrt(2.0) > 1.4) nem hordozható, és
célplatformonként változhat.
consteval-propagálás (P2564) státuszaA P2564 (“consteval needs to propagate up”) egy finom rést zár be: ha
consteval függvényt hívunk egy constexpr
függvényből, és a külső constexpr függvényt futásidőben
hívjuk, a korábbi szabályozás nem garantálta egyértelműen, hogy a belső
consteval hívás fordítási időben értékelődjön. A javaslat a
consteval hívást “felfelé” propagálja, azaz a befoglaló
constexpr függvény hívását is a konstans kiértékelés
kontextusába kényszeríti, amíg a consteval hívás elérhető.
Az állítás pontos megfogalmazása és elfogadási státusza a WG21
jegyzőkönyvekből ellenőrizhető; jelen fejezetben nem áll rendelkezésre
forrás, ezért a P2564-re építő kód előtt érdemes a fordító
dokumementációjában ellenőrizni, hogy a támogatás megvan-e. A jelen
könyv C++23 alapprofilja a P2564-et explicitté tett
if consteval formákkal helyettesíti ott, ahol a viselkedés
hordozhatósága kritikus.
A helyes gyakorlat: a constexpr függvény mindkét ágát
ugyanarra az eredményre tervezzük, majd a konstans ágat
static_assert-tel, a futásidejű ágat futásidejű
ellenőrzéssel validáljuk. A két értéknek egyeznie kell.
// branch_agree.cpp
// Fordítás: g++ -std=c++23 -O2 -Wall -Wextra -pedantic branch_agree.cpp -o branch_agree
#include <cassert>
#include <cstdio>
constexpr int clamp(int v, int lo, int hi) {
if consteval {
// konstans ag: egyszeru, olvashato
if (v < lo) return lo;
if (v > hi) return hi;
return v;
} else {
// futasideju ag: azonos logika, peldakent kulon megirva
return (v < lo) ? lo : (v > hi ? hi : v);
}
}
int main() {
static_assert(clamp(-1, 0, 10) == 0);
static_assert(clamp( 5, 0, 10) == 5);
static_assert(clamp(42, 0, 10) == 10);
for (int v = -5; v <= 15; ++v) {
assert(clamp(v, 0, 10) ==
(v < 0 ? 0 : (v > 10 ? 10 : v)));
}
std::printf("minden ertek egyezik\n");
}A assert futásidejű ellenőrzés, a
static_assert fordítási. A példa nem lett lefuttatva; a
várt viselkedés az, hogy a program a “minden ertek egyezik” üzenetet
írja ki.
consteval hívás a futásidejű ágban. Az
else ág közönséges kontextus, ezért consteval
hívás ott fordítási hiba. Javítás: a hívást a T (konstans)
ágba helyezni.is_constant_evaluated() és
if consteval felcserélése. Az előbbi nem tesz
lehetővé consteval hívást, és a fordító mindkét ágat azonos
módon szemantizálja. Utóbbi a helyes eszköz.<cmath>
constexpr-re. A függvény feltételesen
constexpr; célonként és library-verziónként változhat,
ezért feature-test és compile probe kötelező.A C++23 if consteval szerkezete a konstans és futásidejű
ág valódi szétválasztását adja, és egyedül ez teszi lehetővé
consteval hívását a konstans ágban. A P2242 feloldotta a
nem literál típusú változókra, goto-ra és címkékre
vonatkozó definíciós tilalmat; a P2647 engedélyezte a
static constexpr lokálisokat. A
std::unique_ptr és a
<cmath>/<cstdlib> egyes függvényei
részben constexpr-ré váltak, de feltételesen, ezért
feature-test és compile probe szükséges. A P2564 státusza a jelen
fejezetben nem ellenőrzött, a hordozható kód
if consteval-lel tegye explicitté a szándékot. Végül a két
ág egyezését static_assert és futásidejű
assert kombinációjával érdemes tesztelni.
1. feladat (diagnosztika). Az alábbi C++20 kód nem fordul. Magyarázd meg a fordítási hiba okát, majd írd át C++23-ra.
consteval int ci(int x) { return x + 1; }
constexpr int f(int x) {
if (std::is_constant_evaluated()) {
return ci(x);
}
return x;
}2. feladat (kódolás). Készíts constexpr
gcd(a,b) függvényt, amely if consteval-lel az
Euklideszi algoritmus egyszerű, olvasható változatát használja konstans
kiértékelésben, futásidőben pedig ugyanezt a logikát külön (például
bitmanipulációs) alakban. Igazold static_assert-tel
legalább három inputra, és futásidejű for ciklussal
ellenőrizd, hogy a két ág minden inputra egyezik.
3. feladat (kombinált). Írj constexpr
faktoriális-függvényt, amely előre számolt
static constexpr int táblát használ. Adj C++20 fallbacket,
ahol a tábla a függvényen kívül, detail névtérben van. A
feature-detection a __cpp_constexpr makróra épüljön.
static_assert-tel és futásidejű assert-tel
bizonyítsd, hogy mindkét változat ugyanazt adja.
4. feladat (magyarázó). Mi a szemantikai különbség
aközött, hogy egy consteval hívás az
if consteval konstans ágában, illetve a futásidejű ágában
szerepel? Miért tekinthető úgy, hogy a “nem megtett ág” szintaktikailag
mégis létezik?
1. megoldás. A hiba oka: a ci(x) egy
consteval függvény hívása egy közönséges if
blokkban, ahol a bool-t visszaadó
std::is_constant_evaluated() feltételként szerepel. A
fordító a f függvényt nem tekinti immediate functionnek,
így a ci(x) hívását nem az immediate kontextus szabályai
szerint engedélyezi: mindkét ág közönséges utasítás, ezért a
ci hívás már önmagában fordítási hiba. Helyes C++23
változat:
constexpr int f(int x) {
if consteval {
return ci(x);
}
return x;
}Ellenőrzés: static_assert(f(41) == 42); és futásidejű
hívás esetén a 41 visszaadása. Feature-test:
#if defined(__cpp_if_consteval) && __cpp_if_consteval >= 202106L,
különben a ci hívást elhagyó fallback.
2. megoldás.
// gcd_demo.cpp
// Fordítás: g++ -std=c++23 -O2 -Wall -Wextra -pedantic gcd_demo.cpp -o gcd_demo
#include <cassert>
#include <cstdio>
#include <cstdlib>
constexpr int gcd(int a, int b) {
if consteval {
while (b != 0) {
int t = a % b;
a = b;
b = t;
}
return a < 0 ? -a : a;
} else {
a = a < 0 ? -a : a;
b = b < 0 ? -b : b;
while (b != 0) {
int t = a % b;
a = b;
b = t;
}
return a;
}
}
int main() {
static_assert(gcd(12, 18) == 6);
static_assert(gcd(17, 5) == 1);
static_assert(gcd( 0, 9) == 9);
for (int a = -20; a <= 20; ++a) {
for (int b = -20; b <= 20; ++b) {
int expected = (b == 0) ? std::abs(a)
: gcd(std::abs(a), std::abs(b));
assert(gcd(a, b) == expected);
}
}
std::printf("gcd: minden ertek egyezik\n");
}Indoklás: mindkét ág ugyanazt az Euklideszi algoritmust futtatja; a
konstans ág rövidebb, mert a konstans kiértékelésben nincs szükség az
abs előzetes normalizálására (a standard könyvtár
std::abs futásidejű hívása helyett a konstans ág
közvetlenül kezeli az előjelet). Ellenőrzési feltétel: minden inputpárra
gcd(a,b) == gcd(std::abs(a), std::abs(b)), illetve a
b != 0 esetben a klasszikus megfelelés.
3. megoldás.
// fact_static.cpp
// Fordítás: g++ -std=c++23 -O2 -Wall -Wextra -pedantic fact_static.cpp -o fact_static
#include <cassert>
#include <cstdio>
#if defined(__cpp_constexpr) && __cpp_constexpr >= 202207L
constexpr int fact(int n) {
static constexpr int t[] = {1,1,2,6,24,120,720,5040,40320,362880};
if (n < 0 || n > 9) return -1;
return t[n];
}
#else
namespace detail {
constexpr int fact_table[] = {1,1,2,6,24,120,720,5040,40320,362880};
}
constexpr int fact(int n) {
if (n < 0 || n > 9) return -1;
return detail::fact_table[n];
}
#endif
int main() {
static_assert(fact(0) == 1);
static_assert(fact(5) == 120);
static_assert(fact(9) == 362880);
static_assert(fact(-1) == -1);
for (int n = -2; n <= 10; ++n) {
int got = fact(n);
int want;
if (n < 0 || n > 9) want = -1;
else {
want = 1;
for (int i = 2; i <= n; ++i) want *= i;
}
assert(got == want);
}
std::printf("fact: minden ertek egyezik\n");
}Indoklás: a static constexpr int t[]
konstans-inicializált és megváltoztathatatlan, ezért a
constexpr függvény törzsében való definíciója nem visz be
hívásközi állapotot, és nem akadályozza a konstans kiértékelést. A
fallback változat ugyanazt a táblát a detail névtérben
definiálja, a függvény pedig csak referál rá. Ellenőrzési feltétel:
mindkét változat minden inputra ugyanazt adja, és a -1
érzéketlen tartományon kívülre eső inputokra is helyesen jelez.
4. megoldás. A consteval függvény csak
immediate kontextusból hívható. Az if consteval
T ága ilyen kontextus: a megtett ág konstans kiértékelésben
fut, ezért a consteval hívás érvényes. Az E ág
futásidejű kontextus: ott a consteval hívás olyan, mintha
egy közönséges függvényből próbálnánk egy immediate functiont hívni,
ezért fordítási hiba. A “nem megtett ág” onnan “létezik”, hogy a
fordítónak mindkét ágat szintaktikailag és névfeloldásilag ellenőriznie
kell: a nyelvtan megköveteli, hogy a nem megtett ágban is érvényes kód
álljon, csak a konkrét szemantikai érvényesség (például
consteval hívás engedélyezettsége) tér el a kontextus
szerint. Ez a kettősség adja az if consteval lényegét: a
döntés fordítási idejű, de a kód mindkét ágon értelmes.