A sablon nem szöveges makró: a fordító a sablon definícióját akkor is
elemzi, amikor a konkrét típusargumentum még ismeretlen. Ebből
következik a fejezet központi kérdése: egy név jelentését mikor lehet
eldönteni — már a definíció helyén, vagy csak a példányosítás helyén? A
válasz nem esztétikai: ugyanaz a forrás nem-sablon kontextusban
lefordul, sablonban viszont nem; vagy ami rosszabb, csendben a „rossz”
függvényt hívja meg. Gyakorlati tétje akkor van, amikor egy headerben
lévő generikus segédfüggvény egy projekt-specifikus swap-et
vagy operator<<-t szeretne megtalálni, és nem találja
meg — miközben a kód tökéletesen néz ki.
Egy név (name) dependent (függő), ha a jelentése — nem a
léte, hanem az, hogy melyik entitást jelöli — legalább egy template
paramétertől függ. Klasszikus példa: T::value_type, ahol
T típusparaméter, vagy f(x), ahol
x típusa függ egy paramétertől. Egy név
nondependent, ha a jelentése a sablon definíciójának helyén
eldönthető: std::size_t, ::open,
obj.field, ha obj típusa nem függ
paramétertől.
A megkülönböztetés feltétele, hogy a sablon-definíció és a példányosítás két külön kontextus. A szabvány ezért két pontot nevez meg: a definition context a sablon definíciójának helye, az instantiation context (point of instantiation) az a hely, ahol a sablon ténylegesen példányosul — jellemzően a hívás helye, de implicit példányosításnál a fordítási egység vége is számíthat. A nem dependent neveket a definíció helyén kell feloldani: ekkor még nincs szükség a paraméterre. A dependent neveket a példányosítás helyén, a konkrét argumentumok ismeretében kell feloldani.
Ebből azonnal adódik egy fontos következmény, amely a legtöbb meglepetést okozza: egy sablon belsejében tett nem függő hívás nem látja meg a sablon után deklarált függvényeket. Ez nem fordítóhiba, hanem a nyelv szerződése — a kód szándéka és a nyelv viselkedése tér el egymástól.
A kétfázisú névfeloldás (two-phase lookup) a fenti felosztást fordítja le műveletsorra.
Első fázis (definíció). A fordító értelmezi a sablon szövegét, ellenőrzi a szintaxist, feloldja a nem függő neveket, és a nem függő hívásoknál elvégzi a szokásos túlterhelés-feloldást. A függő hívásoknál megjegyzi a definíció helyén látható jelölteket, de a döntést későbbre halasztja.
Második fázis (példányosítás). A konkrét típusok ismeretében feloldja a függő neveket. Függő argumentumú, minősítetlen függvényhívásnál két forrásból dolgozik: a definíció kontextusában végzett ADL-ből és a példányosítás kontextusában végzett ADL-ből. A közönséges (nem ADL) minősítetlen lookup viszont kizárólag a definíció kontextusában történik meg — a példányosítás helyén nem.
Ez a pont az, amit érdemes szó szerint megjegyezni: a közönséges lookup a definícióhoz kötődik, az ADL viszont a példányosítás helyén is lefut. Az alábbi két példa pontosan ezt a különbséget mutatja meg.
// ch26_adl_instantiation.cpp
#include <cstdio>
template <class T>
void call_f(T value) {
f(value); // függő hívás: ADL a példányosítás helyén
}
struct Widget {};
void f(Widget) { std::puts("f(Widget)"); } // a sablon UTÁN deklarálva
int main() {
call_f(Widget{});
}g++ -std=c++20 -Wall -Wextra -pedantic-errors -o ch26_adl_instantiation ch26_adl_instantiation.cpp
# Clang esetén: clang++ -std=c++20 -Wall -Wextra -pedantic-errors ...A példa lefordul, mert Widget a globális névtérben van,
így a hozzá asszociált névtér a globális névtér, és az ott a
példányosítás idejére már látható f(Widget) ADL-lel
megtalálható. (A kimenet várhatóan f(Widget); ez a cikkben
megadott elvárás, nem mért eredmény.)
A következő változat viszont nem fordul le, mert a
T = int esetben nincs asszociált névtér, amiben az ADL
kereshetne:
// ch26_late_decl_error.cpp -- NEM fordul le (szándékos hibapélda)
template <class T>
void call_helper(T value) {
helper(value); // közönséges lookup: itt semmi; ADL: int-hez nincs névtér
}
void helper(int) {} // a sablon után deklarálva
int main() { call_helper(0); }A diagnosztika jellegzetes alakja: a helper nem
deklarált ebben a hatókörben, és az argumentumfüggő keresés sem talált
deklarációt a példányosítás helyén. Ezt a hibaüzenet-szöveget
illusztrációként idézem, nem mért kimenetként: a pontos megfogalmazás
fordítónként és verzióként változik. Fontos korlát: a kétfázisú lookup
szigorú betartása implementációfüggő, a Microsoft fordító hagyományosan
késleltetett feloldást használ, és csak a /permissive-
kapcsoló mellett viselkedik a szabvány szerint. Ha a kód „az egyik gépen
fordul, a másikon nem”, ez gyakori ok.
A függő minősített név (T::valami) kétértelmű lehet:
jelenthet típust, de jelenthet nem típust (változó, függvény,
enumerátor) is. A nyelv ezért konzervatív szabályt alkalmaz: a függő
minősített nevet alapértelmezésben nem-típusnak tekinti. Ha típus, a
typename kulcsszó kötelező — ez a typename
disambiguator.
// ch26_typename.cpp
#include <cstdio>
#include <vector>
template <class Container>
typename Container::value_type first_of(const Container& c) {
using value_type = typename Container::value_type;
value_type v = *c.begin();
return v;
}
int main() {
std::vector<int> v{7, 8};
std::printf("%d\n", first_of(v));
}g++ -std=c++20 -Wall -Wextra -pedantic-errors -o ch26_typename ch26_typename.cppA C++20 (P0634) azokban a kontextusokban tette elhagyhatóvá a
typename-et, ahol szintaktikailag csak típus állhat: többek
között névtér-szintű függvénydeklaráció visszatérési típusában,
alias-deklarációban (using X = ...;), tagdeklarációban,
záró visszatérési típusban és sablonparaméter alapértékében. A fenti
példa mindkét typename-je ilyen helyen áll, ezért C++20-ban
mindkettő elhagyható; C++17-ben mindkettő kötelező:
// typename_cpp20.cpp — C++20 (P0634): itt mindkét typename elhagyható
#include <cstdio>
#include <vector>
template <class Container>
Container::value_type first_of(const Container& c) { // névtér-szintű deklaráció
using value_type = Container::value_type; // alias-deklaráció
value_type v = *c.begin();
return v;
}
int main() {
std::vector<int> v{7, 8};
std::printf("%d\n", first_of(v));
}Blokk-hatókörű deklarációban viszont a typename
C++20-ban is kötelező, mert ott egy X::y kezdetű utasítás
kifejezés is lehet:
// typename_required.cpp — szándékosan hibás: blokkbeli deklarációban a typename C++20-ban is kell
#include <vector>
template <class Container>
auto first_of(const Container& c) {
Container::value_type v = *c.begin(); // hiba: a függő név itt nem-típusnak számít
return v;
}
int main() {
std::vector<int> v{7, 8};
return first_of(v);
}Buildjegyzőkönyv (lab-gcc profil). A
typename_cpp20.cpp
g++ -std=c++20 -Wall -Wextra -pedantic-errors alatt fordul
és 7-et ír ki; -std=c++17 alatt
need 'typename' before 'Container::value_type' because 'Container' is a dependent scope
hibát ad. A typename_required.cpp -std=c++20
alatt is ugyanezt a hibát adja (Clang 22:
missing 'typename' prior to dependent type name).
Hordozható, C++17-tel is forduló kódban a typename-et
továbbra is kiírjuk.
A második disambiguator a template. Ha egy függő típus
tagtemplate-jét szeretnénk paraméterezni, a <
jelet a fordító kisebb-jelnek is olvashatja. Ilyenkor a tag neve elé
template kerül, hogy a szándék egyértelmű legyen.
// ch26_template_kw.cpp
template <class T>
struct Registry {
template <class U>
static constexpr int id() { return static_cast<int>(sizeof(U)); }
};
template <class T>
constexpr int double_id() {
return Registry<T>::template id<double>(); // 'template' nélkül parse-hiba
}
static_assert(double_id<int>() == sizeof(double));g++ -std=c++20 -Wall -Wextra -pedantic-errors -c ch26_template_kw.cppA template kulcsszó itt nem stílus, hanem a nyelvtan
része: nélküle a < összehasonlító operátorként indul, és
a kifejezés szintaktikailag értelmezhetetlenné válik.
A harmadik, gyakran kellemetlen eset a függő bázis. Ha a bázis típusa függ egy paramétertől, a fordító nem tudhatja, milyen tagokat örököl a leszármazott, ezért a bázis tagjait a minősítetlen lookup nem veszi figyelembe.
// ch26_dependent_base.cpp
template <class T>
struct Base {
int value = 0;
void reset() { value = 0; }
};
template <class T>
struct Derived : Base<T> {
void bump() {
// value = value + 1; // hiba: a függő bázis tagja nem látszik
this->value = this->value + 1;
this->reset();
}
using Base<T>::reset; // alternatíva: név behozatala a hatókörbe
};A javításnak három egyenértékű formája van:
this->tag, Base<T>::tag, illetve
using Base<T>::tag;. Mindhárom azt mondja a
fordítónak, hogy a nevet ne a definíció helyén, hanem a példányosítás
után, a konkrét bázisban keresse. A this-> alak a
legolvashatóbb, és a függőséget is dokumentálja.
Az ADL (argument-dependent lookup, más néven Koenig-lookup) szabálya:
minősítetlen függvényhívásnál a szokásos lookup mellett a hívás
argumentumtípusaihoz asszociált névterekben és osztályokban is
keresünk. Asszociált entitás a class típus maga, annak körülvevő
névterei, bázisosztályai, valamint a template argumentumai által hozott
névterek és osztályok. Pointer, referencia és tömb esetén a hivatkozott
típus számít. Az int, double és a többi
alapvető típus esetén viszont nincs asszociált névtér — innen ered a
fenti hibapélda.
Ennek legismertebb következménye a hidden friend: az osztály
belsejében friend kulcsszóval definiált függvény nem
látható a közönséges minősítetlen lookup számára, kizárólag ADL-lel
található meg.
// ch26_hidden_friend.cpp
#include <cstdio>
namespace geo {
class Point {
public:
Point() = default;
Point(int x, int y) : x_(x), y_(y) {}
friend bool operator==(const Point& a, const Point& b) {
return a.x_ == b.x_ && a.y_ == b.y_;
}
private:
int x_ = 0, y_ = 0;
};
} // namespace geo
int main() {
geo::Point a{1, 2}, b{1, 2};
std::printf("%d\n", a == b); // ADL megtalálja
// std::printf("%d\n", operator==(a, b)); // hiba: közönséges lookup nem látja
}A hidden friend előnye, hogy hozzáfér a privát tagokhoz, mégsem
szennyezi a környező névtér túlterhelési halmazát: csak akkor kerül elő,
ha valaki tényleg geo::Point argumentummal hívja.
A legtöbbet idézett gyakorlati eset a swap. A cél:
generikus kódban a típus saját, hatékony swap-jét
használjuk, ha van, különben a std::swap általános,
mozgatásalapú változatát. A helyes minta:
// ch26_swap_adl.cpp
#include <cstdio>
#include <utility>
namespace lab {
class Pinned {
public:
explicit Pinned(int id) noexcept : id_(id) {}
Pinned(const Pinned&) = delete;
Pinned& operator=(const Pinned&) = delete;
int id() const noexcept { return id_; }
friend void swap(Pinned& a, Pinned& b) noexcept {
std::swap(a.id_, b.id_);
}
private:
int id_;
};
} // namespace lab
template <class T>
void exchange(T& a, T& b) {
using std::swap; // közönséges lookup: std::swap látható a definíció helyén
swap(a, b); // ADL: lab::swap megtalálható a példányosítás helyén
}
int main() {
lab::Pinned a{1}, b{2};
exchange(a, b);
std::printf("%d %d\n", a.id(), b.id()); // várt: 2 1
}g++ -std=c++20 -Wall -Wextra -pedantic-errors -o ch26_swap_adl ch26_swap_adl.cppHárom dolog működik egyszerre. A using std::swap;
deklaráció a definíció helyén láthatóvá teszi a generikus
std::swap-et, tehát a közönséges lookup ad egy használható
jelöltet. A swap(a, b) argumentumai függőek, ezért a
példányosítás helyén ADL is lefut, és megtalálja a
lab::swap-et. A Pinned másolható és mozgatható
sem (törölt műveletek), így a std::swap C++20-ban a
is_move_constructible_v && is_move_assignable_v
megszorítás miatt ki sem kerül a jelöltek közé, a nem-sablon
lab::swap pedig amúgy is elsőbbséget élvezne a sablonnal
szemben.
A hibás változat minősített hívást használ:
template <class T>
void exchange_bad(T& a, T& b) {
std::swap(a, b); // csak std::swap: ADL nincs, Pinned-del nem fordul le
}A minősített névnél nincs ADL, ezért a felhasználói túlterhelés soha
nem kerül szóba. A második gyakori hiba a felhasználói swap
rossz névtérbe tétele: ha a swap a típus névterén kívül
van, az ADL nem találja meg, még akkor sem, ha a hívás előtt
deklarálták.
Két szerződésmegjegyzés. Program-definiált típusra a standard library
azt várja el, hogy a swap a típushoz asszociált névtérben,
nem-tag függvényként legyen elérhető; a std névtérbe új
túlterhelést tenni nem megengedett. Specializáció megengedett, de a
hordozható minta az ADL-lel megtalálható túlterhelés. (A
swap-követelményt a C++20 library-szerződés írja le; ebben
a fejezetben nem áll rendelkezésre ellenőrzött forrásszöveg a pontos
paragrafus idézéséhez.)
Az első hiba a hiányzó typename függő minősített típus
előtt; a javítás nem typedef, hanem a kulcsszó kiírása. A
második a hiányzó template tagtemplate-hívásnál; itt a
< és > párosítása is elromolhat, ezért a
hibaüzenet félrevezető lehet. A harmadik a függő bázis tagjának
minősítetlen használata; a javítás this->,
Base<T>:: vagy using. A negyedik a „majd
később deklarálom” feltételezés: nem függő hívásnál ez sosem működik,
függő hívásnál is csak akkor, ha az argumentum típusához asszociált
névtérben történik a deklaráció. Az ötödik a
std::swap(a, b) minősített hívás generikus kódban. A
hatodik a hidden friend közönséges névvel való hívása
(operator==(a, b) alakban), ami definíció szerint nem
található meg.
A fejezet állításai a C++20 nyelvi és library-szemantikára épülnek,
de ebben a részben nem állt rendelkezésre ellenőrzött elsődleges
forrásszöveg, ezért paragrafus szintű hivatkozást nem adok. A konkrét
diagnosztikai szövegek illusztrációk. A kétfázisú lookup betartása és a
typename elhagyásának C++20-as engedélyezése fordítótól és
verziótól függ; a laborprofilban rögzíteni kell a fordítót, a verziót és
a szigorú módot (-pedantic-errors, MSVC-nél
/permissive-).
A sablonokat a fordító két fázisban dolgozza fel. A nem függő neveket
a definíció helyén oldja fel, ezért a később deklarált függvények nem
számítanak. A függő neveket a példányosítás helyén oldja fel;
minősítetlen függvényhívásnál a közönséges lookup a definícióhoz, az ADL
viszont a definícióhoz és a példányosításhoz is kötődik. A
typename és a template disambiguator a függő
minősített nevek kétértelműségét oldja fel. A függő bázis tagjait
this->, Base<T>:: vagy
using viszi be a hatókörbe. A hidden friend és a
felhasználói swap az ADL-lel válik elérhetővé, ezért a
generikus kód using std::swap; swap(a, b); formát
használ.
1. feladat (magyarázó). Állapítsd meg mindkét kódrészletről, hogy lefordul-e C++20-ban, és indokold a kétfázisú lookup szabályaival!
// A
template <class T> void use(T t) { pick(t); }
void pick(int);
int main() { use(0); }// B
namespace n { struct X {}; }
template <class T> void use(T t) { pick(t); }
namespace n { void pick(X); }
int main() { use(n::X{}); }2. feladat (kódolás). Az alábbi sablon több helyen nem fordul le. Javítsd minimális beavatkozással, és minden javításhoz nevezd meg a szabályt!
template <class C>
struct Holder : C {
C::value_type get() const { return C::get_value(); }
template <class U> static int code() { return sizeof(U); }
};
template <class C>
int run(const Holder<C>& h) {
auto v = h.get();
(void)v;
return Holder<C>::code<int>();
}3. feladat (diagnosztika/tervezés). Adott egy nem
másolható, nem mozgatható Resource osztály, amelynek van
egy nem-tag swap-je a saját névterében. Írj generikus
exchange sablont, amely ezt helyesen használja, majd
magyarázd meg, miért nem fordul le ugyanez std::swap(a, b)
minősített hívással. Adj ellenőrzési feltételt!
1. megoldás. Az A nem fordul le. A
pick(t) függő hívás; a közönséges lookup a definíció helyén
nem talál semmit, mert a pick(int) később van deklarálva. A
példányosításkor az ADL fut le, de T = int-hez nem tartozik
asszociált névtér, így nincs jelölt. A B lefordul: a
pick(t) hívás argumentuma n::X, tehát az
asszociált névtér az n, és a példányosítás idejére az
n::pick(X) már látható. Ellenőrzés: az A-ra a fordító hibát
ad, a B-re nem; a B várható viselkedése a hívás sikeres feloldása.
2. megoldás. Négy javítás kell. (a)
C::value_type helyett typename C::value_type,
mert függő minősített név típuspozícióban. (b) A
C::get_value() hívásnál a függő bázis tagja nem látszik;
this->get_value() (vagy C::get_value())
kell. (c) Holder<C>::code<int>() helyett
Holder<C>::template code<int>(), mert függő
típus tagtemplate-jét paraméterezzük. (d) A get()
visszatérési típusa szintén typename C::value_type. A
javított mag:
template <class C>
struct Holder : C {
typename C::value_type get() const { return this->get_value(); }
template <class U> static int code() { return static_cast<int>(sizeof(U)); }
};
template <class C>
int run(const Holder<C>& h) {
auto v = h.get();
(void)v;
return Holder<C>::template code<int>();
}Ellenőrzés: a typename törlése, illetve a
template elhagyása újra hibát okoz, tehát mindkét kulcsszó
tényleg szükséges; a this-> törlése szintén.
3. megoldás. A Resource nem mozgatható,
ezért a std::swap sablon nem alkalmazható rá; a saját
swap ADL-lel érhető el.
// ch26_ex3.cpp
#include <cstdio>
#include <utility>
namespace res {
class Resource {
public:
explicit Resource(int id) noexcept : id_(id) {}
Resource(const Resource&) = delete;
Resource& operator=(const Resource&) = delete;
int id() const noexcept { return id_; }
friend void swap(Resource& a, Resource& b) noexcept {
std::swap(a.id_, b.id_);
}
private:
int id_;
};
} // namespace res
template <class T>
void exchange(T& a, T& b) {
using std::swap; // generikus tartalék a definíció helyén
swap(a, b); // ADL a példányosítás helyén: res::swap
}
int main() {
res::Resource a{1}, b{2};
exchange(a, b);
std::printf("%d %d\n", a.id(), b.id()); // várt: 2 1
}g++ -std=c++20 -Wall -Wextra -pedantic-errors -o ch26_ex3 ch26_ex3.cppA std::swap(a, b) minősített változat azért hibás, mert
minősített névnél nincs ADL: csak a std::swap sablon jön
szóba, amely C++20-ban a move-követelmények miatt kikerül a jelöltek
közül, illetve C++17-ben példányosításkor hibázna. A swap a
res névtéren kívül nem található meg, ezért a
névtér-elhelyezés is szerződéselem. Ellenőrzési feltétel: a fenti
program lefordul és 2 1-et ír ki; ha a swap-et
kivisszük a globális névtérbe, a kód továbbra is működik (a globális
névtér is asszociált), de ha a res névtéren kívül egy
másik névtérbe tesszük, a fordítás megszűnik.