Az előző részekben a dinamikus polimorfizmus arról szólt, hogy a hívás a dinamikus típus szerint oldódik fel: a hívónak nem kellett ismernie a konkrét osztályt. Most az ellenkező irányt vizsgáljuk: hogyan tud a program futásidőben rákérdezni a dinamikus típusra, és mikor szabad ezt egyáltalán megtenni. A futásidejű típusinformáció (run-time type information, RTTI) a C++ azon képessége, hogy bizonyos objektumokról futásidőben is megállapítható a legjobban leszármazott típus, illetve hogy egy bázisra mutató hivatkozás ellenőrzötten átalakítható legyen egy leszármazott vagy testvér típusra.
A rész vezérmondata: az RTTI nem tervezési alap, hanem
célzott eszköz. Akkor indokolt, amikor a típuskészlet nyílt
(plugin, eseménytovábbítás, szerializálás, diagnosztikai eszköztár), és
a hívási helyen nincs jobb, típusbiztos mechanizmus. Ahol a típuskészlet
zárt és fordítási időben ismert, ott a virtuális függvény, az overload
vagy a std::variant + std::visit olcsóbb és
kifejezőbb. Ahol viszont az RTTI-t használjuk, ott a sikertelenségi
szerződést pointer és referencia esetén egyaránt pontosan kell kezelni —
ez a rész fő gyakorlati mondanivalója.
A szabályok forrása a forrásjegyzék [C++20]
tétele: [expr.typeid], [expr.dynamic.cast] és [type.info]. A fejezet két
teljes programját (typeid_eval.cpp,
rtti_demo.cpp) a lab-gcc profilban
lefordítottuk és lefuttattuk; a kimeneteket a buildjegyzőkönyvek
rögzítik.
A statikus típus (static type) a kifejezés deklarált
típusa; fordítási időben ismert, és a fordító minden típusellenőrzést
ezen végez. A dinamikus típus (dynamic type) annak az
objektumnak a legjobban leszármazott típusa, amelyre egy glvalue
(pointer vagy referencia által megnevezett objektum) ténylegesen
hivatkozik. A kettő csak indirekción keresztül válik szét:
Derived d; esetén nincs kérdés,
Base* p = &d; esetén viszont p statikus
típusa Base, dinamikus típusa Derived.
Dinamikus típusról csak polimorf osztály esetén
beszélhetünk, azaz ha az osztálynak van legalább egy virtuális függvénye
(saját vagy örökölt). Ez az RTTI első és leggyakrabban elrontott
előfeltétele: nem polimorf bázison a typeid csendben a
statikus típust adja vissza, a dynamic_cast pedig nem
fordul le. A dinamikus típus fogalma alatt azt is érteni kell, hogy
konstruktor és destruktor futása közben a dinamikus típus az éppen épülő
vagy bontott osztály — ilyenkor egy leszármazottra irányuló
dynamic_cast jogosan hiúsul meg.
typeid: a
dinamikus típus lekérdezéseA typeid operátor a <typeinfo>
fejlécben deklarált std::type_info osztály egy
const lvalue-ját adja vissza, amely statikus tárolási
idővel él. Két alapvető eset van:
*basePtr vagy egy Base&-et
visszaadó függvény hívása, ahol Base polimorf), az
operandus kiértékelődik, és az eredmény a
dinamikus típust reprezentálja. A hívás és annak
mellékhatásai tehát megtörténnek.typeid(f()), ahol f
int-et ad), és a polimorf osztálytípusú
prvalue is (typeid(h()), ahol
h érték szerint ad vissza D-t): egy
prvalue-nak nincs a statikus típustól eltérő dinamikus típusa.// typeid_eval.cpp — C++20: mikor értékelődik ki a typeid operandusa?
#include <cstdio>
#include <typeinfo>
struct B { virtual ~B() = default; }; // polimorf
struct D : B {};
int f_calls = 0, g_calls = 0, h_calls = 0;
int f() { ++f_calls; return 1; } // prvalue, nem polimorf
B& g() { ++g_calls; static D d; return d; } // polimorf glvalue
D h() { ++h_calls; return D{}; } // polimorf típusú prvalue
int main() {
const std::type_info& t1 = typeid(f()); // statikus típus, nem kiértékelt
const std::type_info& t2 = typeid(g()); // dinamikus típus, kiértékelt
const std::type_info& t3 = typeid(h()); // prvalue: statikus típus, nem kiértékelt
std::printf("f_calls=%d g_calls=%d h_calls=%d\n", f_calls, g_calls, h_calls);
std::printf("t1==int:%d t2==D:%d t3==D:%d\n",
t1 == typeid(int), t2 == typeid(D), t3 == typeid(D));
B* p = nullptr;
try {
(void)typeid(*p); // polimorf glvalue null pointerből
} catch (const std::bad_typeid&) {
std::puts("bad_typeid");
}
}Buildjegyzőkönyv (lab-gcc profil,
g++ -std=c++20 -Wall -Wextra -Wpedantic -O2). A kimenet:
f_calls=0 g_calls=1 h_calls=0, majd
t1==int:1 t2==D:1 t3==D:1, végül bad_typeid. A
számlálók igazolják: csak a polimorf glvalue-t adó g()
hívódott meg.
Ez a különbség a leggyakoribb félreértés forrása. Ráadásul pointer és
pointerált objektum között is éles a határ: typeid(p) a
Base* pointer típust adja, typeid(*p) a
pointerált objektum dinamikus típusát — de csak akkor, ha
Base polimorf, és p nem null. Ha
p null, a typeid(*p) nem null-t ad, hanem
std::bad_typeid kivételt dob, mert a *
operátor eredménye egy null pointerre alkalmazott glvalue.
A std::type_info hasznos felülete: name()
(implementációfüggő, jellemzően torzított/mangled név),
operator==, before() és
hash_code(), három különböző feladattal. Az ==
típusazonosságot dönt el. A before() egy implementáció
által meghatározott teljes rendezés a típusokon
(pontosan az egyik igaz: a.before(b) vagy
b.before(a), ha a típusok különböznek); a sorrend nem
kapcsolódik az öröklési viszonyhoz, és programfuttatások között sem
garantáltan stabil. A hash_code() hash: azonos típusra
ugyanazt adja egy futáson belül, különböző típusokra ütközhet, és nem
rendezésre való. A name() sztring tartalma nem szerződés:
két fordítási egység vagy két fordító eltérő szöveget adhat ugyanarra a
típusra, ezért std::strcmp(typeid(a).name(), ...) helyett
mindig a type_info egyenlőségét használjuk. C++20-ban csak
a külön operator!= került ki a tagok közül (a
!= az ==-ből átírt jelöltként adódik); a
before() megmaradt. Tárolókulcshoz a
<typeindex> fejléc std::type_index
burkolója való: a < operátora a
before()-re, a std::hash specializációja a
hash_code()-ra épül.
// rtti_order.cpp — C++20: before(), ==, hash_code() és type_index
#include <cstdio>
#include <typeindex>
#include <typeinfo>
int main() {
const std::type_info& a = typeid(int);
const std::type_info& b = typeid(double);
bool ab = a.before(b), ba = b.before(a); // C++20-ban is tagfüggvény
std::printf("before: %d %d (pontosan egy igaz)\n", ab, ba);
std::printf("==: %d, hash egyezik int-int: %d\n",
a == typeid(int), a.hash_code() == typeid(int).hash_code());
std::type_index ia{a}, ib{b}; // < a before()-re épül
std::printf("type_index <: %d\n", (ia < ib) == ab);
}Buildjegyzőkönyv (lab-gcc profil).
g++ -std=c++20 -Wall -Wextra -Wpedantic rtti_order.cpp
figyelmeztetés nélkül fordul; a kimenet
before: 0 1 (pontosan egy igaz),
==: 1, hash egyezik int-int: 1,
type_index <: 1. Hogy az int vagy a
double kerül-e előre, az implementációs részlet; csak az
„egyik igaz” tulajdonság szerződés. A name() emberi
olvashatóságához a GNU/Itanium környezetben
abi::__cxa_demangle használható, de ez nem szabványos,
ezért csak diagnosztikai kódban, nem szerződésként jelenhet meg.
Fontos: typeid(a) == typeid(b)
típusazonosságot vizsgál, nem objektumazonosságot. Két
külön objektum, amelyeknek azonos a dinamikus típusa, itt egyenlő; két
bázispointer, amely ugyanarra a teljes objektumra mutat,
typeid-dal nem hasonlítható össze értelmesen.
dynamic_cast: a
sikertelenségi szerződésA dynamic_cast<T>(v) előfeltételei: a cél
(T) pointer vagy referencia teljes
osztálytípusra (vagy void*); a forrásnak polimorfnak kell
lennie, ha a konverzió futásidejű ellenőrzést igényel (leszármazottra
alakítás, keresztkonverzió, void*). A futásidejű ellenőrzés
([C++20] [expr.dynamic.cast]/9) nem a teljes objektumból, hanem
a forrás által mutatott részobjektumból indul, és két
sikeres esetet ismer:
v egy C típusú objektum public
bázis-részobjektumára mutat, és a legjobban leszármazott objektumon
belül pontosan egy C objektum származik
éppen ebből a részobjektumból, az eredmény arra a C-re
mutat. Az egyértelműséget tehát a forrás részobjektumától felfelé
nézzük: ha a teljes objektumban több C is van, de a
mutatott részobjektum csak az egyiknek része, a cast sikeres.v a legjobban leszármazott objektum
public bázis-részobjektumára mutat, és a legjobban leszármazott típusnak
van egyértelmű és public C bázisa, az
eredmény arra a C részobjektumra mutat.Minden más esetben a cast sikertelen.
A két forma hibaszerződése gyökeresen eltér, és ezt nem lehet elégszer hangsúlyozni:
nullptr-t
ad vissza. Ugyanakkor nullptr bemenetre is
nullptr-t ad — sikeres null-konverzió. A hívó tehát a
nullptr-ból nem tudja megkülönböztetni a „nem ez a típus”
és a „nincs objektum” esetet, ezért a null-ellenőrzést a cast előtt kell
elvégezni, ha a kettő számít.std::bad_cast kivételt dob, mert nincs „null referencia”.
Ez viszont azt jelenti, hogy a referencia forma csak ott használható,
ahol a kivétel terjedése megengedett: noexcept függvényben
vagy destruktorban a kezeletlen bad_cast
std::terminate-hez vezet. Null pointerre alkalmazott
referencia-forma nem bad_cast-ot ad, hanem eleve nem
definiált viselkedés, mert a referencia képzése már dereferálja a null
pointert.A dynamic_cast három tipikus esete:
static_cast vagy implicit konverzió való.A*-ból B*-ba, ahol
C : A, B. Ez a static_cast-tal nem
kifejezhető, mert a két statikus típus között nincs öröklési viszony; a
dynamic_cast futásidőben keresi meg a közös C
objektumon belüli B alterületet. Ha a teljes objektumban a
cél bázis nem egyértelmű (például nem virtuális módon többször
szerepel), és a forrás részobjektuma sem választ ki egyetlen célt, a
konverzió meghiúsul: pointer esetén nullptr, referenciánál
bad_cast. A két esetet egy próba mutatja meg, ahol a
D-ben két B részobjektum van:// dynamic_cast_repeated_target.cpp — C++20
#include <cassert>
#include <cstdio>
struct X { virtual ~X() = default; }; // polimorf gyökér
struct B : X {};
struct L : B {};
struct R : B {};
struct M { virtual ~M() = default; }; // független polimorf bázis
struct D : L, R, M {}; // D-ben két B részobjektum van
int main() {
D d;
B* bl = static_cast<L*>(&d); // az L ág B-je
B* br = static_cast<R*>(&d); // az R ág B-je
X* xl = bl; // az L ág X részobjektuma
// (1) Lefelé a forrás részobjektumától: az xl-ből csak EGY B származik
// (az L ágé), ezért a cast sikeres, noha D-ben két B van.
B* r1 = dynamic_cast<B*>(xl);
assert(r1 == bl && r1 != br);
// (2) Keresztkonverzió a teljes objektumon át: M-ből B-re a D-ben
// két B van, a cél nem egyértelmű, ezért nullptr.
M* m = &d;
assert(dynamic_cast<B*>(m) == nullptr);
// M-ből L-re viszont egyértelmű, public bázis: sikeres.
assert(dynamic_cast<L*>(m) == static_cast<L*>(&d));
std::puts("dynamic_cast_repeated_target: OK");
}Buildjegyzőkönyv (lab-gcc profil).
g++ -std=c++20 -Wall -Wextra -Wpedantic dynamic_cast_repeated_target.cpp
figyelmeztetés nélkül fordul, a kimenet
dynamic_cast_repeated_target: OK: az L ág X
részobjektumából indított cast az L ág B-jére mutat (1.
eset), az M-ből indított B* cast
nullptr (a 2. esetben a B nem egyértelmű), az
M-ből L*-ra pedig sikeres.
A dynamic_cast<void*>(p) speciális forma: a
legjobban leszármazott objektum címét adja vissza. Ez nem típus-, hanem
teljes objektum azonosság vizsgálatára alkalmas: két
különböző bázisaltérületre mutató pointer pontosan akkor ad azonos
void*-ot, ha ugyanannak a teljes objektumnak az
alterületei.
static_cast
downcast: előfeltétel és kockázatA static_cast bázisról leszármazottra fordítógenerált
pointer-/referencia-igazítást végez, futásidejű ellenőrzés nélkül.
Előfeltétele, hogy az objektum dinamikus típusa valóban a cél típusa
vagy abból leszármazó legyen; ha nem az, a viselkedés nem definiált —
nem „nullpointer”, nem kivétel, hanem csendben hibás memória-hozzáférés
vagy értelmetlen tagelérés. Ezért a static_cast downcast
csak akkor indokolt, ha az invariánst a program másutt már garantálja
(például egy enum tag vagy egy korábbi ellenőrzött cast
alapján), és ezt a garanciát kód-kommentár és teszt is rögzíti. A
gyakori minta, hogy a teljesítménykritikus belső útvonalon egyszer
dynamic_cast-tal ellenőrzünk, majd a belső ciklusban már
static_cast-ot használunk — ez helyes, ha az objektum
élettartama alatt a dinamikus típus nem változhat.
type_info a shared library határonA GCC és a Clang is ismeri a -fno-rtti (MSVC-nél
/GR-) kapcsolót, amellyel az RTTI-generálás kikapcsolható.
Ez nem szabványos nyelvi mód, hanem implementációs
profil: a dokumentáció szerint ilyenkor a polimorf
dynamic_cast és a dinamikus typeid nem
használható, a kivételkezelés viszont továbbra is működik, mert ahhoz a
fordító szükség szerint maga állít elő típusinformációt. A pontos
korlátozás és a hibaüzenet compiler-verziófüggő, ezért ezt a
laborprofilban fordítási próbával kell rögzíteni; e nélkül a „kikapcsolt
RTTI” viselkedéséről csak annyi állítható biztosan, hogy nem hordozható,
standard programra nem támaszkodhatunk vele.
A type_info azonossága megosztott könyvtár (DSO) határon
implementáció- és ABI-függő kérdés. A típusinformáció a dinamikus
típusokat leíró szimbólumokhoz kötődik; ha két DSO ugyanazt az osztályt
külön fordítja, a type_info példányok egyesítése a loader
és a könyvtár együttműködésén múlik. A libstdc++ egyes konfigurációkban
nevek összehasonlításával dönt az egyenlőségről, más esetekben (például
rejtett láthatóság, -fvisibility=hidden, eltérő burkolás)
ez nem működik, és a dynamic_cast vagy a
typeid-egyenlőség a határon meghiúsulhat. Következmény:
a typeid-azonosságot soha ne használd protokollként
DSO- vagy FFI-határon. Ott a stabil megoldás egy saját,
verziózott típusazonosító (sztring, szám vagy enumerátor) az
interfészben, amelyet mindkét fél ismer, és amely nem függ a C++ ABI
belső szerkezetétől. Kapcsolódó élettartam-csapda: a typeid
eredményére vett const std::type_info& addig érvényes,
amíg a típust leíró kód be van töltve; plugin kirakodása után a
referencia lógó lesz, ezért a típusinformációt nem érdemes a plugin
élettartamán túl tárolni.
A példa egy Device hierarchiát és egy
Persistable mellékhierarchiát használ; a
SmartGauge mindkettőből származik, így a
Device*-ból Persistable*-ra alakítás
keresztkonverzió.
// rtti_demo.cpp — ISO C++20 példa; a kimenet implementációfüggő részt tartalmaz
#include <iostream>
#include <memory>
#include <string>
#include <string_view>
#include <typeinfo>
#include <utility>
#include <vector>
class Device {
public:
virtual ~Device() = default;
virtual std::string_view name() const noexcept = 0;
};
class Persistable {
public:
virtual ~Persistable() = default;
virtual std::string serialize() const = 0;
};
class Gauge : public Device {
public:
explicit Gauge(std::string n) : name_(std::move(n)) {}
std::string_view name() const noexcept override { return name_; }
private:
std::string name_;
};
class SmartGauge final : public Gauge, public Persistable {
public:
using Gauge::Gauge;
std::string serialize() const override { return "gauge:" + std::string{name()}; }
};
// Nem tulajdonol: a hívó felel a d élettartamáért.
void persist_if_possible(Device* d)
{
if (d == nullptr) { // null és "nem ez a típus" szétválasztása
std::cout << "null device\n";
return;
}
if (auto* p = dynamic_cast<Persistable*>(d)) { // cross-cast, pointer forma
std::cout << "persist: " << p->serialize() << '\n';
} else {
std::cout << "skip: " << d->name() << '\n';
}
}
void persist_or_report(Device& d) // referencia forma: bad_cast a hiba
{
try {
Persistable& p = dynamic_cast<Persistable&>(d);
std::cout << "persist(ref): " << p.serialize() << '\n';
} catch (const std::bad_cast& e) {
std::cout << "bad_cast: " << e.what() << '\n';
}
}
int main()
{
std::vector<std::unique_ptr<Device>> devices;
devices.push_back(std::make_unique<Gauge>("g0"));
devices.push_back(std::make_unique<SmartGauge>("g1"));
for (auto& d : devices) {
persist_if_possible(d.get());
persist_or_report(*d);
std::cout << "typeid(*d).name() = " << typeid(*d).name() << '\n';
std::cout << "pontosan SmartGauge? " << (typeid(*d) == typeid(SmartGauge)) << '\n';
}
}Fordítás és futtatás a tanuló laborjában:
g++ -std=c++20 -Wall -Wextra -Wpedantic -O2 rtti_demo.cpp -o rtti_demo && ./rtti_demo
# vagy
clang++ -std=c++20 -Wall -Wextra -Wpedantic -O2 rtti_demo.cpp -o rtti_demo && ./rtti_demoBuildjegyzőkönyv (lab-gcc profil). A
GCC-paranccsal a program figyelmeztetés nélkül fordult, és a következő
viselkedést adta: g0 esetén a pointer forma
skip: g0-t ír, a referencia forma bad_cast-ot
kap el, a typeid-egyenlőség hamis; g1 esetén
mindkét forma sikeres (persist: gauge:g1,
persist(ref): gauge:g1), és a
typeid-egyenlőség igaz. A name() szövege a
lab-gcc profilban 5Gauge, illetve
10SmartGauge (Itanium ABI szerinti torzított név), de ez
nem szerződés: a fordító és a C++ könyvtár dönti el. A Clang-sort nem
ellenőriztük.
if (auto* p = dynamic_cast<D*>(b)) után
p használata helyes;
dynamic_cast<D*>(b)->tag() viszont nullán
dereferál. Javítás: mindig a nullptr-ellenőrzés ágában
használd.noexcept/destruktor
kontextusban. A bad_cast onnan kifelé nem
terjedhet, ezért std::terminate. Javítás: ilyen helyen
pointer forma, vagy előzetes typeid-próba.typeid(*basePtr)
csendben a statikus típust adja, a dynamic_cast nem fordul
le. Javítás: virtuális destruktorral (vagy más virtuális taggal)
polimorffá tenni a bázist — ez egyben a korábbi rész ownership-szabálya
is.typeid a pointeren.
typeid(p).name() a pointer típusát írja le; a dinamikus
típus a typeid(*p).typeid null pointeren.
typeid(*p) null p esetén
std::bad_typeid, nem „ismeretlen típus”.name() sztringje
implementációfüggő; helyette typeid(a) == typeid(b) vagy
std::type_index.typeid-azonosság DSO/FFI határon.
Rejtett láthatóság mellett a típusinformáció nem egyesül, a cast
meghiúsulhat. Javítás: saját, verziózott típusazonosító a határon.static_cast downcast rossz
feltételezéssel. Ha a dinamikus típus nem a cél, nem definiált
viselkedés. Javítás: ellenőrzött cast, majd a bizonyított invariáns
mellett static_cast.A dinamikus típus fogalma csak polimorf osztályokra értelmezhető. A
typeid polimorf glvalue-n a dinamikus, minden más esetben a
statikus típust adja, eredménye const std::type_info&,
amelynek name()-je nem szerződés. A
dynamic_cast pointer formája nullptr-rel,
referencia formája std::bad_cast-tal jelzi a
sikertelenséget; a pointer forma null bemenetre is null-t ad, a
referencia forma null pointerre nem definiált. A cross-cast többszörös
öröklésnél a dynamic_cast egyedi képessége, a
static_cast downcast pedig csak bizonyított invariáns
mellett biztonságos. A kikapcsolt RTTI és a type_info
DSO-határon átnyúló azonossága nem hordozható, implementációfüggő
terület, ezért interfész-szerződés nem épülhet rá.
Magyarázó. Adott:
struct Base { void f(); }; struct Derived : Base { void f(); }; Derived d; Base* b = &d;
Mi lesz a typeid(*b) == typeid(Derived) értéke, és miért?
Mit változtat, ha a Base::f virtuális?
Kódoló. Írd meg az alábbi függvényt, amely a keresztkonverzióval összegyűjti a persistálható alterületeket; az eredmény ne tartalmazzon null elemet:
std::vector<Persistable*> collect_persistable(
const std::vector<std::unique_ptr<Device>>& in);Készíts referencia-formás változatot is, de csak ott, ahol a kivétel terjedése megengedett.
Diagnosztikai. Az alábbi kód lefordul, de futás
közben leáll:
void log(Device& d) noexcept { auto& p = dynamic_cast<Persistable&>(d); std::cout << p.serialize(); }.
Magyarázd meg a hibát, és javítsd két különböző szerződés mentén (nem
dobó és dobó változat).
Nehezebb. Két vektorban Device* és
Persistable* pointerek vannak ugyanazokra az objektumokra.
Hogyan párosítanád őket teljes objektum szerint, és miben más ez, mint a
typeid szerinti csoportosítás?
1. megoldás. Nem polimorf Base esetén a
typeid(*b) a statikus típust, azaz Base-t
adja, ezért az összehasonlítás hamis — pedig az objektum valóban
Derived. Ha a Base-nek van virtuális
függvénye, a *b polimorf glvalue, a typeid a
dinamikus típust adja, és az összehasonlítás igaz lesz. Ellenőrzési
feltétel: a program mindkét változatban forduljon le, és a második
esetben a kiírt érték 1 legyen; a diagnosztika indoklása
nevezze meg a „polimorf osztály” feltételt.
2. megoldás. A pointer forma természetes, mert a „nincs ilyen alterület” nem hiba:
std::vector<Persistable*> collect_persistable(
const std::vector<std::unique_ptr<Device>>& in)
{
std::vector<Persistable*> out;
out.reserve(in.size());
for (const auto& d : in) {
if (d == nullptr) continue;
if (auto* p = dynamic_cast<Persistable*>(d.get())) out.push_back(p);
}
return out;
}A referencia-formás változat csak ott használható, ahol a
bad_cast elkapható:
for (const auto& d : in) {
if (!d) continue;
try {
out.push_back(&dynamic_cast<Persistable&>(*d));
} catch (const std::bad_cast&) {
// nincs Persistable alterület: kihagyjuk
}
}Mindkét változat nem tulajdonol: a visszaadott
pointerek érvényessége a bemeneti vektor és a mögötte lévő objektumok
élettartamához kötött, azt a hívó köteles betartani. Ellenőrzési
feltétel: a kimenet hossza egyezzen a persistálható objektumok számával,
ne tartalmazzon nullptr-t, és a függvény ne módosítsa a
bemenetet.
3. megoldás. A log
noexcept, a
dynamic_cast<Persistable&> viszont
std::bad_cast-ot dob, ha az objektumnak nincs
Persistable alterülete. A kivétel egy noexcept
függvényből kifelé nem terjedhet, ezért a fordító
std::terminate-et hív. Nem dobó javítás: pointer forma,
null-ellenőrzéssel, és a hiányzó alterület naplózása külön ágban. Dobó
javítás: a noexcept eltávolítása, a
try/catch (const std::bad_cast&)
bevezetése, és a hibapolitika dokumentálása (naplózás vagy továbbdobás).
Ellenőrzési feltétel: a nem dobó változat Gauge-ra is és
SmartGauge-ra is lefut, nem hív terminate-t,
és a nem dobó volta static_assert(noexcept(...))-tal vagy
dokumentált indoklással alátámasztható.
4. megoldás. Teljes objektum szerinti párosításhoz
dynamic_cast<void*> való: két pointer pontosan akkor
mutat ugyanarra a teljes objektumra, ha a castolt void*
értékük azonos (ehhez mindkét oldalon polimorf típus kell, és a
pointerek nem nullák). Ezt használhatod kulcsként egy
std::unordered_map<void*, ...>-ban, de figyelj rá,
hogy a kulcs csak az objektum élettartama alatt érvényes: felszabadítás
után cím-újrafelhasználás miatt hamis egyezés lehetséges. A
typeid szerinti csoportosítás ezzel szemben típust
azonosít, nem objektumot: két külön SmartGauge példány egy
csoportba kerül, és a pointer-igazítás miatt a Device* és
Persistable* értékek összehasonlítása önmagában
félrevezető. Ellenőrzési feltétel: a párosítás pontosan a közös
objektumokra adjon egyezést, a typeid-alapú csoportosítás
pedig adott esetben több objektumot vonjon össze — a két eredmény
különbözősége a helyes értelmezés bizonyítéka.