26.5 Dependent name, kétfázisú lookup és ADL

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.

26.5.1 Dependent és nem dependent nevek

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.

26.5.2 Kétfázisú lookup: mi történik mikor

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.

26.5.3 typename és template: a két disambiguator

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

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

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

26.5.4 Függő bázisosztály és a this-> használat

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.

26.5.5 Argument-dependent lookup és a hidden friend

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.

26.5.6 A swap customization minta

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

Há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.)

26.5.7 Tipikus hibák

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.

26.5.7.1 Ellenőrzési korlátok és verziófüggés

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

26.5.8 Összefoglaló

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.

26.5.9 Feladatok

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!

26.5.10 Megoldások

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

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