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.
_Generic és a makró határaiC-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_genericA 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.
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_tplDefiní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:
template int max_value<int>(int, int);. Ennek akkor
van értelme, ha a definíciót egy .cpp-be tesszük, és a
fordítást több TU-ra akarjuk szétosztani. Az
extern template int max_value<int>(int, int); pedig
azt mondja: „ebben a TU-ban ne példányosítsd, máshol már megvan” — ez a
linkelési idő és a kódméret csökkentésének eszköze.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.
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 = doubleEgy 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.
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:
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.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.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.
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.
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:
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.max_value<double>(1, 2.5) —
tudatos konverzió.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ő.
T& és rvalue.
template <class T> void f(T&); mellett az
f(42) nem fordul le, mert nem const lvalue
referencia nem köthető rvalue-hoz. A const T& vagy
T&& a helyes választás, a szemantika tudatos
megválasztásával.const elvész by-value paraméternél.
void f(T) esetén a T sosem lesz
const; ha a const-ra szükség van, azt a
paraméterbe kell írni.void f(T) tömbbel T = int*, elveszik a hossz;
ha a hossz számít, const T& vagy
std::span.char példa ezt jól mutatja.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.
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.
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.