A template-ek generikus szerződést adnak: a primary template
megfogalmazza, milyen műveleteket várunk el egy típustól, a
specializáció pedig ezt a szerződést pontosítja egy konkrét típusra vagy
típusmintára. A specializáció azonban nem varázslat: láthatósági, ODR-
és overload-resolution következményei vannak. Ugyanakkor az explicit
instantiation nem specializáció, hanem fordítási szervezési eszköz: azt
dönti el, melyik fordítási egységben készüljön el a példányosított kód.
Ez a fejezet ezt a három mechanizmust választja szét: teljes és
részleges specializáció, függvény-overload, valamint explicit
instantiation és extern template. A cél, hogy eld tudd
dönteni, melyik mechanizmus milyen szerződést és milyen fordítási
szervezést támogat.
A primary template a sablon általános definíciója. A
teljes specializáció (explicit specialization) a sablon
minden paraméterére konkrét argumentumot ad. Osztálysablonnál ez azt
jelenti, hogy a specializáció a primary mintájára illeszkedő konkrét
argumentumlistára külön definíciót ad. A specializáció nem örökli a
primary tagjait: ha a primarynak van egy encode tagja, a
specializációnak magának kell biztosítania azt, amit a használat
megkövetel.
Ez a gyakorlatban egy programozói szerződés. A szabvány nem kényszeríti ki, hogy a specializáció ugyanazt az interfészt adja, mint a primary; azt sem, hogy ugyanazt a viselkedést adja. A szerződés a felhasználó és a template szerzője között jön létre. Ha a specializáció eltér, a program attól függően fordul vagy nem fordul, hogy a specializáció látható-e a használat helyén.
#include <string>
#include <vector>
template <typename T>
struct serializer {
static std::string encode(T const&) { return "generic"; }
};
template <>
struct serializer<int> {
static std::string encode(int v) { return std::to_string(v); }
};
template <typename T>
struct serializer<std::vector<T>> {
static std::string encode(std::vector<T> const& v) {
std::string out = "[";
for (std::size_t i = 0; i < v.size(); ++i) {
if (i) out += ",";
out += serializer<T>::encode(v[i]);
}
out += "]";
return out;
}
};Itt a serializer<int> teljes specializáció, a
serializer<std::vector<T>> pedig részleges
specializáció. A serializer<std::vector<int>> a
részleges specializációt használja, és azon belül a
serializer<int> teljes specializációját. A primary
generikus szerződése: van encode, amely
std::string-et ad vissza. A specializációk ezt megtartják,
de más implementációt adnak.
A teljes specializáció akkor jó választás, ha egy konkrét típusra teljesen más implementációt akarsz adni, és a típus nem illeszkedik egy általánosabb mintára. Ha a viselkedést csak egy típusmintára akarod szabni, a részleges specializáció a természetes eszköz.
A részleges specializáció (partial specialization) a primary paraméterlistájának egy mintájára illeszkedik, de nem minden paramétert köt le. Osztálysablonnál ez a primary egy specializált változata; a fordító akkor választja, ha a konkrét argumentumlista illeszkedik a specializáció mintájára, és a specializáció jobb illeszkedés, mint a primary.
template <typename T, typename U>
struct pair_traits {
static constexpr bool same = false;
};
template <typename T>
struct pair_traits<T, T> {
static constexpr bool same = true;
};A pair_traits<int, int> a részleges specializációt
választja, a pair_traits<int, double> a primaryt. A
részleges specializáció feltétele, hogy a specializációs argumentumlista
illeszkedjen a primary paraméterlistájára, és a minta ne legyen teljesen
általános. A pair_traits<T, T> minta illeszkedik, de
nem minden paramétert köt le külön-külön; ezért részleges.
Változósablonoknál a részleges specializáció C++14 óta megengedett. Ez különösen type trait-eknél hasznos:
template <typename T>
inline constexpr bool is_pointer_v = false;
template <typename T>
inline constexpr bool is_pointer_v<T*> = true;
template <typename T>
inline constexpr bool is_pointer_v<T* const> = true;A változósablon részleges specializációja ugyanúgy működik, mint az
osztálysabloné: a minta illeszkedését vizsgálja. A
is_pointer_v<int*> értéke true, a
is_pointer_v<int> értéke false.
Fontos különbség: a részleges specializáció osztály- és változósablonnál megengedett, függvénysablonnál nem. Ez nem stíluskérdés, hanem nyelvi szabály.
A függvénysablonnak nincs részleges specializációja. Ha egy függvénysablonra egy típusmintára specializált viselkedést akarsz adni, overloadot írsz. Az overload egy új függvénysablon, amely részt vesz az overload resolutionben; a részleges specializáció hiánya miatt ez a természetes és hordozható megoldás.
#include <iostream>
#include <vector>
template <typename T>
void print(T const&) {
std::cout << "generic\n";
}
template <typename T>
void print(std::vector<T> const&) {
std::cout << "vector\n";
}A print(std::vector<int>{}) hívás a vektoros
overloadot választja, mert az jobb illeszkedés, mint a generikus
print(T const&). A print(42) hívás a
generikusat választja. Ez nem specializáció, hanem overload: a két
függvénysablon versenyez az overload resolutionben.
A függvénysablon teljes specializációja megengedett, de törékeny. Az explicit specializáció nem vesz részt az overload resolutionben; csak akkor választódik, ha a primaryt választották, és a template argumentumok pontosan illeszkednek. Ezért a függvénysablonoknál az overload általában kiszámíthatóbb.
#include <iostream>
template <typename T>
void f(T) { std::cout << "primary\n"; }
template <>
void f<int>(int) { std::cout << "int specialization\n"; }
template <typename T>
void f(T*) { std::cout << "pointer overload\n"; }
int main() {
f(0); // int specialization
int x = 0;
f(&x); // pointer overload
}A f(0) hívásnál a primary f(T) T=int, és a
f<int>(int) teljes specializáció pontos illeszkedés,
ezért azt használja. A f(&x) hívásnál a pointer
overload f(T*) jobb illeszkedés, mint a primary
f(T), ezért az overloadot választja; a specializáció nem
versenyez vele. Ez szemlélteti, miért kell a függvénysablonoknál az
overloadot előnyben részesíteni, ha a viselkedést típusmintára akarod
szabni.
A gyakorlati döntés tehát: ha a viselkedést egy típusmintára akarod szabni, és a hívás függvényhíváson keresztül történik, írj overloadot. Ha a típusrendszerben akarsz egy specializált definíciót adni, és a típus például egy osztálysablon argumentuma, használj class partial specializationt. A függvénysablon partial specializationja nem opció.
A specializáció láthatósága nem részletkérdés: a program jelentését befolyásolja. A specializációt a primary template deklarációja után, de az első olyan használat előtt kell deklarálni, amely implicit instantiációt okozna. Ha a specializáció csak később válik láthatóvá, a program ill-formed, de diagnosztika nem kötelező. Ezért a specializációk headerbe kerüljenek, a primary után.
Az explicit specializáció nem lesz automatikusan inline
attól, hogy a primary inline volt. Ha
függvényspecializációt teszel headerbe, és több fordítási egységben is
definiálod, inline-nak kell lennie, különben megsérted az
ODR-t. Az osztálysablon specializációja headerbe kerülhet, mert a class
definíciókra az ODR többszörös definíciót enged meg azonos definícióval.
A függvényspecializációnál ez nem ilyen egyszerű: a definíciót
inline-ként kell megjelölni, ha headerben van.
Az ODR alapelve: egy programban egy entitásnak egy definíciója lehet; a template-ek és az inline függvények kivételek, de a definícióknak minden fordítási egységben azonosaknak kell lenniük. Az explicit instantiation definition legfeljebb egyszer szerepelhet a programban; ha többször, az ODR-sértés, diagnosztika nem kötelező.
Az explicit instantiation definition azt kéri a fordítótól, hogy egy adott fordítási egységben példányosítsa a sablont:
template struct Widget<int>;
template void process<int>(int);Az extern template deklaráció azt kéri, hogy ebben a fordítási egységben ne történjen meg az implicit példányosítás, mert a definíció máshol elérhető:
extern template struct Widget<int>;
extern template void process<int>(int);A tipikus elrendezés: a sablon definíciója headerben van; egy
.cpp fájlban explicit instantiation definition szerepel a
gyakran használt specializációkra; a többi .cpp fájlban
extern template deklaráció szerepel a használat előtt. Így
a példányosítás egyszer történik meg, a többi fordítási egység csak
hivatkozik rá.
widget.hpp:
#ifndef WIDGET_HPP
#define WIDGET_HPP
#include <cstddef>
template <typename T>
struct Widget {
T value;
std::size_t size() const;
};
template <typename T>
std::size_t Widget<T>::size() const { return sizeof(T); }
#endifwidget.cpp:
#include "widget.hpp"
template struct Widget<int>;
template struct Widget<double>;main.cpp:
#include "widget.hpp"
extern template struct Widget<int>;
extern template struct Widget<double>;
int main() {
Widget<int> a{42};
Widget<double> b{3.14};
return static_cast<int>(a.size() + b.size());
}Buildparancs:
g++ -std=c++20 -O2 -Wall -Wextra -pedantic -c widget.cpp -o widget.o
g++ -std=c++20 -O2 -Wall -Wextra -pedantic -c main.cpp -o main.o
g++ widget.o main.o -o appAz extern template nem specializáció, hanem
példányosítási kérés. Ha a definíciót végül nem adja meg senki, a linker
hibát jelez. Az explicit instantiation definition csak azokat a tagokat
példányosítja, amelyek a definition pontján definiáltak; ha egy tagot
később definiálsz, lehet, hogy nem lesz példányosítva. Ez a szabvány
részletes szabálya, ezért a definíciók sorrendjére figyelni kell.
Az explicit instantiation elsődleges nyeresége a fordítási idő és a
fordítási egységek mérete. Ha sok .cpp fájl használja
ugyanazt a sablont, implicit instantiációval mindegyik példányosítja a
sablont; extern template-tel a példányosítás egy helyre
kerül. A végső bináris méretre gyakorolt hatás bizonytalanabb: a linker
gyakran összevonja a gyenge szimbólumokat, így a bináris méret nem
feltétlenül csökken. A mérést mindkét szinten el kell végezni.
Mérési terv:
extern template nélkül; (B) explicit
instantiation definition egy .cpp-ben,
extern template a többi .cpp-ben.-ftime-report, Clang-nál
-ftime-trace kapcsolóval mérd a fordítási időt. A
-ftime-trace JSON-t ír, amelyet külön eszközzel lehet
elemezni.size-dal, a szimbólumokat
nm -C-vel vizsgáld. A végső bináris méretet szintén
size-dal mérd.g++ -std=c++20 -O2 -ftime-report -c main.cpp -o /dev/null
clang++ -std=c++20 -O2 -ftime-trace -c main.cpp -o /dev/null
nm -C widget.o | grep Widget
size appEzek a parancsok a laborhoz tartoznak; a fejezet nem állítja, hogy a
példák le lettek futtatva. A várt irány az, hogy a main.o
mérete és a main.cpp fordítási ideje csökken, ha a
példányosítás máshol történik. A végső bináris méretnél a linker
összevonása miatt lehet, hogy nem látszik különbség. A mérés célja nem
egyetlen szám, hanem a mechanizmus költségének megértése.
extern template után hiányzik a
definíció. Link error. Javítás: legyen egy .cpp,
ahol az explicit instantiation definition szerepel.extern template.inline. Több fordítási egységben definiálva
ODR-sértés. Javítás: inline jelölés, vagy a definíció egy
.cpp-be kerül.Magyarázd el, mi a különbség a class template partial specialization és a function template overload között. Miért nincs függvénysablon részleges specializációja? Adj egy példát, amelyben a kettő közötti választás megváltoztatja a program viselkedését.
Írj egy serializer<T> primary template-et, egy
teljes specializációt int-re, egy teljes specializációt
std::string-re, valamint egy részleges specializációt
std::vector<T>-re. A vektoros specializáció a
serializer<T>::encode-ot használja az elemekre.
Ügyelj arra, hogy a specializációk a primary után és a használat előtt
legyenek láthatók.
Adott egy header, amelyben a serializer<int>
specializáció a main után van definiálva. Mi a hiba? Hogyan
javítod? Mi a helyzet akkor, ha a specializáció függvénysabloné, és
nincs inline? Fogalmazz meg ellenőrzési feltételt a
javításra.
Tervezz egy mérést, amely összehasonlítja az implicit instantiációt
az explicit instantiation + extern template megoldással.
Milyen parancsokat használsz, mit rögzítesz, és mire kell vigyázni az
értelmezésnél?
A class template partial specialization a típusmintára illeszkedik, és a primaryt helyettesíti, ha a minta illeszkedik. A function template overload egy új függvénysablon, amely részt vesz az overload resolutionben. A függvénysablonnak azért nincs részleges specializációja, mert a függvényeknél az overload resolution és a template argument deduction már így is összetett; a részleges specializáció plusz dimenziót adna, és a függvényspecializációk nem vesznek részt az overload resolutionben. Az overload természetesebb és kiszámíthatóbb.
Példa: a print(T const&) és a
print(std::vector<T> const&) overloadok közül a
print(std::vector<int>{}) a vektorosat választja, a
print(42) a generikusat. Ha ugyanezt függvénysablon
részleges specializációval akarnád megoldani, nem fordulna le.
Ellenőrzés: a hívások a várt overloadot választják; a fordító nem jelez
hibát a részleges specializáció miatt, mert az nem szerepel a
kódban.
#include <string>
#include <vector>
template <typename T>
struct serializer {
static std::string encode(T const&) { return "generic"; }
};
template <>
struct serializer<int> {
static std::string encode(int v) { return std::to_string(v); }
};
template <>
struct serializer<std::string> {
static std::string encode(std::string const& s) { return "\"" + s + "\""; }
};
template <typename T>
struct serializer<std::vector<T>> {
static std::string encode(std::vector<T> const& v) {
std::string out = "[";
for (std::size_t i = 0; i < v.size(); ++i) {
if (i) out += ",";
out += serializer<T>::encode(v[i]);
}
out += "]";
return out;
}
};A specializációk a primary után vannak, és a használat előtt
láthatók. A serializer<std::vector<int>> a
részleges specializációt használja, azon belül a
serializer<int> teljes specializációját. Ellenőrzés:
serializer<std::vector<int>>::encode({1,2})
eredménye [1,2]; a
serializer<std::string>::encode("x") eredménye
"x". A pontos kimenet a std::to_string és a
string-összefűzés miatt a laborban ellenőrizendő.
A specializáció a használat után van deklarálva, ezért a program ill-formed, de diagnosztika nem kötelező. A fordító a primaryt használhatja, és a specializáció nem érvényesül. Javítás: a specializációt a primary után, de az első használat előtt kell deklarálni, lehetőleg headerben.
Ha a specializáció függvénysabloné, és headerben van,
inline kell, különben több fordítási egységben definiálva
ODR-sértés. Ellenőrzés: a specializáció használatának helyén a
specializáció látható; a nm -C kimenetében a specializáció
szimbóluma a várt módon jelenik meg; a program viselkedése a
specializált implementációt tükrözi.
Két buildet készítek. Az (A) változatban minden fordítási egység
implicit instantiációt végez. A (B) változatban egy .cpp
tartalmazza az explicit instantiation definitiont, a többi
.cpp pedig extern template deklarációt a
használat előtt. GCC-nél -ftime-report, Clang-nál
-ftime-trace kapcsolóval mérem a fordítási időt. A
fordítási egységek méretét size-dal, a szimbólumokat
nm -C-vel vizsgálom. A végső bináris méretet szintén
size-dal mérem.
Rögzíteni kell a compiler és a standard library verzióját, a
targetet, az optimalizálási szintet, a fordítási egységek számát és a
buildparancsokat. Vigyázni kell arra, hogy a linker összevonhatja a
gyenge szimbólumokat, ezért a bináris méret nem feltétlenül csökken; a
fő nyereség a fordítási idő és a fordítási egységek mérete. A mérés csak
akkor értelmes, ha a sablon tényleg sok fordítási egységben példányosul.
Ellenőrzés: a (B) változatban a main.o mérete és a
main.cpp fordítási ideje csökken; a végső bináris méret
változása külön mérendő.
A teljes specializáció egy konkrét argumentumlistára ad új
definíciót; a részleges specializáció egy típusmintára illeszkedik, és
osztály- vagy változósablonnál használható. A függvénysablonnak nincs
részleges specializációja; helyette overloadot írunk. A függvénysablon
teljes specializációja megengedett, de nem vesz részt az overload
resolutionben, ezért törékeny. A specializációt a primary után, az első
használat előtt kell deklarálni; a függvényspecializáció headerben
inline kell legyen. Az explicit instantiation definition és
az extern template nem specializáció, hanem fordítási
szervezés: a példányosítást egy fordítási egységbe tereli. A fordítási
idő és a fordítási egységek mérete mérhető; a végső bináris méretre
gyakorolt hatás a linker összevonása miatt bizonytalan.
A fejezethez nem érkezett forrásjegyzet, ezért a szabványparagrafusokra való pontos hivatkozás, a compiler-specifikus viselkedés és a mérési eredmények ebben a szövegben nem ellenőrzöttek. A megadott kiinduló URL-ek – a WG21 nyilvános dokumentumai, a GCC és a Clang dokumentációja – továbbtanulási kiindulópontok, nem a fejezet állításainak bizonyítékai. A laborban a rögzített compiler- és standard library-verzióval, a szabvány nyilvános draftjának megfelelő szakaszával és tényleges méréssel kell ellenőrizni a specializáció láthatóságára, az ODR-re és az explicit instantiation hatására vonatkozó állításokat. A C++20-as alaphoz tartozó nyilvános draft konkrét URL-je nem szerepelt a forrásjegyzetben; ennek hiányát a labornak kell pótolnia.