31.2 tuple, pair, any és type erasure

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.

31.2.1 Pair és tuple: fordítási idejű heterogén csoportosítás

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ásolat

A 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.

31.2.2 Referenciák a tuple-ban és a dangling veszély

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: dangling

Hasonló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.

31.2.3 std::any: futásidejű type erasure

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:

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.

31.2.4 Konfigurációs példa: típusos mezők és plugin-adatok

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.

31.2.5 Döntési táblázat: variant, any, polimorf interfész

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.

31.2.6 Gyakori hibák

31.2.7 Feladatok

  1. (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.

  2. (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.

  3. (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);

31.2.8 Megoldások

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.

31.2.9 Összefoglaló

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.