A C-ben megszokott érték-visszaadás mindig másolás: a
return s; egy struct esetén bájtonként
átmásolja a struktúra tartalmát a hívó által biztosított helyre, és ha a
struktúra pointert tartalmaz, a mutatott erőforrásból csak egy második,
nem tulajdonló hivatkozás keletkezik. A C++ értékszemantikája ezt a
másolást megőrzi szemantikai szinten — a T értékű
visszatérés azt jelenti, hogy a hívó egy önálló, teljes értékű
T-t kap —, de a fordítónak megengedi, hogy a köztes
másolásokat elhagyja. Ez a fejezet arról szól, melyik elhagyás
garantált, melyik csak megengedett, és hogyan lehet
ezt a különbséget helyesen kihasználni, illetve hogyan lehet elrontani
egyetlen felesleges std::move-val. Az előfeltétel a move
konstruktor és a moved-from állapot ismerete: Move
constructor, moved-from állapot és std::move (22.3).
A gyakorlati tétel nem kicsi. Egy konfigurációs objektumot, egy
std::vector<uint8_t> puffert vagy egy
std::string-et visszaadó függvény a program meleg útján
lehet; a visszatérési érték költsége a naiv modellben három
konstruktor/destruktor-hívás, a garantált elision mellett egy. A fejezet
ezért két síkot választ szét végig: a szabvány szerinti
garanciát (mi történhet meg egyáltalán) és a megfigyelést
(mit látunk egy adott fordítónál, adott kapcsolókkal). A kettő nem
ugyanaz, és a különbségtétel elmaradása a leggyakoribb félreértés
forrása.
A C++17 előtt a visszatérési érték előállítása három fogalmon
keresztül írható le. A függvény előállít egy lokális objektumot, ez a
lokális a return utasításnál egy temporális
(névtelen ideiglenes) objektummá másolódik, majd a temporális a hívó
által biztosított célobjektummá másolódik (vagy C++11 óta mozog). A
szabvány már C++17 előtt is megengedte, hogy a fordító ezeket a
másolásokat elhagyja — ezt nevezi a szakirodalom copy
elisionnek (másolás-elhagyás) —, de csak megengedte,
nem írta elő. Ráadásul a megengedés feltétele az volt, hogy a copy vagy
move konstruktor elérhető legyen, még akkor is, ha a fordító végül nem
hívta meg.
A prvalue (pure rvalue) C++17-től nem egy „névtelen objektum”, hanem olyan kifejezés, amelynek értékelése egy objektumot inicializál. Amikor egy prvalue-t egy objektum inicializálására használunk, a prvalue közvetlenül inicializálja a célobjektumot; nincs köztes temporális, amit el kellene hagyni. Ez a garantált prvalue-konstrukció (mandatory vagy guaranteed copy elision), és nem optimalizáció: a program szemantikájának része. A C++17 előtti és utáni viselkedés különbsége éppen ezért nem teljesítménykérdés, hanem nyelvi kérdés — és bizonyos programok csak C++17-től fordulnak le.
A garancia a return utasításban akkor áll fenn, ha a
visszaadott kifejezés prvalue, és típusa megegyezik a függvény
visszatérési típusával, a legfelső szintű (top-level)
const/volatile minősítők eltérését leszámítva.
Ilyenkor a prvalue közvetlenül a visszatérési objektumot inicializálja:
nincs temporális, nincs copy, nincs move, és a copy/move konstruktornak
nem is kell léteznie. Ez utóbbi a legfontosabb következmény,
mert olyan típust is vissza lehet adni érték szerint, amely nem
másolható és nem mozgatható.
A tipikus alakok, amelyekre a garancia vonatkozik:
T make_direct() { return T{1, 2}; } // prvalue: garantált
T make_braced() { return {1, 2}; } // a visszatérési típusra célzott inicializálás: garantált
T make_factory() { return make_direct(); } // a belső hívás prvalue-ja: garantáltAz utolsó sor fontos: a make_factory visszatérési értéke
nem egy lokális változó, hanem egy függvényhívás, amelynek eredménye
prvalue, ezért a láncolt visszatérés is garantáltan költségmentes. Ez a
gyárak és a make_unique-szerű segédfüggvények alapja.
Az NRVO (Named Return Value Optimization) ennél
gyengébb: a visszaadott kifejezés egy automatikus tárolási idejű, nem
volatile, nem függvényparaméter nevű lokális változó
neve, és a változó típusa megegyezik a visszatérési típussal (a
top-level cv eltérését itt is leszámítva). Ha a fordító él a
lehetőséggel, a lokális objektumot közvetlenül a hívó által biztosított
tárhelyen építi fel, így a visszatéréskor nincs sem másolás, sem
mozgatás. Ez azonban megengedett, nem kötelező: a szabvány
megengedi, hogy a fordító elhagyja a copy/move konstruktor hívását, de
nem írja elő.
Két következményt érdemes rögzíteni. Egyrészt az NRVO csak akkor
alkalmazható, ha a return utasításban szereplő név minden
végrehajtási útvonalon ugyanazt az objektumot nevezi meg; két különböző
lokális változóból feltételesen visszaadott értéknél a fordítók a
gyakorlatban nem élik vele a lehetőséggel, és ilyenkor move (vagy ha az
nem elérhető, copy) történik. Másrészt mivel az NRVO nem garantált, a
copy vagy move konstruktornak elérhetőnek kell lennie akkor is,
ha a fordító végül nem hívja meg. Ez a látszólagos ellentmondás a C++17
utáni szabály: a prvalue-esetben nem kell konstruktor, a névvel történő
visszatérésnél kell.
Amikor egy függvény érték szerint ad vissza, és a return
kifejezés egy automatikus tárolási idejű objektumot megnevező
azonosító-kifejezés — lokális változót vagy érték szerinti
függvényparamétert —, a kifejezést az overload resolution során
úgy kell kezelni, mintha rvalue lenne, mintha a programozó
std::move-ot írt volna. Ez az implicit
move szabály. C++11 óta létezik, és a paraméterekre kezdettől
vonatkozik; a C++14 (CWG1579) a konverziós esetekre, a C++20 (P1825) az
rvalue-referencia változókra, a throw-ra és a
co_return-ra terjesztette ki; a C++23 (P2266)
egyszerűsítette: az ilyen név a return-ben eleve xvalue
(lásd Érték, élettartam és kifejezések: auto(x),
implicit move és range-for (40.5)).
Két jogosultságot kell szétválasztani:
| Visszaadott név | NRVO lehetséges? | Implicit move? |
|---|---|---|
lokális, nem volatile automatikus objektum, a
visszatérési típussal azonos típus |
igen (nem garantált) | igen |
| érték szerinti függvényparaméter | nem — a paramétert a hívó építi, nem a célhelyen | igen |
T&& lokális rvalue-referencia (C++20 óta) |
nem | igen |
lvalue-referencia paraméter vagy tag (T&,
member_) |
nem | nem — itt std::move kell, ha mozgatni
akarunk |
Az érték szerinti paraméternél tehát a return p; mozgat,
és a std::move felesleges; a referencia-paraméternél a
return r; másolna (move-only típusnál nem is fordul):
// param_move.cpp — C++20: érték szerinti és referencia-paraméter visszaadása
#include <memory>
#include <utility>
std::unique_ptr<int> by_value(std::unique_ptr<int> p) {
return p; // implicit move: fordul, nincs NRVO (paraméter)
}
std::unique_ptr<int> by_ref(std::unique_ptr<int>& r) {
// return r; // hiba: r lvalue-referencia, nincs implicit move
return std::move(r); // itt a std::move szükséges és helyes
}
int main() {
auto a = by_value(std::make_unique<int>(1));
auto src = std::make_unique<int>(2);
auto b = by_ref(src); // src kiürül: ez a hívó számára látható szerződés
return (*a + *b == 3 && !src) ? 0 : 1;
}Buildjegyzőkönyv (lab-gcc profil).
g++ -std=c++20 -Wall -Wextra -Wpedantic param_move.cpp
figyelmeztetés nélkül fordul, és 0 kóddal fut. A kikommentezett
return r; visszakapcsolva C++20 és C++23 módban is
use of deleted function 'std::unique_ptr<...>::unique_ptr(const std::unique_ptr<...>&)'
hibát ad. A by_value törzsét
return std::move(p);-re írva a GCC
-Wredundant-move figyelmeztetést ad.
A gyakorlati haszon: ha az NRVO nem történik meg, a lokális objektum
nem másolódik, hanem mozog. Ez akkor is így van, ha a programozó nem írt
semmit. A return local; tehát „ingyen” megkapja a move-ot,
és közben megőrzi az NRVO esélyét.
return std::move(local)A std::move(local) egy függvényhívás, amelynek eredménye
xvalue — nem azonosító-kifejezés. Ezzel két dolgot rontunk el egyszerre.
Egyrészt az NRVO feltétele megszűnik, mert a visszaadott kifejezés már
nem a lokális változó neve; a fordító nem építheti a lokálist a
célhelyre, hiszen a std::move egy külön objektumra mutató
referenciát ad. Másrészt az implicit move szabályára sincs szükség, mert
a kifejezés már eleve xvalue: a move konstruktor meg fog hívódni. Az
eredmény egy olyan move művelet, amely NRVO esetén nulla lett volna.
Fontos árnyalás, hogy ne essünk a másik végletbe: a
std::move nem mindig káros, csak akkor, ha az NRVO
egyébként megtörtént volna. Ha a visszaadott kifejezés nem implicit
módon mozgatható — például egy tag
(return std::move(member_);) vagy egy lvalue-referencia
paraméter (return std::move(ref);) —, akkor az implicit
move szabály rá nem terjed ki, és a std::move hiányában
copy történne. Ezekben az esetekben a std::move helyes és
szükséges, ha a forrás kiürítése a szerződés része. Az érték szerinti
paraméter viszont nem ilyen: ott a return param;
már mozgat, a std::move(param) csak felesleges. A
megkülönböztetés kulcsa tehát nem a szintaxis, hanem az, hogy a
kifejezés milyen tárolási idővel és milyen névvel rendelkezik.
Jelölje
és
a copy, illetve move konstruktor költségét,
a destruktorét,
pedig a lokális objektum felépítésének költségét. Egy
return local; naiv, elision nélküli költsége:
NRVO esetén a lokális a célhelyen épül fel, ezért a move és a lokális destruktora elmarad:
A return std::move(local); viszont kikényszeríti a
move-ot, még akkor is, ha az NRVO egyébként lehetséges lett volna:
A különbség pontosan
.
Ez kis objektumnál elhanyagolható, nagy puffer vagy drága destruktor
esetén nem az. A std::vector és std::string
esetében a move általában olcsó (pointer-átadás), de a destruktor és a
move konstruktor hívásának elhagyása továbbra is mérhető különbség lehet
szoros ciklusban — és ami fontosabb, a szemantika szempontjából az NRVO
megőrzi az objektum identitását, amit a move nem.
A copy elision nem csak konstruktorokat hagy el, hanem destruktorokat
is, és ez megfigyelhető viselkedést jelent. Garantált
prvalue-konstrukciónál a temporális soha nem jön létre, ezért a
destruktora sem fut le. NRVO esetén a lokális objektum a hívó által
biztosított helyen épül fel, így egyetlen destruktor fut le — a
visszatérési érték élettartamának végén, a hívónál. Ha az elision nem
történik meg, két destruktor fut le: a lokálisé a return
után, a visszatérési értéké a hívónál.
Ebből az következik, hogy a konstruktorokba és destruktorokba írt
oldalhatások (számlálók, logolás, erőforrás-felszabadítás, lockolás)
nem determinisztikusak az opcionális elision esetén. A szabvány
ezt kifejezetten megengedi: a copy elision az as-if szabály
egyik dokumentált kivétele. A program helyességét ezért soha nem szabad
arra építeni, hogy egy copy vagy move konstruktor pontosan hányszor
hívódik meg — legfeljebb diagnosztikai célra szabad ezt megfigyelni, és
akkor is jelezni kell, hogy a megfigyelés fordító-, verzió- és
kapcsolófüggő. Ez a fejezet instrumentált példájának is a fő
tanulsága.
Az alábbi program három visszatérési módot hasonlít össze: garantált
prvalue-t, NRVO-jelöltet és kikényszerített move-ot. A számlálók
statikusak, hogy a main a végén kiírhassa őket; a
konstruktorokban szándékosan nincs oldalhatás, hogy a kimenet ne függjön
attól, melyik konstruktor hívódik meg.
// counter.cpp
#include <cstdio>
#include <utility>
struct Tracked {
static int constructed;
static int copied;
static int moved;
static int destroyed;
int id;
explicit Tracked(int i) : id{i} { ++constructed; }
Tracked(const Tracked& other) : id{other.id} { ++copied; }
Tracked(Tracked&& other) noexcept : id{other.id} {
++moved;
other.id = -1; // moved-from: érvényes, de nem specifikált érték
}
~Tracked() { ++destroyed; }
};
int Tracked::constructed = 0;
int Tracked::copied = 0;
int Tracked::moved = 0;
int Tracked::destroyed = 0;
Tracked make_prvalue() {
return Tracked{1}; // garantált prvalue-konstrukció
}
Tracked make_named() {
Tracked local{2};
return local; // NRVO-jelölt: megengedett, nem kötelező
}
Tracked make_named_forced_move() {
Tracked local{3};
return std::move(local); // NRVO blokkolva; move kényszerítve
}
int main() {
Tracked a = make_prvalue();
std::printf("a: copy=%d move=%d\n", Tracked::copied, Tracked::moved);
Tracked b = make_named();
std::printf("b: copy=%d move=%d\n", Tracked::copied, Tracked::moved);
Tracked c = make_named_forced_move();
std::printf("c: copy=%d move=%d\n", Tracked::copied, Tracked::moved);
std::printf("total: ctor=%d copy=%d move=%d dtor=%d\n",
Tracked::constructed, Tracked::copied,
Tracked::moved, Tracked::destroyed);
return 0; // a, b, c destruktorai itt futnak le
}A példa önállóan fordítható; a fordítót és a verziót a laborprofilban kell rögzíteni.
g++ -std=c++17 -O0 -Wall -Wextra -pedantic -o counter counter.cpp
clang++ -std=c++17 -O0 -Wall -Wextra -pedantic -o counter counter.cpp
# diagnosztikai változat: az opcionális elision kikapcsolása (GCC/Clang-specifikus)
g++ -std=c++17 -O0 -fno-elide-constructors -o counter_noelide counter.cppA -fno-elide-constructors nem hordozható kapcsoló, és a
garantált prvalue-esetet nem érinti; kizárólag az NRVO viselkedésének
szemléltetésére való. A várt megfigyelés — nem mért eredmény,
mert a kód futtatása a tanuló laborjának feladata:
| Konfiguráció | a után copy/move |
b után copy/move |
c után copy/move |
|---|---|---|---|
| C++17, alapértelmezett | 0 / 0 | 0 / 0 (NRVO) | 0 / 1 |
C++17, -fno-elide-constructors |
0 / 0 | 0 / 1 | 0 / 1 |
| C++14, alapértelmezett | 0 / 1 vagy 1 / 0 | 0 / 1 vagy 1 / 0 | 0 / 1 |
A táblázat értelmezése a lényeg. Az a sor minden
C++17-es konfigurációban garantáltan 0/0, mert a
make_prvalue visszatérése prvalue. A b sor
azért változik, mert az NRVO opcionális:
-fno-elide-constructors mellett a fordító move-ot
kényszerít, alapértelmezésben a legtöbb fordító elvégzi az NRVO-t. A
c sor mindenhol move-ot mutat, mert a
std::move(local) kizárja az NRVO-t. C++14-ben az
a sor sem garantált: a fordító döntése, hogy copy-t vagy
move-ot hív-e, vagy elhagyja-e — innen a „vagy” a táblázatban.
Három dolgot érdemes tudatosítani. Egyrészt a számlálók értéke nem a
szabvány által előírt viselkedés, hanem az adott fordító döntésének
lenyomata; más compiler, más verzió vagy más optimalizálási szint eltérő
számot adhat. Másrészt az -O0 nem jelenti azt, hogy az NRVO
elmarad: a legtöbb fordító az NRVO-t szemantikai szinten, az
optimalizálástól függetlenül végzi el, ezért a „debug buildben nincs
elision” feltevés hamis. Harmadrészt a táblázat „várt” értékeit senki
nem mérte meg a fejezet írásakor; a mérés a labor része, és az eredményt
a fordító és a kapcsolók rögzítésével együtt kell dokumentálni. A
printf beszúrása a konstruktorokba egyébként
megváltoztathatja az optimalizáló döntéseit, ezért a valódi méréshez
érdemes külön, oldalhatás nélküli változatot használni, és csak a
számlálókat kiírni.
A garancia és az NRVO közti különbség legélesebben nem mozgatható típusnál látszik:
struct Pinned {
Pinned() = default;
Pinned(const Pinned&) = delete;
Pinned(Pinned&&) = delete;
};
Pinned make_pinned() {
return Pinned{}; // C++17: OK, a copy/move ctor nem is kell
}
Pinned make_pinned_named() {
Pinned p;
return p; // C++17: ill-formed, mert a move ctor törölt,
// és az NRVO nem garantált
}A make_pinned_named azért nem fordul le, mert az
implicit move szabály szerint a visszatéréshez move konstruktorra lenne
szükség, és a fordító nem köteles az NRVO-t elvégezni. C++14-ben mindkét
függvény hibás, mert ott a garantált esetben is elérhetőnek kellett
lennie a konstruktornak. Ez a példa nem javasolt mintaként szolgál,
hanem annak illusztrálására, hogy a két elision-fajta feltételei és
következményei eltérnek.
A leggyakoribb hiba a return std::move(local);
használata lokális változóra. A javítás mechanikus: a
std::move elhagyása, és a return local; írása.
Ez megőrzi az NRVO esélyét, és ha az NRVO nem történik meg, az implicit
move akkor is mozgatni fog. A második hiba ennek a fordítottja: a
return local; mintától elvárni, hogy garantáltan nulla
másolással járjon. A garancia csak a prvalue-esetre vonatkozik; NRVO-nál
a helyes állítás az, hogy „a fordító valószínűleg elvégzi, de nem
köteles rá”.
A harmadik hiba a konstruktorokba és destruktorokba írt
oldalhatásokra építés: számláló, logolás, lockolás,
erőforrás-felszabadítás a copy konstruktorban. Mivel az elision
opcionális, ezek száma nem determinisztikus, és a program viselkedése
fordítófüggő lesz. A javítás az, hogy az ilyen oldalhatásokat külön,
explicit életciklus-kezelő hívásokba (pl. close(),
release()) helyezzük, és a destruktort idempotenssé
tesszük.
A negyedik hiba a std::move és a szeletelés
összekeverése: ha a lokális típusa származtatott, a visszatérési típus
pedig bázis, a std::move bázisra konvertál, és a
származtatott rész elveszik. Ez a slicing problémája, és nem az elision
kérdése; a részletes tárgyalás az öröklésről szóló részre tartozik.
Végül az ötödik hiba a referenciát visszaadó függvény és az értéket
visszaadó függvény összetévesztése: a T& visszatérésnél
nincs elision, mert nincs mit inicializálni — ott a dangling kockázata a
fő kérdés, ami a Move constructor, moved-from
állapot és std::move (22.3) alfejezethez kapcsolódik.
A C++17 garantált prvalue-konstrukciója nyelvi szabály: ha a
return kifejezés prvalue és típusa megegyezik a
visszatérési típussal, a prvalue közvetlenül a visszatérési objektumot
inicializálja, copy/move konstruktor nélkül, sőt azok hiányában is. Az
NRVO ezzel szemben megengedett, de nem kötelező elhagyás, amelynek
feltétele, hogy a visszaadott kifejezés egy automatikus tárolási idejű
lokális változó neve legyen; ilyenkor a copy/move konstruktornak
elérhetőnek kell lennie. Az implicit move szabály a
return local; alakot rvalue-ként kezeli az overload
resolution során, így NRVO hiányában is move történik — ezért a
return std::move(local); felesleges és káros, mert kizárja
az NRVO-t. A tagra vagy paraméterre alkalmazott std::move
viszont indokolt lehet, mert ezekre az implicit move nem terjed ki. Az
elision destruktorokat is elhagy, ezért a konstruktorok és destruktorok
oldalhatásaira építeni hibás; a megfigyelés pedig mindig fordító-,
verzió- és kapcsolófüggő, amit a mérés dokumentálásakor rögzíteni
kell.
1. feladat (magyarázó). Hasonlítsd össze a
példaprogram make_prvalue és make_named
függvényét! Melyik esetben garantált, hogy nem hívódik meg copy vagy
move konstruktor, és melyik esetben csak valószínű? Fogalmazd meg, mi a
pontos feltétel különbsége, és hogyan viselkedik a
make_prvalue C++14-ben.
2. feladat (kódolási). Az alábbi függvény
szándékosan rossz mintát mutat. Javítsd, indokold a javítást, és add
meg, milyen esetben lenne mégis helyes a std::move
használata.
Tracked build(int n) {
Tracked local{n};
return std::move(local);
}3. feladat (diagnosztikai). Írj egy
Tracked pick(bool first) függvényt, amely két különböző
lokális változó közül ad vissza egyet a feltétel alapján. Egészítsd ki a
számlálókat, add meg a build parancsot, és magyarázd el, miért nem
alkalmazható itt NRVO, valamint hogy milyen copy/move számot vársz.
Milyen ellenőrzési feltétellel tudod eldönteni, hogy a megfigyelésed
megfelel-e az elvárásnak?
4. feladat (elemző). A Pinned példában
melyik függvény fordul le C++17-ben, és melyik nem? Indokold a választ
az implicit move szabályával és az NRVO feltételével, majd mondd meg, mi
a helyzet C++14-ben. Milyen minimális módosítással tehető mindkét
függvény C++17-kompatibilissé?
1. megoldás. A make_prvalue
visszatérési kifejezése Tracked{1}, azaz prvalue, amelynek
típusa megegyezik a visszatérési típussal. C++17-ben ezért a prvalue
közvetlenül inicializálja a visszatérési objektumot: nincs temporális,
nincs copy/move, és a konstruktoroknak nem is kell létezniük. Ez
garantált. A make_named visszatérési kifejezése a
local név, azaz automatikus tárolási idejű lokális változó,
ezért NRVO-jelölt: a fordító elhagyhatja a copy/move hívást, de nem
köteles rá; ha nem éli vele a lehetőséget, az implicit move szabály
miatt move történik. C++14-ben a make_prvalue esetében
sincs garancia: a szabvány csak megengedi az elhagyást, és a copy/move
konstruktornak elérhetőnek kell lennie. Ellenőrzési feltétel: a
válasz akkor helyes, ha kimondja, hogy a garancia a prvalue-esethez
kötött, és hogy C++14-ben ez a garancia nem létezik.
2. megoldás. A javítás: return local; —
a std::move elhagyása. Ezzel az NRVO lehetősége megmarad,
és ha a fordító nem éli vele, az implicit move akkor is mozgatni fogja a
lokálist, mert a return kifejezés egy automatikus tárolási
idejű objektumot megnevező azonosító-kifejezés. A std::move
akkor lenne indokolt, ha a visszaadott kifejezés nem ilyen lokális
változó: például egy tag (return std::move(member_);) vagy
egy függvényparaméter (return std::move(param);) esetén,
mert ezekre az implicit move szabály nem terjed ki, és
std::move nélkül copy történne. Ellenőrzési
feltétel: a javított függvény fordul, a Tracked
számlálói a pick-től függetlenül azt mutatják, hogy a
build visszatérésekor legfeljebb egy move történik, és az
NRVO engedélyezett konfigurációban nulla.
3. megoldás. A függvény például:
Tracked pick(bool first) {
Tracked a{10};
Tracked b{20};
if (first) return a;
return b;
}Itt mindkét return egy-egy lokális változó nevét adja
vissza, de különböző objektumokét. Az NRVO feltétele az, hogy a
visszaadott kifejezés egy konkrét lokális objektumot nevezzen meg; két
különböző lokális esetén a fordító nem tudja mindkettőt ugyanabba a
célhelybe építeni, ezért a gyakorlatban move történik (a nem
kiválasztott lokális pedig a saját destruktorával lezárul). A várt
megfigyelés: a kiválasztott ágon egy move, a másik ágon a lokális
destruktora; a számlálók értéke tehát move >= 1.
Ellenőrzési feltétel: a program lefut, a kiválasztott objektum
id értéke megmarad (10 vagy 20), és a számlálók
konzisztensek a konstruktorok és destruktorok számával; a megfigyelést a
fordító és a kapcsolók rögzítésével együtt kell dokumentálni, mert az
NRVO hiánya itt nem garantált, csak tipikus.
4. megoldás. C++17-ben a make_pinned
lefordul, mert a return Pinned{}; prvalue, és a garantált
prvalue-konstrukcióhoz nem kell copy vagy move konstruktor. A
make_pinned_named nem fordul le, mert a
return p; NRVO-jelölt, és ha az NRVO nem történik meg, az
implicit move szabály miatt a törölt move konstruktort kellene hívni — a
program ill-formed. C++14-ben mindkét függvény hibás, mert a copy/move
konstruktornak elérhetőnek kell lennie akkor is, ha a fordító elhagyja a
hívást. A minimális javítás: a Pinned copy és move
konstruktorának törlése helyett definiálása (vagy legalább a move
konstruktor engedélyezése), illetve a névvel történő visszatérés
elkerülése és a return Pinned{}; alak használata.
Ellenőrzési feltétel: a javított változat mindkét függvénye
lefordul C++17-ben, és a make_pinned esetében a számlálók
nulla copy/move-ot mutatnak.
A fejezet állításai a C++17 ISO szabvány copy elisionre és implicit move-ra vonatkozó szabályain alapulnak, valamint a C++20-as kiterjesztés ismertetett körére. A generálás során nem történt forrásellenőrzés: a konkrét szakaszhelyeket, a C++20-as kiterjesztés pontos hatókörét és a fordítók viselkedését a választott szabványban, illetve a fordító dokumentációjában kell ellenőrizni. Kiindulópontként a WG21 nyilvános oldala (https://www.open-std.org/jtc1/sc22/wg21/), a C++23 munkatervezet (https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/n4950.pdf) és a C++ Core Guidelines (https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines) szolgálhat; ezek a keresés kiindulópontjai, nem a fejezet állításainak bizonyítékai. A Core Guidelines ajánlás, nem normatív ISO-szöveg. A példák nincsenek lefuttatva; a táblázat várható értékeket tartalmaz, nem mért eredményeket.