A heterogén adatok kezelése visszatérő feladat minden
DevOps-eszközben: konfiguráció, plugin-metaadat, esemény-payload,
telemetria-címke. A C++ három egymástól eltérő modellt ad erre: a
fordítási idejű, zárt típuslistát (std::pair,
std::tuple), a futásidejű típus-törlést
(std::any), valamint a klasszikus polimorf interfészt
(öröklés + virtuális hívás). Ebben az alfejezetben ezeket a modelleket
hasonlítjuk össze egy kis konfigurációs példán, kiemelve a típusismeret
és a tulajdonlás következményeit. Az
optional/variant alapokat itt már ismertnek
tekintjük optional, variant és visit (31.1), a
callable-wrapper és a std::function-alapú type erasure
tárgyalása invoke, function wrapper és type erasure
(30.2), a típuslevezetésé auto, decltype és
típuslevezetés (19.4) szerepel.
A std::pair<T1,T2> (<utility>,
C++98) pontosan két publikus tagot tárol first és
second néven, a std::tuple<Ts...>
(<tuple>, C++11) nulla vagy több elemet tetszőleges
típuslistával. Mindkettő értéktípus: másolás és
mozgatás tagonként történik; nincs bennük rejtett pointer vagy
megosztott állapot. Ezzel szemben a std::any (lásd alább)
típus-érzéketlen konténer — a kettő nem keverendő.
A C++17-es strukturált kötés
(structured binding) rövid hozzáférést ad:
auto [host, port] = endpoint; // másolat készül a két tagról
auto& [h2, p2] = endpoint; // referenciát kötünk, nincs másolatA deklaráció mögött egy névtelen, auto-val levezetett
ideiglenes vagy referenciaobjektum áll; a nevek ehhez kötődnek, és a
tuple_size/tuple_element specializációkon
keresztül get<I> hívásokra fordulnak. Ezért
auto [x,y] másol, auto& [x,y] köt. Ugyanez
a mechanizmus működik std::pair, std::tuple,
std::array esetén.
Az elemek elérése: std::get<0>(t) indexszel,
std::get<int>(t) típus szerint, ha az adott típus
egyedi a listában. Visszatérési értéke referencia;
std::get<I>(std::move(t)) esetén
T&&. A std::tie(a, b) egy
std::tuple<A&, B&>-t készít — unpackingra
(std::tie(x,y) = p;) és lexikografikus összehasonlításra
egyaránt használjuk. A std::apply(f, t) a tuple elemeit
argumentumként átadja egy callable-nek; a
std::tuple_cat(t1, t2) új tuple-t fűz össze, elemenként
másolva vagy mozgatva:
auto t = std::make_tuple(1, 2.5, std::string{"x"});
auto print = [](int a, double b, const std::string& s) {
std::printf("%d %.1f %s\n", a, b, s.c_str());
};
std::apply(print, t); // print(1, 2.5, "x")
auto m = std::tuple_cat(t, std::make_tuple(true)); // (1, 2.5, "x", true)A std::make_tuple a tagok decay-elt típusát
tárolja (tömb → pointer, referencia → érték); referencia megtartásához
std::tie vagy std::forward_as_tuple
szükséges.
A tuple nem tartja életben a benne lévő elemeket; ha elemként
referencia szerepel, az csak kölcsönzés. A
std::tie(x, y) típusa
std::tuple<int&, int&>; meddig él ez a
hivatkozás? Addig, amíg x és y élnek. Ha a
tuple hosszabb életű, mint a hivatkozott objektum, dangling pointerré
válik:
std::tuple<int&, std::string&> refs; // referencia-tuple
{
int x = 1;
std::string s = "a";
refs = std::tie(x, s);
} // x és s megszűnt
// std::get<0>(refs); // UB: danglingHasonlóan hibás a return std::tie(a, b); minta: a
visszaadott tuple lokális változókra hivatkozik. A
std::forward_as_tuple(f()) rvalue-referenciákat ad, de az
f() ideiglenese a teljes kifejezés végén megszűnik, így a
tuple rögtön utána dangling. Ez alattomos hiba, mert a fordító gyakran
nem jelez.
A tie eredményét csak akkor tárold, ha a hivatkozott
objektumok élettartama biztosan fedi. Ha másolás kell, használj
std::make_tuple-t; ha a szándék kölcsönzés, az legyen
lokális, rövid életű. A strukturált kötéssel kapcsolatban ugyanez a
szabály: auto [x, y] = std::tie(a, b); esetén
x, y referencia-tagokká válnak, ami addig biztonságos, amíg
a és b él.
A std::any (<any>, C++17) egyetlen
értéket tárol tetszőleges másolható típusból.
Típus-törlést (type erasure) valósít meg: a tárolt típus a fordító
számára ismeretlen, de futásidőben type() visszaad egy
std::type_info-t. A tárolt típusnak
CopyConstructible-nek kell lennie, mert a
std::any maga is másolható, és a másolás a benne lévő
objektumot is másolja. Ezért nem lehet std::any-be
std::unique_ptr-t tenni (fordítási hiba), se nem-másolható
mutexet vagy stream-et.
Hozzáférés any_cast-tal:
std::any_cast<T>(a) — érték szerint: másolatot
ad; típuseltérés esetén std::bad_any_cast-et dob.std::any_cast<T&>(a) és
const T& — referenciát ad a belső objektumra. Érvényes
mindaddig, amíg az any módosítatlan;
minden reset, értékadás vagy emplace
érvényteleníti (invalidation).std::any_cast<T>(&a) — pointer form:
típuseltérésnél nullptr-t ad, nem dob. Ez a „nem dobó”
változat, amit a gyakorlatban a legtöbbször használunk.A small-object optimalizálás (SBO) a std::any esetén
implementációfüggő: a szabvány nem garantálja, hogy egy
adott méret alatti típust in-line tárol, és nem garantálja a cím
stabilitását sem. Ne építs tehát teljesítmény-becslést vagy
mutató-érvényességet arra, hogy „biztos a belső bufferben van”. Ugyanígy
a type() által visszaadott type_info-ra
típusnév szerinti string-összehasonlítást alapozni nem hordozható minta;
az any típusellenőrzését mindig any_cast-ra
bízzuk.
Az any gyakran keveredik a „view” fogalmával: nem az. Az
any tulajdonol (értékszemantika, belső
másolat), és a any_cast<T&> az egyetlen kölcsönző
kilépés. Ha hosszabb életű hozzáférést akarsz, T-t értékben
vagy T&-et csak az any élettartamán belül
kérj.
A tipikus minta: a szolgáltatás saját konfigurációját struct/tuple
fogja össze (zárt, fordítási idejű típuslista), míg a plugin-specifikus
kulcs–érték párok std::map<std::string, std::any>-ben
landolnak, mert a plugin készlete a fő programból nem ismert.
// cfg.cpp — teljes program
#include <any>
#include <cstdint>
#include <cstdio>
#include <map>
#include <string>
#include <tuple>
struct Config {
std::string bind{"127.0.0.1"};
std::uint16_t port{8080};
std::int64_t timeout_ms{500};
};
bool operator==(const Config& a, const Config& b) {
return std::tie(a.bind, a.port, a.timeout_ms)
== std::tie(b.bind, b.port, b.timeout_ms);
}
std::map<std::string, std::any> plugin_settings;
void read_retries() {
auto it = plugin_settings.find("retries");
if (it == plugin_settings.end()) { std::puts("retries: nincs"); return; }
if (auto* p = std::any_cast<int>(&it->second)) std::printf("retries=%d\n", *p);
else std::puts("retries: nem int");
}
int main() {
Config base;
auto [bind, port, timeout_ms] = base;
std::printf("bind=%s port=%u timeout=%lld\n",
bind.c_str(), port, static_cast<long long>(timeout_ms));
plugin_settings["retries"] = 3;
plugin_settings["tag"] = std::string{"edge"};
read_retries();
// plugin_settings["tag"] = std::make_unique<int>(1); // nem fordul: nem másolható
return 0;
}Build a tanuló laborjában, rögzített compilerrel:
g++ -std=c++20 -O2 -Wall -Wextra cfg.cpp -o cfg (Clang
esetén
clang++ -std=c++20 -O2 -Wall -Wextra cfg.cpp -o cfg). A
példa nem lett lefuttatva e dokumentum fordításakor; a
helyességét a tanuló saját mérési környezetében ellenőrzi. Figyeljük meg
a tulajdonlási különbséget: Config mezői értéktulajdonosok,
a plugin_settings értékei is (az any másol),
viszont a std::any_cast<int>(&it->second)
pointer rövid életű, csak a hívás scope-jában használjuk.
| Szempont | std::tuple/pair |
std::variant<Ts...> |
std::any |
Polimorf interfész |
|---|---|---|---|---|
| Típuslista | rögzített, fordítási idejű | zárt, fordítási idejű | nyitott, futásidejű | nyitott (öröklés) |
| Hozzáférés | get<I> |
get<T>, visit,
get_if |
any_cast |
virtuális hívás |
| Hibamód | fordítási hiba | bad_variant_access / nullptr |
bad_any_cast / nullptr |
nem jellemző |
| Tárolási méret | elemek összege | legnagyobb alternatíva + tag | SBO vagy heap (impl.) | pointer + heap |
| Másolás | elemenként | ha mind másolható | csak másolható típus | interfész dönt |
| Mikor válaszd | ismert, vegyes mezők | zárt alternatívahalmaz | plugin/config | viselkedés-orientált |
Hüvelykujjszabály: ha a lehetséges típusok halmaza fordítási
időben ismert és zárt, tuple (egyidejű jelenlét)
vagy variant (egy aktív alternatíva). Ha a típus
futásidőben érkezik, any. Ha viselkedést is kell (nem csak
adatot), polimorf interfész.
any-be:
std::any a = std::make_unique<int>(1); — nem fordul,
a unique_ptr nem másolható. Ilyenkor
variant<..., unique_ptr<...>> a helyes
választás.return std::tie(a, b); lokálisokból:
dangling. Helyette return std::make_tuple(a, b);
értékben.auto [x, y] = std::tie(a, b); majd az
eredeti séma szerint módosítás: x, y referenciák, ezért az
a-ra mutatnak. Szándékos lehet, de gyakran félreértés.any_cast<T>(a) érték szerint
minden hívásnál másol — teljesítmény-érzékeny kódban pointer vagy
T& forma.(magyarázó) Mutasd meg, hogy a
auto t = std::tie(x, y); std::get<0>(t) = 10; után
x értéke 10 lesz-e, és meddig érvényes a t.
Indokold, mi a helyzet akkor, ha t egy függvényből
visszatérő érték.
(kódolási) Írj egy
split_host_port(const std::string&) függvényt, ami
"127.0.0.1:8080"-ból
std::tuple<std::string,int>-et ad. A hívásnál
használj strukturált kötést, és std::tie-val hasonlíts egy
elvárt tuple-höz.
(diagnosztikai) Az alábbi kód
std::bad_any_cast-et dob. Keresd meg az okot, és írd át nem
dobó változatra.
std::any a = 42;
double v = std::any_cast<double>(a);1. A std::tie(x, y) egy
std::tuple<int&, int&>;
std::get<0>(t) int&-et ad, ami
x-re hivatkozik. A std::get<0>(t) = 10;
tehát x-et módosítja. A t addig érvényes, amíg
x és y élnek — lokális scope-ban helyes. Ha
return std::tie(x, y); függvényből jön vissza, a
visszatérés pillanatában a lokálisok megszűnnek, a hívó oldal dangling
referenciát kap, és bármilyen dereferálás UB.
2.
#include <stdexcept>
#include <string>
#include <tuple>
std::tuple<std::string, int> split_host_port(const std::string& s) {
auto pos = s.rfind(':');
if (pos == std::string::npos) throw std::invalid_argument("no ':'");
return {s.substr(0, pos), std::stoi(s.substr(pos + 1))};
}
int main() {
auto [host, port] = split_host_port("127.0.0.1:8080");
auto expected = std::make_tuple(std::string{"127.0.0.1"}, 8080);
return std::tie(host, port) == expected ? 0 : 1;
}Ellenőrzés: main 0-t ad, ha az egyezés teljes. A
std::tie(host, port) lokális változók referenciáit adja,
ugyanabban a scope-ban, tehát biztonságos; ha a függvény visszatérne
vele, már nem lenne az.
3. Az a tárolt típusa int,
a any_cast<double> nem egyezik, ezért dob. Nem dobó
javítás:
std::any a = 42;
if (auto* p = std::any_cast<int>(&a)) {
int v = *p; // biztos elérés
}
// vagy ha double kell:
double d = static_cast<double>(std::any_cast<int>(a));A pointer forma nullptr-t ad típuseltérésnél; az érték
forma csak akkor biztonságos, ha a típus garantáltan egyezik, vagy
try/catch (const std::bad_any_cast&)
védi.
A pair és a tuple fordítási idejű,
rögzített típuslistát képvisel értékszemantikával; a get,
tie, apply, tuple_cat és a
strukturált kötés a mindennapi eszköztáruk. A referenciát tartalmazó
tuple (különösen tie, forward_as_tuple) csak
kölcsönöz — élettartamát a hivatkozott objektumokhoz kell kötni,
különben dangling. A std::any futásidejű típus-törlést
valósít meg, csak másolható típust tárol, any_cast-tal
érhető el; az SBO implementációfüggő, a visszaadott referencia pedig
addig érvényes, amíg az any módosítatlan. A modellek
közötti választás a típushalmaz zártsága, a hibamód és a tulajdonlás
szerint történik: zárt alternatívák → variant, nyitott
futásidejű adat → any, viselkedés → polimorf interfész.