A C záróprojekt funkcionális szerződését a C++ port nem értelmezi újra: ugyanazt a rekordnyelvtant, ugyanazokat a hibakódokat és hibapozíciókat, ugyanazt a CLI-kimenetet kell produkálnia, mint a 17.2 C referencia. A Portterv és azonos viselkedési szerződés (39.1) rögzítette a viselkedési határt, a migrációs naplót és a C ABI wrappert; ez a rész a C++ magot építi fel: az értéktípusokat, a parser állapotgépét, a platformoldali erőforrás-tulajdonosokat és a teszteket. A cél nem az, hogy minden új C++ eszközt beépítsünk, hanem hogy a szerződést a típusokba kódoljuk, és a hibalehetőségeket fordítási időbe emeljük.
A C++ port három szerepet különböztet meg. Az értéktípus (value type) önálló, invariánssal rendelkező érték: másolható vagy mozgatható, és minden megfigyelhető állapota kielégíti az invariánst. A tulajdonos (owner) erőforrást birtokol, és a destruktorában felszabadítja; a tulajdonjog mozgatható, de nem másolható, ha az erőforrás egyedi. A nézet (view) nem birtokol, hanem más objektum által birtokolt adatra mutat; élettartama nem haladhatja meg a mögöttes adatét.
A portban:
std::span<const unsigned char>
nézet, amelyet a parser csak a feed hívás
alatt olvas, és nem tárol el;diag::report, a diag::error és maga a
diag::parser értéktípus: nem birtokol
külső erőforrást, ezért szabadon másolható; egy másolat a bemenet
ugyanazon pontjáról, a többitől függetlenül folytatható;dg_parser * handle-je
tulajdonos: a dg_create a hívó
allokátorával hozza létre, a dg_destroy ugyanazzal
szabadítja fel;UniqueFd, MappedFile) move-only
tulajdonosok.A C változatban a tulajdonlás és az élettartam a dokumentációban élt; a C++ változatban a típusok mondják ki. A megfigyelhető viselkedés eközben bájtra azonos marad.
A C++ core felülete (include/diag_core.hpp) a portterv kidolgozott példájában szerepel. Az
értéktípusok a C struktúrák közvetlen megfelelői:
| C ABI | C++ core | Megjegyzés |
|---|---|---|
dg_report |
diag::report |
Azonos mezők: bytes, records,
sum |
dg_error |
diag::error |
kind és offset; a kind
hatókörös enum class |
dg_code |
diag::code |
Azonos numerikus értékek, static_assert ellenőrzi |
DG_MAX_INPUT |
diag::max_input |
inline constexpr, egy definíció a programban |
Az enum class nem konvertálódik implicit módon egésszé,
ezért a C értékre fordítás a wrapperben explicit
static_cast, és csak ott történik. Mivel minden
alapértelmezett tagkezdőérték nulla, egy alapállapotú
diag::parser pontosan a C dg_create utáni
állapotnak felel meg: üres jelentés, code::ok,
névfázis.
A diag::parser invariánsa ugyanaz, mint a C struktúráé:
a report_.bytes legfeljebb max_input; a
névhossz 0–16; a számjegyszám 0–5; a részérték legfeljebb 65535; és az
első hiba után az error_ nem változik. A privát adattagok
miatt ezt csak a tagfüggvények módosíthatják.
A core nem lát erőforrást; a C++ oldali platformadapterben (például
egy C++ CLI-ben vagy a TCP-adapter portjában) viszont a file descriptor
és a memóriatérkép a két tipikus egyedi erőforrás. Mindkettő move-only
owner: a másolás tiltott, a mozgatás noexcept, a destruktor
felszabadít. A UniqueFd a close-ot, a
MappedFile a munmap-ot hívja.
// include/diag_io.hpp (részlet)
#pragma once
#include <cerrno>
#include <cstddef>
#include <span>
#include <system_error>
#include <utility>
#include <sys/mman.h>
#include <unistd.h>
class UniqueFd {
public:
explicit UniqueFd(int fd = -1) noexcept : fd_(fd) {}
~UniqueFd() { if (fd_ >= 0) ::close(fd_); }
UniqueFd(UniqueFd&& o) noexcept : fd_(std::exchange(o.fd_, -1)) {}
UniqueFd& operator=(UniqueFd&& o) noexcept {
if (this != &o) { reset(); fd_ = std::exchange(o.fd_, -1); }
return *this;
}
UniqueFd(const UniqueFd&) = delete;
UniqueFd& operator=(const UniqueFd&) = delete;
int get() const noexcept { return fd_; }
int release() noexcept { return std::exchange(fd_, -1); }
private:
void reset() noexcept { if (fd_ >= 0) { ::close(fd_); fd_ = -1; } }
int fd_;
};
class MappedFile {
public:
MappedFile(int fd, std::size_t length, off_t offset = 0)
: length_(length) {
addr_ = ::mmap(nullptr, length, PROT_READ, MAP_PRIVATE, fd, offset);
if (addr_ == MAP_FAILED) {
addr_ = nullptr;
throw std::system_error(errno, std::generic_category(), "mmap");
}
}
~MappedFile() { reset(); }
MappedFile(MappedFile&& o) noexcept
: addr_(std::exchange(o.addr_, nullptr)),
length_(std::exchange(o.length_, 0)) {}
MappedFile& operator=(MappedFile&& o) noexcept {
if (this != &o) { reset(); addr_ = std::exchange(o.addr_, nullptr);
length_ = std::exchange(o.length_, 0); }
return *this;
}
MappedFile(const MappedFile&) = delete;
MappedFile& operator=(const MappedFile&) = delete;
std::span<const std::byte> bytes() const noexcept {
return {static_cast<const std::byte*>(addr_), length_};
}
private:
void reset() noexcept {
if (addr_ != nullptr) { ::munmap(addr_, length_); addr_ = nullptr; length_ = 0; }
}
void* addr_ = nullptr;
std::size_t length_ = 0;
};A bytes() nézet a mapping élettartamáig érvényes. A
MappedFile mozgatása nem invalidálja a nézetet, mert a
címzett cím nem változik, és a mozgatott-from objektum már nem szabadít
fel; a nézet akkor válik érvénytelenné, amikor az új tulajdonos
megszűnik. Ezt a szerződést a header dokumentációjában rögzíteni kell. A
MappedFile konstruktora std::system_error-t
dobhat; ez a ritka, nem várt erőforrás-hiba, nem a parser várható
hibája.
A megvalósítás a C dg_feed és dg_finish
állapotgépének soronkénti megfelelője. A C++ eszközök csak ott jelennek
meg, ahol a szerződést pontosabban fejezik ki: tartományalapú
for a span fölött, enum class a fázisra,
aggregátum-inicializálás a hibához.
// src/diag_core.cpp
#include "diag_core.hpp"
namespace diag {
bool parser::fail(code kind) noexcept
{
if (error_.kind == code::ok)
error_ = error{kind, report_.bytes};
return false;
}
bool parser::feed(std::span<const unsigned char> input) noexcept
{
if (error_.kind != code::ok)
return false;
if (finished_)
return fail(code::state);
for (const unsigned char byte : input) {
if (report_.bytes == max_input)
return fail(code::too_large);
if (phase_ == phase::key) {
if (byte >= 'a' && byte <= 'z') {
if (key_length_ == 16)
return fail(code::syntax);
++key_length_;
} else if (byte == '=' && key_length_ != 0) {
phase_ = phase::number;
digits_ = 0;
value_ = 0;
} else {
return fail(code::syntax);
}
} else {
if (byte >= '0' && byte <= '9') {
const unsigned long digit = byte - '0';
if (digits_ == 5 || value_ > (65535UL - digit) / 10UL)
return fail(code::syntax);
value_ = value_ * 10UL + digit;
++digits_;
} else if (byte == '\n' && digits_ != 0) {
++report_.records;
report_.sum += value_;
phase_ = phase::key;
key_length_ = 0;
} else {
return fail(code::syntax);
}
}
++report_.bytes;
}
return true;
}
bool parser::finish() noexcept
{
if (error_.kind != code::ok)
return false;
if (phase_ != phase::key || key_length_ != 0)
return fail(code::truncated);
finished_ = true;
return true;
}
} // namespace diagA bájtosztályokat explicit összehasonlítás dönti el, nem
std::islower vagy std::isdigit: azok
locale-függők lehetnek, a nyelvtan viszont rögzített ASCII-oktetteket ír
elő. A túlcsordulásellenőrzés a szorzás előtt fut,
ugyanazzal a
feltétellel, mint a C változatban.
A hibapozíció a teljes munkamenet elejéhez viszonyított bájtoffset: a
report_.bytes értéke a hiba pillanatában. Mivel a számláló
csak sikeresen feldolgozott bájt után nő, ez éppen az első elutasított
bájt helye; csonkolásnál a bemenet vége; limitnél pontosan
max_input. A darabolás nem számít: több feed
hívás offsetjei összeadódnak, nem szeletenként indulnak nulláról.
A port minden ága a C referencia egy ágának felel meg, azonos
feltétellel és azonos állapotváltozással; a vezérlés bájtonként halad,
és a hívások határán nincs implicit lezárás. Indukcióval a feldolgozott
bájtok számára: ha a két implementáció állapota
bájt után megegyezik (fázis, névhossz, számjegyszám, részérték,
jelentés, hiba), akkor a
-edik
bájt után is, mert ugyanaz a döntés születik. A finish
ugyanazt a feltételt vizsgálja. Ezért a port és a C referencia minden
bemenetre és minden darabolásra azonos jelentést és hibát ad. A vázlat
feltételezi, hogy a span bájtjai a hívás idején érvényesek.
A vázlat a C referenciához viszonyított egyezést
igazolja. A specifikációhoz viszonyított helyességet a
projektszerződés nyelvtanából levezetett
tests/spec_examples.c és a 17.2 bizonyításvázlatai
támasztják alá; a kettő együtt adja a port elfogadását.
A C referencia egyetlen, rögzített méretű parserobjektummal dolgozik,
és nem tárol rekordokat. A port ezt megtartja: a core-ban nincs
std::vector, std::string vagy más dinamikus
tároló, és nincs new. Ha egy rekordokat gyűjtő tárolót
vezetnénk be, az megfigyelhető változás lenne a memóriakorlátban, és
csak külön migrációként jöhetne szóba.
Az allokátorszerződést a wrapper őrzi meg. A dg_create a
hívó dg_allocator callbackjével foglal tárolót, abba
placement new-val hozza létre a dg_parser objektumot, a
dg_destroy pedig meghívja a destruktort, majd ugyanazzal az
allokátorral szabadít fel. A C tesztkészlet allokációs hibát injektáló
esete ezért a C++ könyvtárral is ugyanúgy fut.
Egy std::pmr::memory_resource adapter, amely a
dg_allocator callbackjeit hívja, kézenfekvő kiterjesztés
lenne, ha a core-ba később valódi dinamikus tároló kerülne. A jelenlegi,
memóriájú parsernél nincs mit az arena alá helyezni.
A port alapprofilja: a várható hibák — szintaktikai
hiba, csonkolás, limit, API-használati hiba — értékként utaznak: a
feed és a finish bool eredményt
ad, a részleteket a last_error() adja. Ez a C API
egy-az-egyben leképezhető alakja, ezért nincs szükség
std::variant-ra vagy C++23
std::expected-re.
A core minden művelete noexcept, és nem allokál; így
kivétel nem keletkezhet benne. Az egyetlen erőforrás-hiba a létrehozás:
ezt a C API NULL visszatérése jelzi, ahogyan a C
változatban is. A platformadapter UniqueFd és
MappedFile típusai std::system_error-t
dobhatnak; ezek a C ABI határán kívül élnek, és a határon belépő
függvénynek el kell kapnia őket, mielőtt hibakóddá alakítja.
A C interfész a változatlan include/diag.h; a C++ oldali
definíciókat a portterv
src/diag_c_api.cpp wrappere adja. Új C header nem kell, és
nem is szabad: a C kliensek, köztük a src/main.c és a C
tesztek, forrásszinten változatlanok maradnak.
A C++ core saját tesztje közvetlenül a diag::parser
osztályt hívja, C ABI nélkül. Az ellenőrzések NDEBUG
mellett sem tűnnek el, mert nem assert-re épülnek:
// tests/test_core.cpp -- a C++ core közvetlen tesztje
#include "diag_core.hpp"
#include <cstdio>
#include <cstdlib>
#include <span>
#include <string_view>
#define CHECK(condition) do { \
if (!(condition)) { \
std::fprintf(stderr, "CHECK failed at line %d\n", __LINE__); \
std::exit(EXIT_FAILURE); \
} \
} while (0)
static std::span<const unsigned char> bytes(std::string_view s)
{
return {reinterpret_cast<const unsigned char *>(s.data()), s.size()};
}
int main()
{
const auto input = bytes("a=7\ncpu=12\n");
for (std::size_t split = 0; split <= input.size(); ++split) {
diag::parser p;
CHECK(p.feed(input.first(split)));
CHECK(p.feed(input.subspan(split)));
CHECK(p.finish());
const diag::report r = p.current();
CHECK(r.records == 2 && r.sum == 19 && r.bytes == 11);
}
diag::parser truncated;
CHECK(truncated.feed(bytes("a=7\ncpu=12")));
CHECK(!truncated.finish());
CHECK(truncated.last_error().kind == diag::code::truncated);
CHECK(truncated.last_error().offset == 10);
diag::parser bad;
CHECK(!bad.feed(bytes("a=65536\n")));
CHECK(bad.last_error().kind == diag::code::syntax);
CHECK(bad.last_error().offset == 6);
unsigned char reused[] = {'a', '=', '7', '\n'};
diag::parser r;
CHECK(r.feed(std::span<const unsigned char>(reused, 2)));
reused[0] = reused[1] = 'X'; // a hívó újrahasználja a puffert
CHECK(r.feed(std::span<const unsigned char>(reused + 2, 2)));
CHECK(r.finish() && r.current().sum == 7 && r.current().bytes == 4);
return 0;
}mkdir -p build/cxx
g++ -std=c++20 -O2 -g -Wall -Wextra -Wpedantic -Wconversion -Wshadow \
-Iinclude src/diag_core.cpp tests/test_core.cpp \
-o build/cxx/test_core
build/cxx/test_coreA C ABI-n keresztüli ellenőrzést a C referencia saját tesztjei végzik, a C++ könyvtárhoz linkelve; a buildparancsokat és a differenciális tesztet a portterv adja.
Buildjegyzőkönyv (lab-gcc profil). A
diag_core.cpp, a diag_c_api.cpp és a
test_core.cpp G++ 13.3.0-val,
-std=c++20 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Werror
beállítással figyelmeztetés nélkül fordult Linux x86-64 környezetben; a
test_core sikeresen lefutott. Az RAII-szakasz
UniqueFd és MappedFile részlete nem része
ennek a buildnek.
A leggyakoribb hiba a span megőrzése: a parser a feed
után nem tárolhat a bemenetre mutató nézetet, mert a hívó a puffert
újrahasználja; a félbemaradt rekordhoz elég a fázis, a névhossz, a
számjegyszám és a részérték. A második: a bájtosztályok cseréje
std::isalpha/std::isdigit hívásra, ami
locale-függővé teszi a nyelvtant. A harmadik: a túlcsordulásellenőrzés
áthelyezése a szorzás utánra, különösen szűkebb tárolótípussal; ilyenkor
az érték csendben körbefordul. A negyedik: a diag::code
értékeinek átrendezése; a static_assert ezt fordításkor
megfogja, ezért nem szabad eltávolítani. Az ötödik: a
MappedFile bytes() nézetének használata a
MappedFile megszűnése után; a nézet nem tartja életben a
mappinget.
A C++ core a 17.2 C referencia állapotgépének hű portja. A bemenet nem tulajdonló span, a jelentés, a hiba és maga a parser értéktípus, a C handle és a platformerőforrások tulajdonosok. A core nem allokál és nem dob, ezért a C ABI wrapper kivétel- és allokátorszerződése egyszerű. A C header és a C kliensek változatlanok; a port helyességét a C referencia saját tesztjei, a specifikációs példák és a differenciális korpusz együtt igazolják.
Magyarázó feladat. Nevezd meg a portban a
nézetet, az értéktípusokat és a tulajdonosokat! Miért lehet a
diag::parser másolható, miközben a UniqueFd
nem? Mit jelent egy rekord közepén készített másolat?
Kódolási feladat. Írj C++ tesztet, amely a
"a=7\ncpu=12\n" bemenetet bájtonként táplálja be, a hatodik
bájt előtt másolatot készít a parserről, majd a másolatot a maradék
bemenettel külön lezárja. Ellenőrizd, hogy a két parser jelentése
azonos.
Diagnosztikai feladat. Egy kolléga a részértéket
std::uint16_t típusban tárolja, és az ellenőrzést a szorzás
utánra helyezi:
value_ = static_cast<std::uint16_t>(value_ * 10u + digit);
if (value_ > 65535u)
return fail(code::syntax);Mit ír ki a C++ CLI a "a=65536\n" bemenetre, és mit a C
referencia? Melyik ellenőrzés fogja meg a hibát?
MappedFile bytes() nézetét eltárolod egy
std::span-ban, majd a MappedFile-ot mozgatod
egy másik objektumba. Érvényes marad-e a nézet? Fogalmazd meg a
szerződést, és egészítsd ki a MappedFile dokumentációját
egy bekezdéssel.A std::span<const unsigned char> a nézet: nem
birtokol, és csak a feed hívás idejéig érvényes. A
diag::report, a diag::error és a
diag::parser értéktípus: csak skalárokat tartalmaz, külső
erőforrást nem birtokol, ezért a másolás teljes, független állapotot hoz
létre. A dg_parser * handle és a UniqueFd
tulajdonos; a UniqueFd másolása két tulajdonost teremtene
ugyanarra a file descriptorra, ami dupla close-hoz vezetne,
ezért tiltott. A rekord közepén készített másolat a félbemaradt rekord
teljes állapotát (fázis, névhossz, számjegyszám, részérték) hordozza,
így bármelyik példány ugyanonnan folytatható. Ellenőrzési feltétel: a
magyarázat a három szerepet és a másolhatóság okát is
megnevezi.
// tests/test_bytewise.cpp -- bájtonkénti betáplálás és értékszemantika
#include "diag_core.hpp"
#include <cstdio>
#include <cstdlib>
#include <span>
#include <string_view>
#define CHECK(condition) do { \
if (!(condition)) { \
std::fprintf(stderr, "CHECK failed at line %d\n", __LINE__); \
std::exit(EXIT_FAILURE); \
} \
} while (0)
int main()
{
constexpr std::string_view text = "a=7\ncpu=12\n";
const std::span<const unsigned char> input{
reinterpret_cast<const unsigned char *>(text.data()), text.size()};
diag::parser p;
diag::parser snapshot;
for (std::size_t i = 0; i < input.size(); ++i) {
if (i == 6)
snapshot = p; // másolat a rekord közepén
CHECK(p.feed(input.subspan(i, 1)));
}
CHECK(p.finish());
CHECK(p.current().records == 2 && p.current().sum == 19);
// A másolat független: ugyanonnan folytatva ugyanoda jut.
CHECK(snapshot.current().bytes == 6);
CHECK(snapshot.feed(input.subspan(6)));
CHECK(snapshot.finish());
CHECK(snapshot.current().records == p.current().records);
CHECK(snapshot.current().sum == p.current().sum);
CHECK(snapshot.current().bytes == p.current().bytes);
return 0;
}A hatodik bájt előtt a parser 6 bájtot fogyasztott el
(a=7\ncp), és a második rekord névfázisában áll. A teszt
G++ 13.3.0-val, -Werror mellett fordult és sikeresen
lefutott. Ellenőrzési feltétel: a két jelentés mindhárom mezője
megegyezik.
A std::uint16_t legfeljebb 65535-öt tárol, ezért a
6553 után a 6 számjegynél a
érték 0-ra fordul körbe, és a value_ > 65535u feltétel
soha nem teljesül. A C++ CLI kimenete
OK records=1 sum=0 bytes=8, kilépési kódja 0; a C
referenciáé ERR code=syntax offset=6, kilépési kódja 4. A
hibát a differenciális teszt a korpusz a=65536 elemén
megfogja (stdout, stderr és kilépési kód is eltér), a specifikációs
példák "a=65536\n" sora szintén, és a GCC
-Wextra alatt -Wtype-limits figyelmeztetést ad
a mindig hamis összehasonlításra. A javítás a C referencia sorrendje: a
tartományellenőrzés a szorzás előtt, elég széles típusban. Ellenőrzési
feltétel: a hibás változat a fenti kimenetet adja, a javított a C
referenciáéval azonosat. A hibás változatot GCC 13.3.0-val ténylegesen
lefordítottuk és lefuttattuk; a fenti kimenetek ebből
származnak.
A nézet érvényes marad, feltéve, hogy a mozgatott-from
MappedFile nem szabadítja fel a mappinget (a move után a
addr_ nullptr), és az új tulajdonos életben marad. A
std::span csak a címet és a hosszt tárolja, nem az owner
identitását, ezért a mozgatás nem változtatja meg a nézet által látott
memóriát. A szerződés: a bytes() nézet a mapping
élettartamáig érvényes; a mapping akkor szűnik meg, amikor az őt
birtokló MappedFile (a mozgatás után az új példány)
megszűnik, vagy explicit módon felszabadítják. A dokumentáció
kiegészítése: „A bytes() által visszaadott span a mapping
élettartamához kötött; a MappedFile mozgatása nem
invalidálja, a tulajdonos megszűnése igen.” Ellenőrzési feltétel: a
válasz különbséget tesz a owner objektum identitása és a mapping
élettartama között.