40.4 Konstans kiértékelés C++23-ban: if consteval és constexpr bővítések

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).

40.4.1 Az if consteval és if !consteval szemantikája

Az 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:

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);
    }
}

40.4.2 P2242: nem literál típusú változó, goto és címke

C++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.

40.4.3 P2647: static constexpr változó constexpr függvényben

C++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.

40.4.4 Részben constexpr könyvtári elemek

A 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.

40.4.5 A consteval-propagálás (P2564) státusza

A 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.

40.4.6 A két ág eredményének tesztelése

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.

40.4.7 Tipikus hibák és javításuk

40.4.8 Összefoglaló

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.

40.4.9 Feladatok

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?

40.4.10 Megoldások

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.