Ez a labor a fejezet elveit fordítja át ellenőrizhető gyakorlatokká.
Két típust építünk fel, mindkettőnél ugyanazzal a három lépéses
módszerrel: előbb szándékosan rossz változatot adunk, azt
interfészreview alá vetjük, majd a javítást fordítási és futásidejű
feltételekkel igazoljuk. A két típus két tipikus hibatípust fed le: az
fd_handle egy erőforrás-burkot
(ownership), a byte_count egy erős domain
értéket (konverziók és operátorok).
A laborkörnyezet: ISO C++20, explicit -std=c++20,
rögzített GCC- vagy Clang-kiadás, x86-64 System V, Linux. Az
fd_handle a POSIX <unistd.h>
::close/::read/::socket
függvényeit használja; más platformon ezt a megfelelő primitívre kell
cserélni. Ajánlott fordítás:
g++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -O2 \
-fsanitize=address,undefined lab.cpp -o labA példák viselkedését a leírás a C++20 szabályai alapján adja meg; a tényleges lefuttatás a tanuló laborjának feladata, a jegyzet nem tartalmaz mérési eredményt.
Az alábbi definíciók adják a labor ellenőrzőlistáját.
Erős domain típus (strong domain type): olyan
osztály, amelynek reprezentációja és megengedett műveletei egy fogalmat
írnak le, nem pedig egy általános gépi típust (például
std::size_t), és amelynek nincsenek olyan implicit
konverziói, amelyek fogalomhatárt lépnek át.
Kölcsönzött accessor (borrowed accessor): olyan tagfüggvény, amely a belső erőforrás nem tulajdonló handle-jét adja vissza. A visszaadott értéket a hívó nem zárhatja le, nem szabadíthatja fel, és nem tárolhatja a burkoló élettartamán túl.
Baleseti konverzió (accidental conversion): implicit
átalakítás, amely egy értéket más jelentésű típusba visz anélkül, hogy a
forráskódban bármi jelezné a szándékot. Ellentéte a szándékos
konverzió, amely nevesített művelet: bc.value(),
static_cast<int>(x), explicit konstruktorhívás.
A review négy kérdése minden publikus tagra: (1) átad-e
tulajdonjogot, és ha igen, kiderül-e a szignatúrából; (2) enged-e
implicit konverziót, és az melyik irányba visz; (3)
noexcept-e ott, ahol a standard tároló erre épít; (4)
konzisztens-e az egyenlőség, a rendezés és a hash.
A rossz változat mindössze annyit tesz, hogy a destruktorban lezárja az fd-t. A hiba nem a destruktorban van, hanem abban, amit a fordító helyette legenerál.
// BAD — tanítási ellenpélda, nem termelési kód.
#include <unistd.h>
class fd_handle_bad {
public:
explicit fd_handle_bad(int fd) : fd_(fd) {}
~fd_handle_bad() { if (fd_ >= 0) ::close(fd_); }
int get() const { return fd_; } // bárki lezárhatja
// nincs másolás/mozgatás deklarálva:
// a fordító mindkettőt hallgatólagosan legenerálja
private:
int fd_ = -1;
};
void demo_bad(int raw_fd) {
fd_handle_bad h1{raw_fd};
fd_handle_bad h2 = h1; // hallgatólagos másolás: ugyanaz az fd
} // h2 és h1 destruktora is lezárja ugyanaztHárom külön hiba:
::close. A második hívás legalább
EBADF, de több szál esetén a szám újra kiosztható, és egy
másik szál élő fájlját zárja le — ez már nem EBADF, hanem
csendes adatvesztés.get() nem kölcsönzés, hanem nyitott
ajtó. A hívó ::close(h1.get())-et írhat, majd a
destruktor újra lezár.std::vector<fd_handle_bad> feltöltése másolással jár,
mert a fordító a másoló konstruktort választja, ha nincs
noexcept mozgató.A javított változat szándékosan mindent kikényszerít, amit a rossz engedett:
#include <unistd.h>
#include <utility>
class fd_handle {
public:
fd_handle() noexcept = default;
explicit fd_handle(int fd) noexcept : fd_(fd) {}
~fd_handle() { reset(); }
fd_handle(const fd_handle&) = delete; // egy owner
fd_handle& operator=(const fd_handle&) = delete;
fd_handle(fd_handle&& o) noexcept : fd_(o.release()) {}
fd_handle& operator=(fd_handle&& o) noexcept {
if (this != &o) reset(o.release()); // önmozgatás ellen véd
return *this;
}
int get() const noexcept { return fd_; } // kölcsönzés
explicit operator bool() const noexcept { return fd_ >= 0; }
int release() noexcept { // tulajdonjog átadása
int tmp = fd_; fd_ = -1; return tmp;
}
void reset(int fd = -1) noexcept { // hibaágak egy helyen
if (fd_ >= 0) ::close(fd_);
fd_ = fd;
}
void swap(fd_handle& o) noexcept { std::swap(fd_, o.fd_); }
private:
int fd_ = -1;
};
inline void swap(fd_handle& a, fd_handle& b) noexcept { a.swap(b); }A szerződés táblázatban:
| Művelet | Szerződés | Indok |
|---|---|---|
fd_handle(int) |
explicit, átveszi a tulajdonjogot | a nyers fd ne maradjon a hívónál |
~fd_handle() |
lezár, ha nem üres | RAII |
| másoló műveletek | = delete |
két owner → dupla lezárás |
| mozgató műveletek | noexcept, forrás üres lesz |
konténer-átrakás másolás nélkül |
get() |
kölcsönzés, nem zárható le | a POSIX API nyers int-et vár |
release() |
tulajdonjog a hívóhoz kerül | nem RAII határ, pl. dup2 |
reset(int) |
lezárja a régit, átveszi az újat | egyetlen hibakezelési pont |
swap() |
noexcept |
std::swap és konténerbarát |
A fordító által igazolható feltételek:
#include <type_traits>
static_assert(!std::is_copy_constructible_v<fd_handle>);
static_assert(!std::is_copy_assignable_v<fd_handle>);
static_assert( std::is_nothrow_move_constructible_v<fd_handle>);
static_assert( std::is_nothrow_move_assignable_v<fd_handle>);
static_assert( std::is_nothrow_destructible_v<fd_handle>);
static_assert(!std::is_convertible_v<fd_handle, int>); // nincs baleseti konverzióFutásidejű feltétel (a <sys/socket.h>-t is
include-olva):
fd_handle a{::socket(AF_UNIX, SOCK_STREAM, 0)};
fd_handle b{std::move(a)};
assert(b.get() >= 0);
assert(a.get() == -1); // a forrás üres: a destruktor nem zárja le újraA rossz verzió két irányban is nyit: nyers számból lesz
byte_count, és byte_count-ból nyers szám.
// BAD — tanítási ellenpélda.
#include <cstddef>
struct byte_count_bad {
byte_count_bad(std::size_t v) : value(v) {} // implicit be
operator std::size_t() const { return value; } // implicit ki
operator std::ptrdiff_t() const { // implicit ki
return static_cast<std::ptrdiff_t>(value);
}
std::size_t value;
};Két következmény, mindkettő a labor egy-egy tesztje.
Baleseti konverzió. Mivel a konstruktor implicit,
egy nyers int átcsúszik egy másik jelentésű függvénybe:
void set_retry_count(byte_count_bad);
int n = 5;
set_retry_count(n); // int -> std::size_t -> byte_count_bad: lefordulAz implicit konverziós sorozat szabálya ezt megengedi (standard + egy felhasználói + standard lépés), a forráskódban mégsem látszik, hogy darabszám-fogalomba nyers egész lépett be.
Overload-ambiguitás. A két konverziós operátor két egyenrangú jelöltet teremt:
void schedule(std::size_t);
void schedule(std::ptrdiff_t);
schedule(byte_count_bad{1024}); // mindkettő felhasználói konverzió: ambiguusA javított típus csak a szándékos alakot engedi:
#include <compare>
#include <cstddef>
#include <functional>
class byte_count {
public:
explicit constexpr byte_count(std::size_t v = 0) noexcept : v_(v) {}
constexpr std::size_t value() const noexcept { return v_; } // szándékos
friend constexpr byte_count operator+(byte_count a, byte_count b) noexcept {
return byte_count{a.v_ + b.v_};
}
friend bool operator==(byte_count a, byte_count b) = default;
friend auto operator<=>(byte_count a, byte_count b) = default;
private:
std::size_t v_;
};A value() szándékosan nem konverziós operátor: a hívás
helye a forráskódban látszik, így a review elkapja, ha valaki nyers
méretet kever a domain típussal. Ugyanezt kényszeríti ki a fordító:
static_assert(!std::is_convertible_v<int, byte_count>); // nincs be
static_assert(!std::is_convertible_v<byte_count, std::size_t>); // nincs ki
static_assert( std::is_constructible_v<byte_count, std::size_t>); // explicit igenA gyakorlatban ez azt jelenti, hogy a schedule(1024) a
std::size_t túlterhelést választja (nincs rejtett
byte_count), a schedule(byte_count{1024})
pedig pontos illeszkedés — nincs ambiguitás, mert csak egy jelölt
életképes.
Az Egyenlőség, spaceship és rendezési kategóriák
(23.4) alfejezetben rögzített szerződés itt is kötelező: két egyenlő
értéknek ekvivalensnek kell lennie a rendezésben, és azonos hash-t kell
adnia, különben a
std::unordered_set/unordered_map elveszíti a
beszúrt elemet.
template <> struct std::hash<byte_count> {
std::size_t operator()(const byte_count& b) const noexcept {
return std::hash<std::size_t>{}(b.value());
}
};A = default comparisons a v_ tagra bontják
a műveletet, ezért az ==, a <=> és a
fenti hash automatikusan összhangban marad, amíg a hash ugyanazt az
egyetlen tagot használja. Ellenőrzés:
#include <cassert>
#include <unordered_set>
byte_count a{1024}, b{1024};
assert(a == b);
assert(!(a < b) && !(b < a)); // ekvivalens
assert(std::hash<byte_count>{}(a) == std::hash<byte_count>{}(b));
std::unordered_set<byte_count> s{a};
assert(s.contains(b));Ha valaha második tag kerül az osztályba (például mértékegység), a
= default comparisons azt is bevonják, de a hash
specializációt kézzel kell frissíteni — ezt a
review-nak külön ki kell kérnie.
Az ownership-szerződést a legkönnyebbinstrumentált típussal látni. A nyomkövető minden életeseményt kiír; nem a formázás a lényeg, hanem hogy a műveletek sorrendje megfigyelhető legyen.
#include <cstdio>
struct tracked {
int id;
explicit tracked(int i) : id(i) { std::printf("ctor %d\n", id); }
tracked(const tracked& o) : id(o.id) { std::printf("copy %d\n", id); }
tracked(tracked&& o) noexcept : id(o.id) { o.id = -1; std::printf("move %d\n", id); }
tracked& operator=(const tracked& o) { id = o.id; std::printf("copy= %d\n", id); return *this; }
tracked& operator=(tracked&& o) noexcept { id = o.id; o.id = -1; std::printf("move= %d\n", id); return *this; }
~tracked() { std::printf("dtor %d\n", id); }
};Két megfigyelendő pont: a mozgatott objektum Forrása -1
azonosítójú lesz (a forrás „üres”), és hogy a
std::vector<tracked> újrafoglalásakor a mozgatás
történik, nem másolás — utóbbi feltétele a noexcept,
swap és container-reallocation (22.5) alfejezetben tárgyalt
noexcept mozgató konstruktor. Ugyanezt a sémát
fd_handle-re alkalmazva a printf helyett a
reset/release hívások számlálhatók.
Az alábbi blokk szándékosan nem fordul le; nem javítandó, hanem a hibaüzenet olvasását gyakorolja.
// NEGATÍV FORDÍTÁSI PÉLDA — nem fordul le, ez a cél.
#include <cstddef>
struct byte_count_bad {
byte_count_bad(std::size_t v) : value(v) {}
operator std::size_t() const { return value; }
operator std::ptrdiff_t() const { return static_cast<std::ptrdiff_t>(value); }
std::size_t value;
};
void schedule(std::size_t);
void schedule(std::ptrdiff_t);
void demo() { schedule(byte_count_bad{1024}); }A várt diagnosztika jellege: a schedule(byte_count_bad)
hívás ambiguus, mert mindkét jelölt azonos rangú
felhasználói konverziót igényel. A javítás nem az overload-halmaz
átírása, hanem a típus szűkítése: implicit konverziós operátor nélkül a
hívás egyértelműen nem fordul le, ami itt a helyes eredmény — a
programozó kénytelen kiírni a szándékát.
= delete
a másoló műveletekre, noexcept mozgatás.get() helyett release()-et
használni mindenhol. A release() tulajdonjogot ad
át, a hívó felelőssége lesz a lezárás — ez csak valódi nem-RAII határon
indokolt.byte_count implicit konverziós
operátorral. Egy „kényelmi” operator std::size_t()
visszahozza az összes fenti hibát. Javítás: value().std::unordered_set a ==-t és a hash-t együtt
használja; eltérés esetén az elem „eltűnik”.h = std::move(h) a reset(o.release()) sorrend
miatt lezárhatja a saját fd-t. Javítás:
if (this != &o).1. feladat (könnyű). Add hozzá az
fd_handle-hez egy borrowed() nevű
const noexcept tagot, amely ugyanazt adja vissza, mint a
get(), de a neve is jelzi a szerződést. Igazold
static_assert-tel, hogy a fd_handle továbbra
sem konvertálható int-té, és hogy mozgatása
noexcept.
2. feladat (közepes). Egészítsd ki a
byte_count-ot operator+=-vel, amely
csak byte_count-ot fogad el. Mutasd meg
egy külön, jelölt negatív példában, hogy bc += 1 nem fordul
le, majd egy működő példában, hogy bc += byte_count{1}
igen. Indokold, miért nem elég a value()-t használni a
belső művelethez.
3. feladat (diagnosztikai). Készíts egy
checked_byte_count változatot, amely
operator+-ban std::size_t-túlcsordulást
ellenőriz (a.value() > SIZE_MAX - b.value()), és
ilyenkor std::overflow_error-t dob. Írj hozzá három esetet:
normál összeadás, pontosan SIZE_MAX, és túlcsordulás.
Egészítsd ki egy review-mondattal arról, hogy a dobó művelet milyen
exception guarantee-t ad.
1. megoldás. A borrowed() a
get() átnevezett változata; a szerződés nem változik, csak
a hívási hely lesz önmagyarázó.
class fd_handle {
public:
// ...
int get() const noexcept { return fd_; }
int borrowed() const noexcept { return fd_; } // ugyanaz, beszélő névvel
};
static_assert(!std::is_convertible_v<fd_handle, int>);
static_assert(std::is_nothrow_move_constructible_v<fd_handle>);Ellenőrzési feltétel: a két accessor bitre azonos értéket ad, és a
static_assert-ek változtatás nélkül átmennek. Megjegyzés:
ha get()-et megtartod, a két név egyike felesleges —
termelési kódban egyiket töröld, és a hívási helyeket igazítsd.
2. megoldás. Az operator+= szándékosan
tagfüggvény, hogy a bal oldal típusa adott legyen:
class byte_count {
public:
// ...
byte_count& operator+=(byte_count o) noexcept { v_ += o.v_; return *this; }
};Negatív példa (nem fordul le, mert nincs implicit int →
byte_count):
// NEGATÍV — a byte_count explicit konstruktora miatt nem fordul le.
byte_count bc{1024};
bc += 1; // hiba: nincs implicit konverzióMűködő példa:
byte_count bc{1024};
bc += byte_count{1}; // helyes: a szándék látszik
assert(bc == byte_count{1025});Indoklás: ha a belső műveletet v_ += o.value()-ként
írnánk, az csak a reprezentáción belül azonos; a különbség abban van,
hogy az operator+= a publikus felületen
kényszeríti ki a típust, nem a belső kódban. A value()
használata a hívónál megengedné a nyers szám keverését, amit épp ki
akarunk zárni.
3. megoldás. Az ellenőrzött összeadás:
#include <cstddef>
#include <limits>
#include <stdexcept>
class checked_byte_count {
public:
explicit constexpr checked_byte_count(std::size_t v = 0) noexcept : v_(v) {}
constexpr std::size_t value() const noexcept { return v_; }
friend checked_byte_count operator+(checked_byte_count a, checked_byte_count b) {
if (a.v_ > std::numeric_limits<std::size_t>::max() - b.v_)
throw std::overflow_error{"byte_count overflow"};
return checked_byte_count{a.v_ + b.v_};
}
friend bool operator==(checked_byte_count, checked_byte_count) = default;
friend auto operator<=>(checked_byte_count, checked_byte_count) = default;
private:
std::size_t v_;
};Tesztesetek:
assert((checked_byte_count{1} + checked_byte_count{2}) == checked_byte_count{3});
constexpr auto m = std::numeric_limits<std::size_t>::max();
assert((checked_byte_count{m - 1} + checked_byte_count{1}).value() == m); // pontosan MAX
bool threw = false;
try { (void)(checked_byte_count{m} + checked_byte_count{1}); }
catch (const std::overflow_error&) { threw = true; }
assert(threw);Review-mondat: az operator+ nem ad
noexcept-ot, mert hiba esetén dob; az erős exception
guarantee teljesül, mivel a művelet vagy új értéket ad vissza, vagy dob,
a bemeneteit nem módosítja. A byte_count egyszerű
összeadása ehelyett noexcept és unsigned
wrappolással túlcsordul — a két változat szándékosan más szerződést
képvisel, és a választást a hívó kontextusa indokolja.
A labor két típussal mutatta meg ugyanazt a mintát. Az
fd_handle-nél a = delete másoló műveletek, a
noexcept mozgatás és a nevesített release()
teszik kényszeríthetővé az egy-owner szerződést; a
get()/borrowed() kölcsönzés marad, mert a
POSIX API nyers int-et vár. A byte_count-nál
az explicit konstruktor és a konverziós operátor elhagyása
szünteti meg a baleseti konverziót és az overload-ambiguitást; a
value() a szándékos kilépőpont. Mindkettőnél a
= default comparisons és egyetlen tagot használó hash adja
az ==/<=>/hash
konzisztenciát. Az interfészreview tehát nem esztétikai kérdés: minden
implicit konverzió és minden hiányzó noexcept egy
potenciális hibát enged be a publikus felületen, amelyet a fordító
static_assert-tel, a futtatás pedig nyomkövető típussal
tesz láthatóvá.
Forrás és ellenőrzési korlát. Ehhez az egységhez nem állt rendelkezésre feldolgozott forrásjegyzet, ezért a szöveg a C++20 nyelvi szabályaira és a bemutatott kód saját szerkezetére támaszkodik; konkrét szabványparagrafust vagy mérési eredményt nem állít. A tanuló laborjában a C++20 munkatervezet (N4950), a hivatalos GCC/Clang dokumentáció és a C++ Core Guidelines megfelelő szakaszai ellenőrizhetik a részleteket; a fejezettervben megadott URL-ek kiindulópontok, nem bizonyítékok.