A perfect forwarding célja, hogy egy argumentumot úgy adjunk tovább
egy hívási láncon, hogy a végső célfüggvény pontosan ugyanazt a
value category-t (lvalue/rvalue) és ugyanazokat a
cv-minősítőket (const/volatile) lássa, mintha a hívó közvetlenül őt
hívta volna. A téma látszólag deklarációs trükk, valójában
értékszemantikai szerződés: könyvtári factory-k, emplace és
wrapper API-k mind erre épülnek, és a félreértése néma másolást,
teljesítményvesztést vagy épp nem szándékolt átvételt okoz.
Ez az alfejezet a Függvénysablon, instantiation
és deduction (26.1) template-dedukciójára és a Move constructor, moved-from állapot és std::move
(22.3) értékkategória-kezelésére épül. A std::move ott
az “erőforrás átadásának jelzése” volt; itt élesen elválasztjuk a
std::forward “változatlan továbbadás” szándékától. A C++20
alapot használjuk, a requires-szel megfogalmazott
konstraint csak említés szintjén jelenik meg, mert részletes kezelése a
Concepts, requires és constrained overload
(27.2) témája.
Tegyük fel, hogy van két sink overloadunk, egy lvalue-re
és egy rvalue-re, és szeretnénk egy-egy köztes függvényt, amely csak
továbbad. Naiv megoldás:
void sink(int&);
void sink(int&&);
void relay_lvalue(int& x) { sink(x); } // OK: lvalue marad
void relay_rvalue(int&& x) { sink(x); } // HIBA: x maga lvalue!A második függvényben x deklarált típusa
int&&, de mivel nevesített változó, value
category-je lvalue, ezért a sink(int&) hívódik.
Javíthatjuk sink(std::move(x))-szel, de akkor a két
függvényre ketté kell választani — és N paraméter, illetve
minden cv-kombináció esetén ez nem skálázódik. Kell egy sablon, amely
megőrzi az argumentum értékkategóriáját.
Tekintsük a kanonikus alakot:
template<class T>
void f(T&& p);Ez forwarding reference (köznyelvi, Scott
Meyers-féle “universal reference”; a szabvány hivatalosan nem használja
ezt a nevet, csak a T&& dedukciós alakot írja le).
A dedukció két esetre bomlik, és ehhez kell a reference
collapsing szabály:
Mnemotechnika: ha bármelyik oldalon & szerepel, az
eredmény &; csak a && &&
marad &&.
Lvalue argumentum U típusú (esetleg
const U, volatile U): a dedukált
T U& lesz (a cv megmarad,
tehát const U&), azaz a T&&
paraméter típusa U& && → collapse →
U&.
Rvalue argumentum U típusú: a dedukált
T U, a paraméter típusa
U&&.
Numerikusan, int i; const int ci = 0; mellett:
| Hívás | Argumentum kategóriája | Dedukált T |
p típusa |
a p névkifejezés |
std::forward<T>(p) |
|---|---|---|---|---|---|
f(i) |
lvalue | int& |
int& |
lvalue | lvalue (int&) |
f(ci) |
lvalue (const) |
const int& |
const int& |
lvalue | lvalue (const int&) |
f(42) |
prvalue | int |
int&& |
lvalue | xvalue (int&&) |
f(std::move(i)) |
xvalue | int |
int&& |
lvalue | xvalue (int&&) |
Ez a táblázat a forwarding reference egész lényege. A p
név mint kifejezés minden sorban lvalue — egy névvel rendelkező
változó mindig lvalue, akkor is, ha a típusa int&&.
Az eredeti argumentum értékkategóriáját a dedukált T őrzi
meg, és a std::forward<T>(p) állítja vissza: lvalue
argumentumra lvalue-t, rvalue argumentumra xvalue-t ad.
// fwd_category.cpp — C++20: a p név mindig lvalue, a forward visszaadja az eredetit
#include <type_traits>
#include <utility>
template<class T>
constexpr int f(T&& p) {
static_assert(std::is_lvalue_reference_v<decltype((p))>); // p: mindig lvalue
using Fwd = decltype(std::forward<T>(p));
if constexpr (std::is_lvalue_reference_v<T>) {
static_assert(std::is_lvalue_reference_v<Fwd>); // forward: lvalue
return 1;
} else {
static_assert(std::is_rvalue_reference_v<Fwd>); // forward: xvalue
return 2;
}
}
int main() {
int i = 0; const int ci = 0;
static_assert(f(i) == 1 && f(ci) == 1 && f(42) == 2 && f(std::move(i)) == 2);
}Buildjegyzőkönyv (lab-gcc profil).
g++ -std=c++20 -Wall -Wextra -Wpedantic fwd_category.cpp
figyelmeztetés nélkül fordul: mind a négy hívásnál teljesül a
decltype((p)) lvalue-referencia volta, és a
std::forward<T>(p) lvalue-, illetve rvalue-referencia
típusa a táblázat szerinti.
A “forwarding reference” nem minden T&&. Négy
feltételnek egyszerre kell teljesülnie:
T (vagy más néven a paramétertípus) template
paramétere egy függvénysablonnak — nem egy
osztálysablon tagjának fix paramétere —, vagy auto
dedukciós kontextusban szerepel.T&&, azaz
cv nélküli T és && —
const T&&, volatile T&& nem
forwarding.f<int>(x) esetén
T = int, p típusa int&&,
nem forwarding).template<class U> void g(U&&);), nem az
osztály T paramétere.Ellenpéldák, hogy a különbség éles legyen:
void h1(int&&); // konkrét típus, csak rvalue-t fogad, nincs dedukció
template<class T>
void h2(const T&&); // NEM forwarding: lvalue nem köthető rá
template<class T>
struct Box {
void m1(T&&); // NEM forwarding: T az osztály paramétere
template<class U>
void m2(U&&); // forwarding: U a tag template paramétere
};
template<class T>
void h3(T&& t) { /* forwarding */ }
auto&& x = produce(); // forwarding reference (auto kontextusban)h2 esetén h2(i) (lvalue int)
fordítási hiba: a T&& alakú forwarding csak úgy tud
lvalue-t fogadni, hogy a T maga &-t
tartalmaz. Ha a const már a paraméter szintjén ott van, a
T&& nem tud &-t “nyelni”.
A std::forward<T>(t) feltételes kaszt:
// egyszerűsített vázlat, C++14 utáni alak
template<class T>
constexpr T&& forward(std::remove_reference_t<T>& t) noexcept {
return static_cast<T&&>(t);
}Ha T = U&, akkor T&& = U&,
a visszaadott referenciatípus U& →
lvalue marad. Ha T = U, akkor
T&& = U&& → rvalue lesz. A
std::forward tehát nem “mozgat”, hanem visszaadja
azt a value category-t, amit a T argumentum
kódol.
Ezzel szemben a std::move(t) mindig
static_cast<remove_reference_t<decltype(t)>&&>(t)
— feltétel nélkül rvalue-t csinál. A két függvény közötti választás nem
stílus, hanem szándék:
std::forward<T>: “nem tudom, mit
kaptam, add tovább változatlanul” — pass-through wrapper, factory,
relay.std::move: “én birtoklom ezt az
objektumot, és most átadom” — owner oldali kifejezés, akkor is, ha a
típus forwarding reference.Helyes használat:
#include <iostream>
#include <utility>
struct Tracer {
Tracer() = default;
Tracer(const Tracer&) { std::cout << "copy\n"; }
Tracer(Tracer&&) noexcept { std::cout << "move\n"; }
};
void consume(const Tracer&) { std::cout << "consume(const&)\n"; }
void consume(Tracer&&) { std::cout << "consume(&&)\n"; }
template<class T>
void relay(T&& t) {
std::cout << "relay: ";
consume(std::forward<T>(t)); // értékkategória megőrzése
}
template<class T>
void relay_bad(T&& t) {
std::cout << "relay_bad: ";
consume(std::move(t)); // mindig rvalue: ellopná az lvalue-t is
}
int main() {
Tracer t;
const Tracer ct;
relay(t); // T = Tracer& -> consume(const&)
relay(ct); // T = const Tracer& -> consume(const&)
relay(Tracer{}); // T = Tracer -> consume(&&)
relay_bad(t); // T = Tracer&, de move -> consume(&&): hibás szándék
}A fenti program a szerző szándéka szerint a jelzett három sort írja
ki a relay_bad hibás sorával együtt
(consume(const&), consume(const&),
consume(&&), majd
consume(&&)); a kód nem futtatott
és nem tesztelt, a kimenet csak elvárt illusztráció. A
fordítás tipikus parancsa:
g++ -std=c++20 -Wall -Wextra -pedantic-errors -o relay relay.cpp
# vagy
clang++ -std=c++20 -Wall -Wextra -pedantic-errors -o relay relay.cppKritikus pont: a std::forward<T> argumentumának
pontosan a dedukált T-nek kell lennie. Ha
std::forward<U>-t írunk olyan U-val,
amely nem a p dedukált típusa, a kaszt rossz
értékkategóriát állít elő — ez a leggyakoribb “majdnem helyes” hiba.
A std::make_unique, std::make_shared és
emplace_back mind forwarding reference-re épül. Az
emplace_back variadikus forwarding: az argumentumok
Args&&... args alakban érkeznek, és a tároló a
célobjektum helyén hívja meg a konstruktort.
#include <vector>
#include <string>
#include <iostream>
struct Widget {
std::string name;
int value;
Widget(std::string n, int v) : name(std::move(n)), value(v) {
std::cout << "ctor " << name << '=' << value << '\n';
}
Widget(const Widget& o) : name(o.name), value(o.value) {
std::cout << "copy " << name << '\n';
}
Widget(Widget&& o) noexcept : name(std::move(o.name)), value(o.value) {
std::cout << "move " << name << '\n';
}
};
int main() {
std::vector<Widget> v;
v.reserve(8);
std::string s = "alpha";
v.emplace_back(s, 1); // s lvalue -> string copy
v.emplace_back(std::move(s), 2); // s rvalue -> string move
v.emplace_back("gamma", 3); // const char* -> string(ptr)
}Itt az emplace_back az Args vektorát
ugyanúgy dedukálja, mint bármelyik függvénysablon: s lvalue
-> Args = {std::string&, int}, a forward az
std::string&-et továbbítja, a Widget
konstruktora másol. std::move(s) esetén
Args = {std::string, int}, a forward rvalue-t ad, és a
Widget std::string tagja move-ol. A viselkedés
Tracer-szerű nyomkövetéssel ellenőrizhető; a kimenet itt is
elvárt, nem mért.
A legravaszabb csapda: egy
template<class T> X(T&&) konstruktor
versenybe száll a copy/move konstruktorral, és lvalue
esetben nyerhet ellene. Ez azért van, mert a const X&
és az X& között a kötés minősége eltér:
X&-re X& identity,
const X&-re const X&
qualification conversion, és az identity jobb.
Nem-template vs template esetén pedig a nem-template nyer, de csak
azonos minőségű kötéseknél.
#include <iostream>
#include <utility>
class Node {
public:
int tag = 0;
Node() = default;
template<class T>
Node(T&&) : tag(1) { std::cout << "template ctor\n"; }
Node(const Node&) : tag(2) { std::cout << "copy ctor\n"; }
Node(Node&&) noexcept : tag(3) { std::cout << "move ctor\n"; }
};
int main() {
Node a;
Node b(a); // template ctor nyer: T = Node&, Node& pontosabb, mint const Node&
const Node c;
Node d(c); // copy ctor nyer: nem-template döntetlenben
Node e(std::move(a)); // move ctor nyer: nem-template döntetlenben
std::cout << b.tag << ' ' << d.tag << ' ' << e.tag << '\n'; // elvárt: 1 2 3
}A hiba lényege: Node b(a) esetén a
template<class T> Node(T&&)
T = Node&-del identity kötést ad, és a copy
constructort megelőzi. Ezért a C++20 előtti kódok tipikusan
std::enable_if-fel kizárták az azonos típust
(!std::is_same_v<std::decay_t<T>, Node>),
C++20-ban a requires a természetes eszköz:
template<class T>
requires (!std::same_as<std::remove_cvref_t<T>, Node>)
Node(T&&) : tag(1) { /* ... */ }A requires-szel kapcsolatos részletek a Concepts, requires és constrained overload (27.2)
témához tartoznak; itt csak a diagnosztikai minta a fontos: ha egy
forwarding konstruktor unexpected módon nyer, az első kérdés mindig az,
hogy kizártuk-e a saját típust.
A forwarding reference nem csodaszer; három tipikus esetben a dedukció egyszerűen nem tud végbemenni, mert nincs dedukálható típus:
#include <initializer_list>
struct S { int b : 4; };
void g(int);
void g(double);
template<class T> void f(T&&);
S s;
f(s.b); // HIBA: bit-field nem köthető referenciához
f(int(s.b)); // OK: ideiglenes int
f({1, 2, 3}); // HIBA: braced-init-list nem típus
f<std::initializer_list<int>>({1, 2, 3}); // OK: explicit template argumentum
f(g); // HIBA: overload set nem egy típus
f(static_cast<void(*)(int)>(g)); // OK: konkrét függvénypointerA braced-init-list {1,2,3} csak akkor vihető át, ha a
cél típusa ismert — emiatt emplace_back({1,2,3}) sem
működik, még akkor sem, ha a cél
Widget{ std::initializer_list<int> }-et várna. Az
overload set neve nem típus; a void g(int) és
void g(double) között a dedukció nem tud választani, ezért
kell cast vagy +g jellegű kényszerítés (bár a
+g overload set esetén is csak akkor működik, ha pontosan
egy g fordul elő, mert az unáris + is
túlterhelés-feloldást végez). A bit-field nem rendelkezik címmel, így
referencia nem köthető hozzá — ilyenkor kényszerített másolás
(int(s.b)) a helyes válasz.
std::forward<T> helyett
std::move. Egy relay wrapperben a
std::move néma rvalue-konverziót okoz, és lvalue
argumentumot is “ellop” a hívó elől.std::forward<T> és std::move
felcserélése az owner oldalon. A
std::forward<T>(member) ott, ahol a hívó szándéka az
átadás, csak látszólag helyes: az T szerint viselkedik,
miközben a member sose volt forward referenciánk.const T&& vagy konkrét
T&& összetévesztése a forwarding
reference-szel. A const T&& nem fogad el
lvalue-t, a konkrét int&& pedig egyáltalán nem
dedukál.T kétszeri dedukálása.
template<class T> void f(T&& a, T&& b)
esetén a két argumentumnak ugyanazt a T-t kell dedukálnia,
így lvalue + rvalue pár nem megy át.std::forward nem a T-vel.
std::forward<SomeOtherType>(p) csendben rossz
értékkategóriát ad.1. feladat (dedukciós tábla). Egészítse ki a
táblázatot a template<class T> void f(T&& p);
sablonhoz az alábbi hívásokra:
int i; const int ci; volatile int vi; int&& ri = std::move(i);
esetén f(i), f(ci), f(vi),
f(ri), f(std::move(ci)). Adja meg a dedukált
T-t, a p típusát és value category-jét,
indokolja a reference collapsing lépést.
2. feladat (relay implementálása). Írjon
template<class T> void relay(T&& x)
wrapper-t, amely a consume(const Tracer&) /
consume(Tracer&&) overload párra továbbítja az
argumentumot. Készítsen tesztet lvalue, const lvalue és rvalue esetre,
és ellenőrizze a kiválasztott overloadot. Mutassa meg a
std::move-os hibaváltozatot, és magyarázza el, miért
rossz.
3. feladat (greedy diagnózis). Adott a
Node osztály, amelynek forwarding konstruktora van. Írjon
egy hívási sorozatot, amelyben a forwarding konstruktor elnyeli
a copy constructort, majd javítsa a kódot C++20
requires-szel. Adjon egy ellenőrzési feltételt: milyen
hívásra melyik konstruktor fut, és miért.
| Hívás | T |
p típusa |
p value category |
|---|---|---|---|
f(i) |
int& |
int& |
lvalue |
f(ci) |
const int& |
const int& |
const lvalue |
f(vi) |
volatile int& |
volatile int& |
volatile lvalue |
f(ri) |
int& |
int& |
lvalue (nevesített!) |
f(std::move(ci)) |
const int |
const int&& |
const rvalue |
Indoklás: f(ri) esetén ri deklarált típusa
int&&, de nevesített, ezért value
category-je lvalue, így T = int&, és a collapse után a
p típusa int& — nem
int&&. Az utolsó esetben a
std::move(ci) const int&&-t ad, a
dedukció T = const int, a paraméter
const int&&.
#include <iostream>
#include <utility>
struct Tracer {
Tracer() = default;
Tracer(const Tracer&) { std::cout << "copy\n"; }
Tracer(Tracer&&) noexcept { std::cout << "move\n"; }
};
void consume(const Tracer&) { std::cout << "consume(const&)\n"; }
void consume(Tracer&&) { std::cout << "consume(&&)\n"; }
template<class T>
void relay(T&& x) {
consume(std::forward<T>(x));
}
template<class T>
void relay_bad(T&& x) {
consume(std::move(x)); // mindig rvalue
}
int main() {
Tracer t;
const Tracer ct;
relay(t); // consume(const&) – T = Tracer&
relay(ct); // consume(const&) – T = const Tracer&
relay(Tracer{}); // consume(&&) – T = Tracer
relay_bad(t); // consume(&&) – de t-t elloptuk volna
}Ellenőrzési feltétel: relay(t) és relay(ct)
sora consume(const&), relay(Tracer{}) sora
consume(&&). Ha bármelyik korábbi sor
consume(&&)-t ad, a T nem a dedukált
típus vagy hibás a forward. relay_bad(t) bizonyítja, hogy a
std::move minden lvalue-t rvalue-ként továbbít — ez a
wrapperben néma másolás-eltörlés (esetleg adatvesztés) felé vezet.
#include <iostream>
#include <utility>
class Node {
public:
int tag = 0;
Node() = default;
template<class T>
requires (!std::same_as<std::remove_cvref_t<T>, Node>)
Node(T&&) : tag(1) { std::cout << "template\n"; }
Node(const Node&) : tag(2) { std::cout << "copy\n"; }
Node(Node&&) noexcept : tag(3) { std::cout << "move\n"; }
};
int main() {
Node a;
Node b(a); // copy ctor (constraint kizárja a template-et) -> tag 2
Node d(std::move(a)); // move ctor -> tag 3
Node e(42); // template ctor -> tag 1
std::cout << b.tag << ' ' << d.tag << ' ' << e.tag << '\n'; // elvárt: 2 3 1
}A
requires (!std::same_as<std::remove_cvref_t<T>, Node>)
konstraint kizárja, hogy a forwarding konstruktor bármely cv/ref
kombinációjú Node-ra versenyezzen. A
remove_cvref_t C++20-ban elérhető, az
std::same_as szintén. A Node e(42) hívás
mutatja, hogy a template továbbra is fogad idegen típust — a konstraint
nem tiltja le a forwardingot, csak a saját típusra zárja ki.
A forwarding reference a T&& azon alakja, ahol
T függvénysablon-paraméter, cv nélküli, és tényleges
dedukció történik. Lvalue argumentumra T = U&,
rvalue-ra T = U; a reference collapsing biztosítja, hogy a
paraméter pontosan az eredeti értékkategóriát és cv-jét tükrözze. A
std::forward<T> ennek a value category-nek a
továbbadása, a std::move feltétel nélküli rvalue-konverzió
— a kettő nem ekvivalens, és a wrapper/factory mintákban a forward a
helyes eszköz, a move az owner oldal eszköze. A
const T&&, int&& konkrét
alakok és az osztálysablon fix T-je nem forwarding
reference-ek. A braced-init-list, overload set és bit-field nem
dedukálható, ezekhez célzott kényszerítés vagy explicit template
argumentum kell. A greedy forwarding konstruktor kizárja magát a saját
típusra, C++20-ban tipikusan requires-szel; a részletek a
concepts fejezetben következnek.