33.5 Feature-test, standard library és build capability

A fejezet gerincét a korábbi részekben már rögzítettük: a constexpr kiértékelés szabályait constexpr, consteval és constant evaluation (33.1), az ODR-t ODR, inline és több fordítási egység (33.3), a modulokat pedig C++20 modules és header unit (33.4) tárgyalta. Itt most nem ezeket ismételjük, hanem azt a gyakorlati kérdést vesszük elő, amely mindháromhoz hozzákapcsolódik: honnan tudom meg, hogy egy adott fordítási lánc tényleg tudja-e azt, amit a példakódom feltételez? A modern C++ példatár gyorsabban mozdul, mint az intézményi laborkörnyezet, és a -std=c++23 kapcsoló önmagában semmit nem garantál. A cél egyetlen, karbantartható capability-tábla felépítése, amely külön kezeli a nyelvi és a könyvtári oldalt, és amelyet configure time-ban, valódi fordítással ellenőrzünk.

33.5.1 A capability kérdés pontos megfogalmazása

Definíció (capability). Egy adott (compiler, standard library, nyelvi mód, target) négyes esetén egy F nyelvi vagy könyvtári funkció elérhető, ha létezik olyan program, amely F-et a szabvány által előírt módon használja, és a lánc azt fordítási hiba és diagnosztikai figyelmeztetés nélkül lefordítja, majd a dokumentált viselkedést produkálja. Az elérhetőség nyilatkozata ehhez képest gyengébb állítás: valamilyen makró definiált, vagy a dokumentáció említi a funkciót.

Ez a megkülönböztetés nem szőrszálhasogatás. A feature-test makrók a szándékot rögzítik, a compile probe a tényt. Egy capability-tábla akkor használható, ha megmondja, melyik sor melyik bizonyítékon nyugszik.

Előfeltételek. A fejezet példái feltételezik, hogy a fordítót explicit nyelvi móddal hívjuk (-std=c++20 vagy újabb), GNU bővítések nélkül, Linux hosted user-space alatt. A pontos compiler- és library-kiadást a laborprofil rögzíti; ahol a szöveg konkrét verziót nem nevez meg, ott az állítás verziófüggő, és a tanulónak kell ellenőriznie.

33.5.2 __cplusplus és a __cpp_* nyelvi makrók

A __cplusplus makró értéke a nyelvi módot kódolja, nem a fordító kiadását és nem a könyvtárét. A C++20 alapértéke 202002L, a C++23-é 202302L; a C++17 201703L. Ezek a számok dátum-alapúak, és a szabvány rögzíti őket, tehát összehasonlításra alkalmasak:

#if __cplusplus >= 202002L
/* C++20 vagy újabb nyelvi mód */
#endif

Ez a vizsgálat azonban csak azt mondja meg, hogy a nyelvtan és a szemantika melyik kiadása aktív. Semmit nem mond arról, hogy a <format> header létezik-e, hogy a std::expected implementált-e, vagy hogy a std::stacktrace mögött van-e működő unwinder.

A nyelvi funkciók többségéhez a fordító külön, finomabb makrót definiál: ezek a __cpp_* alakúak, például __cpp_constexpr, __cpp_consteval, __cpp_constinit, __cpp_concepts, __cpp_modules, __cpp_if_consteval. A nevük azt sugallja, hogy egy az egyben megfelelnek egy funkciónak, de két dolgot fontos észrevenni. Egyrészt az értékük nem logikai igaz/hamis, hanem egy dátum, amely a funkció aktuális specifikációjának évjáratát jelöli — ugyanannak a makrónak több elfogadott értéke létezhet, és a nagyobb érték gazdagabb szemantikát ígér. Másrészt nem minden nyelvi funkcióhoz tartozik makró; a makró hiánya ezért nem bizonyíték a funkció hiányára, csak a makró hiányát bizonyítja. A #ifdef tehát konzervatív irányban használható: ha definiált, elfogadhatjuk; ha nem, compile probe kell.

33.5.3 <version> és a __cpp_lib_* makrók

A C++20-tól létezik a <version> header, amelynek egyetlen feladata a szabványos feature-test makrók közreadása. A könyvtári funkciók makrói __cpp_lib_* alakúak: __cpp_lib_format, __cpp_lib_print, __cpp_lib_expected, __cpp_lib_mdspan, __cpp_lib_stacktrace, __cpp_lib_source_location és így tovább.

Kritikus részlet, hogy ezeket a standard library implementációja definiálja, nem a compiler. Ugyanaz a g++ -std=c++20 hívás két különböző láncot jelenthet aszerint, hogy a driver melyik libstdc++-t és melyik fejlécet találja; és ugyanaz a Clang bináris hol a libstdc++-hoz, hol a libc++-hoz linkel. A nyelvi makrók és a könyvtári makrók ezért két független tengelyen mozognak.

A <version> jelenlétét magát is vizsgálni kell, és erre a C++17 óta szabványos __has_include való:

#if defined(__has_include)
#  if __has_include(<version>)
#    include <version>
#    define HAVE_VERSION_HEADER 1
#  endif
#endif
#ifndef HAVE_VERSION_HEADER
#  define HAVE_VERSION_HEADER 0
#endif

A __has_include szemantikája szűk: a preprocesszor #if kifejezésében használható, és azt válaszolja meg, hogy a megadott header megtalálható-e a keresési útvonalon. Azt nem mondja meg, hogy a header tartalma megfelel-e a várt szerződésnek, és nem helyettesíti a tényleges fordítási próbát. Ha a <version> hiányzik, a __cpp_lib_* makrók jellemzően definiálatlanok maradnak — de egyes implementációk más headerekből is közreadhatják őket, ezért a #ifdef __cpp_lib_format include nélkül félrevezető lehet.

33.5.4 Két független állapot: compiler és standard library

A labornaplóban a capability-t nem egyetlen verziószámmal kell azonosítani, hanem egy rekorddal, amelynek minden mezője külön forrásból származik:

Dimenzió Hol olvasható Mit bizonyít
nyelvi mód __cplusplus a frontend által elfogadott nyelvtan
nyelvi funkció __cpp_* makró, -dM -E kimenet a compiler állítása a funkcióról
könyvtár-változat _GLIBCXX_RELEASE (libstdc++), _LIBCPP_VERSION (libc++) melyik implementáció, melyik generáció
könyvtári funkció __cpp_lib_* a <version>-ből a library állítása a funkcióról
tényleges használhatóság compile probe, -Werror-rel a négyes együttes viselkedése

A _GLIBCXX_RELEASE és a _LIBCPP_VERSION olyan implementációs makrók, amelyeket a szabvány nem ír elő; használatuk diagnosztikára alkalmas, de nem hordozható feature-detection. Hasonlóan, a __VERSION__ a compiler saját leírója. Ezeket a naplóba írjuk, nem feltételként használjuk.

Kidolgozott példa. Az alábbi program egyetlen forrásból, egy adott (compiler, stdlib, -std) hármasra rögzíti a fenti mezőket. A SHOW makró trükkje: ha a makró definiált, az értéke stringesül, ha nem, akkor a neve jelenik meg a kimenetben — ez diagnosztikára elég, produkciós feature-detectionre nem.

// capability_snapshot.cpp
// Egy (compiler, stdlib, -std) hármas állapotának rögzítése.
// Illusztratív eszköz, nem helyettesíti a tényleges használati tesztet.
#include <cstdio>

#if defined(__has_include)
#  if __has_include(<version>)
#    include <version>
#    define HAVE_VERSION_HEADER 1
#  endif
#endif
#ifndef HAVE_VERSION_HEADER
#  define HAVE_VERSION_HEADER 0
#endif

#define STR_(x) #x
#define STR(x) STR_(x)
#define SHOW(m) std::printf("  %-24s %s\n", #m, STR(m))

int main()
{
    std::printf("== language ==\n");
    SHOW(__cplusplus);
    SHOW(__cpp_constexpr);
    SHOW(__cpp_consteval);
    SHOW(__cpp_constinit);
    SHOW(__cpp_concepts);
    SHOW(__cpp_modules);

    std::printf("== library (<version>: %s) ==\n",
                HAVE_VERSION_HEADER ? "found" : "missing");
#if HAVE_VERSION_HEADER
    SHOW(__cpp_lib_format);
    SHOW(__cpp_lib_print);
    SHOW(__cpp_lib_expected);
    SHOW(__cpp_lib_mdspan);
    SHOW(__cpp_lib_stacktrace);
#endif

    std::printf("== implementation ==\n");
#ifdef __VERSION__
    SHOW(__VERSION__);
#endif
#ifdef _GLIBCXX_RELEASE
    SHOW(_GLIBCXX_RELEASE);
#endif
#ifdef _LIBCPP_VERSION
    SHOW(_LIBCPP_VERSION);
#endif
    return 0;
}

Fordítás és futtatás a laborban, két lánccal, hogy az eltérés látszódjék:

g++ -std=c++20 -O2 -Wall -Wextra -pedantic \
    -o capability_snapshot_gcc capability_snapshot.cpp
./capability_snapshot_gcc

clang++ -std=c++20 -O2 -Wall -Wextra -pedantic -stdlib=libc++ \
    -o capability_snapshot_clang capability_snapshot.cpp
./capability_snapshot_clang

A kimenet nem mért eredmény ebben a szövegben: a konkrét sorok a telepített kiadásoktól függenek, és a tanulónak kell rögzítenie őket a saját labornaplójában. Amit a példa bizonyít, az a módszer: két külön blokk, két külön állításforrás, és a __cplusplus önmagában egyik blokkra sem elég.

33.5.5 Configure-time compile probe

A makróvizsgálat olcsó, de megengedő: egy definiált __cpp_lib_format nem garantálja, hogy a laborban használt pontos hívás lefordul. A CMake check_cxx_source_compiles a valódi próbát végzi el, és az eredményből konfigurációs makró lesz.

cmake_minimum_required(VERSION 3.20)
project(capability_probe LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

include(CheckCXXSourceCompiles)

check_cxx_source_compiles([[
#include <format>
#include <string>
int main() {
    std::string s = std::format("{}", 42);
    return s == "42" ? 0 : 1;
}
]] APP_HAS_STD_FORMAT)

check_cxx_source_compiles([[
#include <expected>
int main() {
    std::expected<int, int> e{7};
    return *e == 7 ? 0 : 1;
}
]] APP_HAS_STD_EXPECTED)

add_executable(capability_snapshot capability_snapshot.cpp)
if(APP_HAS_STD_FORMAT)
    target_compile_definitions(capability_snapshot PRIVATE APP_HAS_STD_FORMAT=1)
endif()
if(APP_HAS_STD_EXPECTED)
    target_compile_definitions(capability_snapshot PRIVATE APP_HAS_STD_EXPECTED=1)
endif()

A check_cxx_source_compiles a próbát a projekt C++ standardjával és a CMAKE_REQUIRED_FLAGS tartalmával futtatja; azt, hogy a standard-beállítás pontosan hogyan öröklődik a check-fordításba, a használt CMake-kiadás dokumentációja szerint kell ellenőrizni. A probe eredménye 0/1 konfigurációs makró, amelyet a forrás és a build script is lát — így a fallback ugyanazon a néven dől el, nem két helyen.

A capability-tábla a fenti kimenetből és a CMake cache-ből áll össze. Egy sor akkor tekinthető igazoltnak, ha a probe lefutott és a használt hívásminta lefordult; a makró csak előzetes szűrő.

A feldolgozandó funkciók közül a format, a print, az expected, az mdspan és a stacktrace tipikusan könyvtári oldalon jelenik meg, ezért a __cpp_lib_* és a probe a mérvadó. A modules ezzel szemben vegyes: nyelvi makró (__cpp_modules) és build-rendszer-támogatás kell hozzá; a modulok stabil, cross-compiler bináris formátumként való kezelése nem része ennek a fejezetnek, és a laborprofilban rögzített compiler/build párosítás nélkül a __cpp_modules definiáltsága keveset bizonyít C++20 modules és header unit (33.4). A print C++23-as státusza miatt a C++20-as fallback itt is kötelező: std::format + std::fputs charconv, format és C++23 print (32.2). Az mdspan kétdimenziós indexeléséhez a korábbi view-térkép adja a fogalmi hátteret Multidimenziós view és numeric utility térkép (31.5).

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

__cplusplus mint könyvtár-feltétel. A #if __cplusplus >= 202302L körül #include <expected> szerepel, majd a build elhasal egy olyan láncon, ahol a frontend már C++23 módban van, de a könyvtár generációja régebbi. Javítás: a nyelvi és a könyvtári feltételt külön sorba tenni, és a könyvtárit __has_include + __cpp_lib_* + probe hármasra bízni.

Feature-makró include nélkül. A #ifdef __cpp_lib_format a #include <version> (vagy <format>) előtt áll, ezért hamis negatívot ad, és a fallback ág fut le feleslegesen. Javítás: a makróvizsgálat mindig a header include után.

A makró értékének logikai kezelése. A #if __cpp_lib_format az értéket igaz/hamisnak veszi; valójában dátum, és egy adott évjáratot kell megcélozni: #if defined(__cpp_lib_format) && __cpp_lib_format >= 202110L. Az évjárat-számot a <version> és a fordítási lánc dokumentációja ellenőrzi, nem a tankönyvi emlékezet.

A __has_include rossz helyen. Nem használható futásidejű kódban, és nem lehet rá #if-en kívül hagyatkozni; továbbá a találat nem jelent funkcionális megfelelést.

A lánc azonosítatlan rögzítése. A build logból csak a -std= látszik, a compiler és a library nem. Javítás: a capability-snapshot kimenetét a build artefaktum mellé menteni, dátumozva.

Bizonyíték-korlát. Ez a fejezet nem nevez meg konkrét compiler- és library-kiadást, és nem állítja, hogy a fenti kód bármely láncon lefutott. A szabványos makrónevek és a -std értékek a szabványból származó ismeretek; a tényleges állapotot a tanuló által futtatott snapshot rögzíti.

33.5.7 Feladatok

1. feladat (magyarázó). Egy kolléga azt állítja, hogy „C++23-mal a std::expected biztosan elérhető, hiszen __cplusplus == 202302L”. Cáfold meg az állítást, és adj egy konkrét, elvi ellenpéldát olyan (compiler, standard library) párra, ahol a nyelvi mód C++23, a <expected> mégis hiányzik vagy hiányos.

2. feladat (kódolási). Írj app_capabilities.hpp fejlécet, amely APP_HAS_FORMAT makrót ad 1-re, ha a std::format használható, egyébként 0-ra. A döntés használja a __has_include(<format>)-ot és a __cpp_lib_format-ot, de a végső szót a build-rendszer által átadott APP_HAS_STD_FORMAT (probe-eredmény) mondja ki. Mellékelj egy rövid #if-ágat, amely a két esetben eltérő implementációt választ.

3. feladat (diagnosztikai). Adott egy build, amely clang++ -std=c++20 -stdlib=libstdc++ alatt lefordul, clang++ -std=c++20 -stdlib=libc++ alatt viszont a __cpp_lib_expected hiánya miatt más ágat vesz. Írd le, hogyan izolálod a három változót (compiler, library, nyelvi mód), mit rögzítesz a naplóban, és hogyan döntöd el, hogy ez valódi capability-különbség-e, vagy csak a fejléc-keresési útvonal hibája.

33.5.8 Megoldások

1. megoldás. A __cplusplus a frontend nyelvi módját kódolja; a <expected> elérhetőségét a standard library implementációja dönti el, amelyet a fordítótól függetlenül, külön csomagként telepítenek és verzióznak. Elvi ellenpélda: egy új frontend egy régebbi, de ABI-kompatibilis standard library-vel párosítva — a nyelvi makrók a C++23 értéket adják, a <version> viszont nem definiálja a __cpp_lib_expected-et, vagy a header egyáltalán nincs a keresési útvonalon. Ellenőrzési feltétel: az állítás akkor tekinthető cáfoltnak, ha a snapshot a nyelvi blokkban 202302L-t, a könyvtári blokkban definiálatlan __cpp_lib_expected-et mutat, és a <expected> próba-fordítása hibára fut. A konkrét verziókat a labor rögzíti; a fejezet nem nevez meg párost.

2. megoldás. A fejléc a probe-eredményt tekinti elsődlegesnek, a makrókat csak fallbackként:

// app_capabilities.hpp
#pragma once

#if defined(APP_HAS_STD_FORMAT)
#  define APP_HAS_FORMAT APP_HAS_STD_FORMAT
#else
#  if defined(__has_include)
#    if __has_include(<format>) && defined(__cpp_lib_format)
#      define APP_HAS_FORMAT 1
#    else
#      define APP_HAS_FORMAT 0
#    endif
#  else
#    define APP_HAS_FORMAT 0
#  endif
#endif

Használat:

#include "app_capabilities.hpp"
#include <string>

#if APP_HAS_FORMAT
#  include <format>
inline std::string tag(int v) { return std::format("[{}]", v); }
#else
#  include <cstdio>
inline std::string tag(int v) {
    char b[32];
    std::snprintf(b, sizeof b, "[%d]", v);
    return std::string{b};
}
#endif

Indoklás: a #if APP_HAS_FORMAT mindig numerikus, nem defined, ezért a 0-s fallback és a definiálatlan eset nem keveredik. Ellenőrzési feltétel: -std=c++20 -Werror mellett mindkét ág fordul, és APP_HAS_FORMAT értéke a CMake cache-ből visszaolvasható. A <format> viselkedését ez a fejezet nem méri.

3. megoldás. Először a compilert rögzítjük (--version és -v kimenet), majd a standard libraryt (_GLIBCXX_RELEASE vagy _LIBCPP_VERSION a snapshotból), végül a nyelvi módot (__cplusplus, -std). A három változóból egyszerre csak egyet mozgatunk: azonos compiler mellett -stdlib-ot váltunk, majd azonos library mellett compilert. Valódi capability-különbségről akkor beszélünk, ha a probe-fordítás mindkét láncon lefut, és az eredmény konzisztensen követi a library-változatot; ha viszont a __has_include(<expected>) hamis, miközben a fájl a fájlrendszeren létezik, akkor a keresési útvonal vagy az include-sorrend a hiba, nem a capability. Ellenőrzési feltétel: a napló tartalmazza mindhárom dimenzió értékét, a probe parancsát és annak exit-státuszát.

33.5.9 Összefoglaló

A capability nem azonos a verziószámmal. A __cplusplus és a __cpp_* makrók a compiler állításai a nyelvi oldalról; a <version>-ből olvasott __cpp_lib_* makrók a standard library állításai a könyvtári oldalról; a kettő külön telepített, külön verziózott komponens. A __has_include csak a header megtalálhatóságát jelzi, nem a szerződés teljesülését. A megbízható döntés három rétegből áll: makró-szűrő, configure-time compile probe, és a laborban rögzített tényleges használati teszt. Az újabb funkciók — format, print, expected, mdspan, stacktrace — esetében ez a hármas adja a példa és a fallback közötti választás alapját; a modules esetében ehhez build-rendszer-támogatás is kell, amit a korábbi rész tárgyalt C++20 modules és header unit (33.4).