26.1 Függvénysablon, instantiation és deduction

Ez a rész a C-beli típusgenerikusság korlátaiból indul ki, és eljut a C++ függvénysablonig: hogyan lesz egy deklarációból konkrét függvény, hogyan következtet a fordító a sablonparaméterre, és hogyan olvassuk ki a hibát a diagnosztikai láncból. A template mint szöveges helyettesítés tévhitével itt csak egy bekezdésben foglalkozunk, a teljes overload-resolution algoritmus pedig későbbi részre tartozik; a cél a pontos, de kezelhető modell.

26.1.1 Kiindulópont: a C _Generic és a makró határai

C-ben a „típusfüggő függvény” hagyományos megoldása a makró. A C11 óta a _Generic segítségével a makró a controlling expression statikus típusa alapján választhat kifejezést, tipikusan egy típusonként külön megírt függvényt:

/* max_generic.c -- ISO C17 */
#include <stdio.h>

static int    max_int(int a, int b)       { return a < b ? b : a; }
static double max_double(double a, double b) { return a < b ? b : a; }

#define MAX(a, b) _Generic((a), \
    int:    max_int,            \
    double: max_double          \
)(a, b)

int main(void) {
    printf("%d\n",   MAX(2, 3));
    printf("%.2f\n", MAX(2.0, 3.0));
    printf("%d\n",   MAX(1, 2.5));   /* csendben int-re konvertál! */
    return 0;
}
gcc -std=c17 -Wall -Wextra -Wpedantic -O2 max_generic.c -o max_generic
clang -std=c17 -Wall -Wextra -Wpedantic -O2 max_generic.c -o max_generic

A minta tanulsága nem az, hogy a _Generic rossz, hanem hogy nem paraméterezhető: minden támogatott típushoz kézzel kell felvenni egy ágat és egy függvényt. Ha egy új típust (például long long) használni akarunk, a kód nem fordul le, és a hibaüzenet a makró kifejtésére hivatkozik, nem a szándékunkra. Ráadásul a típusválasztás a controlling expression alapján történik, nem az összes argumentum közös típusa alapján: a MAX(1, 2.5) esetben az int ág fut le, a 2.5 pedig csendben int-té konvertálódik. Ez pontosan az a hibaosztály, amit egy típusbiztos generikus eszköznek ki kell zárnia.

A C++ válasza erre a függvénysablon (function template): olyan függvény-leírás, amelyben egy vagy több típus (vagy nem típus) paraméter szerepel, és amelyből a fordító a használat helyén konkrét függvényt állít elő.

Fontos határvonal: a sablon nem szöveges makró. A helyettesítés nem karakterlánc-szintű, a definíciót a fordító a deklaráció helyén ellenőrzi (a nem függő részeket), és a létrejövő függvény valódi, típusellenőrzött entitás. A nem függő és függő nevek keresésének kettős szabályát (two-phase lookup) és az ADL-t a Dependent name, kétfázisú lookup és ADL rész tárgyalja részletesen.

26.1.2 Deklaráció, definíció és instantiation

A sablon deklarációja template fejléccel kezdődik, amely felsorolja a sablonparamétereket. A klasszikus példa:

// max_tpl.cpp -- ISO C++20
#include <iostream>

template <class T>
T max_value(T a, T b) {
    return a < b ? b : a;
}

int main() {
    std::cout << max_value(2, 3)     << '\n';  // T = int
    std::cout << max_value(2.0, 3.0) << '\n';  // T = double
}
g++     -std=c++20 -Wall -Wextra -Wpedantic -O2 max_tpl.cpp -o max_tpl
clang++ -std=c++20 -Wall -Wextra -Wpedantic -O2 max_tpl.cpp -o max_tpl

Definíció. Legyen adott egy template<...> fejléc és egy függvénydeklaráció, amelyben a sablonparaméterek szerepelnek. A sablon önmagában nem hoz létre futtatható függvényt; a belőle konkrét típusokkal előállított függvényt nevezzük specializációnak (specialization), az előállítás folyamatát pedig instantiation-nak. A max_value(2, 3) hívás hatására a fordító implicit módon létrehozza a max_value<int> specializációt; a max_value(2.0, 3.0) hívás a max_value<double>-t.

Két fogalmat érdemes szétválasztani:

Fontos következmény. Mivel a specializációt a használat helyén kell előállítani, a sablon definíciójának minden olyan fordítási egységben láthatónak kell lennie, ahol példányosítás történik. Ezért kerülnek a sablonok (a nem sablon függvényekhez képest szokatlan módon) általában fejlécfájlba. A deklaráció önmagában nem elég: template <class T> T max_value(T, T); hívása linkelési hibát ad, ha a definíció nem elérhető a TU-ban.

26.1.3 Típusparaméter és explicit template argumentum

A template <class T> és a template <typename T> között nincs szemantikai különbség; a typename utóbb lett bevezetve, és ott kötelező, ahol a név függő típusra utal. A típusparaméter neve a sablon testén belül típusként viselkedik.

Az explicit template argumentum a függvénynév után, szögletes zárójelben adható meg: max_value<double>(2, 3). Ilyenkor a megadott argumentum felülírja a következtetést, és a hívást a szokásos függvényhívási konverziókra bízza: a 2 és 3 itt double-lé konvertálódik. Ez a _Generic példa MAX(1, 2.5) esetével rokon, de itt mi döntöttünk tudatosan, nem a makró csendben.

Ha a sablonnak több paramétere van, az explicit argumentumok balról jobbra töltődnek ki, a maradék továbbra is következtethető:

template <class A, class B>
void pair_print(A a, B b);   // pair_print<int>(1, 2.5) -> A = int, B = double

Egy explicit template argumentum után nem lehet „kihagyni” egy paramétert és később újra következtetni — a megadott lista mindig az első paraméterektől indul.

26.1.4 Deduction: a paraméter és az argumentum alapján

A template argument deduction (sablonargumentum-következtetés) az a folyamat, amelyben a fordító a hívás argumentumainak típusából (A) és a függvény deklarált paramétertípusából (P) visszafejti a sablonparaméter értékét. A következtetés paraméterenként, párban történik; ha ugyanarra a T-re két argumentum két különböző eredményt ad, a hívás hibás.

A legfontosabb szabályok:

  1. By-value paraméter (T): az argumentum típusából a felső szintű const/volatile minősítő eltűnik, a tömb- és függvénytípus pedig pointerré alakul (array-to-pointer, function-to-pointer decay). const int argumentumból tehát T = int lesz.
  2. Lvalue referencia (T&): a T az argumentum teljes típusát veszi fel, a minősítőkkel együtt; tömb esetén a tömb típusa marad. Nem const T& nem köthető rvalue-hoz, ezért f(1) hibás.
  3. const T&: a const a paraméterben van, nem a T-ben, ezért a const int argumentumból is T = int lesz; rvalue-t is köt (átmeneti objektum létrehozásával), és a tömb típusát is megőrzi.

Az alábbi táblázat template <class T> void f(P); esetére mutatja az eredményt:

Paraméter (P) Argumentum (A) Dedukált T Tényleges paramétertípus
T int int int
T const int lvalue int int
T int[3] int* int*
T& int lvalue int int&
T& const int lvalue const int const int&
const T& const int lvalue int const int&
const T& int[3] int[3] const int(&)[3]

A táblázat utolsó sora a legtanulságosabb: referencián keresztül a tömb nem decay-el pointerré, ezért a const T& overload képes a tömb hosszát is megőrizni — ez a későbbi std::span és range-tervezés egyik alapmotívuma. A T&& alak (forwarding reference) szándékosan kimarad, azt a Forwarding reference és perfect forwarding rész tárgyalja.

A következtetés nem mindig lehetséges: ha a paraméter típusában a sablonparaméter nem szerepel „lehúzható” helyen (például csak egy std::vector<T>::value_type alakban), az non-deduced context, és ott explicit argumentum kell. A következtetés alapja mindig a hívás argumentumtípusa, nem a függvény belsejében használt típus.

26.1.5 Template overload és nontemplate candidate

Ha ugyanarra a névre létezik sablon és közönséges (nem sablon) függvény is, mindkettő jelölt lesz, és a szokásos túlterhelés-feloldás dönt — azzal a kiegészítéssel, hogy egyenlően jó illeszkedés esetén a nem sablon változat nyer. Ez teszi lehetővé a „kényelmi” overloadok biztosítását:

#include <cstdio>

template <class T>
T pick(T a, T b) { return a < b ? b : a; }

int pick(int a, int b) { return a < b ? b : a; }  // specializált gyors út

int main() {
    std::printf("%d\n", pick(1, 2));      // nem sablon: pontos illeszkedés mindkettőnél -> nem sablon nyer
    std::printf("%.2f\n", pick(1.0, 2.0)); // sablon: T = double
    std::printf("%d\n", pick('a', 'b'));  // sablon: T = char (a nem sablon promóciót igényelne)
}

A harmadik hívás mutatja a lényeget: a pick(int, int) csak char → int promócióval hívható, ami gyengébb illeszkedés, mint a sablon pontos T = char következtetése, ezért a sablon nyer. Ez a viselkedés sok meglepetés forrása; ha a nem sablon változatot mindig elő akarjuk húzni, azt nem a feloldásra kell bízni, hanem külön névre vagy if constexpr-re.

26.1.6 Diagnosztikai lánc és minimális reprodukció

A sablonhiba üzenete hosszú, de szerkezete ismétlődő: a fordító megmutatja, melyik sablon melyik példányosításában történt a hiba, mely argumentumtípusokból próbált következtetni, és hol vallott kudarcot. A hatékony olvasás iránya: először a legbelső „note: candidate template ignored” vagy „deduced conflicting types” sor, utána a hívási lánc.

// fail_deduce.cpp -- szándékosan nem fordul le
template <class T>
T max_value(T a, T b) { return a < b ? b : a; }

int main() {
    auto x = max_value(1, 2.5);   // hiba: T = int és T = double egyszerre
}

A tipikus üzenet lényege: deduced conflicting types for parameter 'T' ('int' vs. 'double'). A javítás háromféle lehet, és a választás tervezési döntés:

  1. Két külön paraméter: template <class A, class B> auto max_value(A a, B b) — de ekkor a visszatérési típus és az összehasonlítás típusa kérdéses.
  2. Explicit argumentum: max_value<double>(1, 2.5) — tudatos konverzió.
  3. A hívó oldal típusának egységesítése: max_value(1.0, 2.5).

A minimális reprodukció módszere: a hibás kódból tartsuk meg a sablon deklarációját, egyetlen hívást és a szükséges típusokat; a #include-okat és a többi függvényt dobjuk el. Ha a hiba így is megvan, a hiba a következtetésben van; ha eltűnik, a hiba a környező overload-halmazban vagy a fejlécsorrendben keresendő. A reprodukciót mindig a rögzített compiler-verzióval és -std=c++20 kapcsolóval futtassuk, mert a diagnosztika szövege compiler- és verziófüggő.

26.1.7 Tipikus hibák

26.1.8 Feladatok

1. feladat (elemzés). Adott template <class T> void show(T v);, template <class T> void show_ref(T& v); és template <class T> void show_cref(const T& v);. Mi lesz a dedukált T és a paraméter típusa a következő hívásoknál: show(3), show(ci) ahol const int ci = 3;, show_ref(a) ahol int a[4];, show_cref(5)?

2. feladat (kódolás). Írj clamp_value függvénysablont, amely egy értéket egy [lo, hi] zárt intervallumba szorít, és működik int, double és const char* esetén is. A const char* esethez indokold, miért problematikus a < operátor, és mit teszel helyette.

3. feladat (diagnosztika). Készíts minimális reprodukciót arra a hibára, amikor egy sablon két argumentuma miatt T következtetése ellentmondásos, majd add meg két különböző javítást, és indokold, melyik melyik szemantikát választja.

26.1.9 Megoldások

1. megoldás. show(3): T = int, paraméter int. show(ci): T = int (a felső szintű const eltűnik), paraméter int. show_ref(a): T = int[4], paraméter int(&)[4] — a referencia megőrzi a tömb típusát. show_cref(5): T = int, paraméter const int&, és a const a paraméterből jön, nem a T-ből. Ellenőrzési feltétel: írj minden hívás elé static_assert(std::is_same_v<decltype(...), ...>)-ot a megfelelő típusra, és forduljon le -std=c++20-szal.

2. megoldás. template <class T> T clamp_value(const T& v, const T& lo, const T& hi) { return v < lo ? lo : (hi < v ? hi : v); }. A const T& választás azért jó, mert nem másol, rvalue-t is köt, és tömböt is megőriz. const char* esetén a < a pointereket hasonlítja össze, ami nem lexikografikus és nem hordozható értelmes eredményt — ott külön overload kell std::strcmp-vel vagy std::string_view-vel. Ellenőrzési feltétel: clamp_value(5, 1, 3) == 3, clamp_value(2.5, 0.0, 1.0) == 1.0, és a -Wall -Wextra ne adjon figyelmeztetést.

3. megoldás. Reprodukció: a fenti max_value(1, 2.5) példa. Első javítás: max_value<double>(1, 2.5) — itt a hívó dönt, a 2.5 és az 1 is double-lé konvertálódik, a visszatérés double. Második javítás: template <class A, class B> auto max_value(A a, B b) — itt a két argumentum típusa független, a visszatérési típus auto-val a feltételes kifejezésből következik, ami viszont a common_type szabályai szerint konvertál. Az első megoldás előre deklarált, egységes típust kényszerít; a második megengedi a heterogén bemenetet, de a visszatérési típusról külön kell dönteni. Ellenőrzési feltétel: mindkét változat forduljon -std=c++20 -Wall -Wextra -Wpedantic alatt, és a viselkedés a kétféle hívásnál dokumentált legyen.

26.1.10 Összefoglaló

A C _Generic + makró megoldása típusonként kézzel bővítendő, és a típusválasztás nem az argumentumok közös típusán alapul. A C++ függvénysablon ezt paraméterezhetővé teszi: a template <class T> fejléc és a benne szereplő T együtt egy függvénycsaládot ír le, amelyből a fordító a használat helyén instantiationnal állítja elő a konkrét specializációt. A deduction a paraméter (P) és az argumentum (A) párosából fejti vissza T-t: by-value esetén a felső szintű minősítők eltűnnek és a tömb pointerré alakul, T& esetén a teljes típus megőrződik, const T& esetén a const a paraméterből jön. Sablon és nem sablon overload közül egyenlő illeszkedésnél a nem sablon nyer. A hibát a diagnosztikai lánc legbelső sorától olvassuk visszafelé, és minimális reprodukcióval izoláljuk. A sablon definíciójának minden példányosító TU-ban láthatónak kell lennie — ezért él tipikusan fejlécben. A forwarding reference, a specializáció és a kétfázisú lookup a következő részek témája. A példák fordítási parancsai a saját laborodhoz tartoznak; ebben a szövegben nem történt futtatás vagy mérés.