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.
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.
__cplusplus és
a __cpp_* nyelvi makrókA __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 */
#endifEz 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.
<version>
és a __cpp_lib_* makrókA 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
#endifA __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.
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_clangA 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.
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).
__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.
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.
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
#endifHaszná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};
}
#endifIndoklá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.
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).