Ez az alfejezet a C-ből hozott deklarációs rutinra épül: ott a típust
általában mi írtuk ki, itt viszont a fordító is megállapíthatja. Ez nem
kényelmi „var”-szerűség, hanem két, pontosan definiált levezetési
szabályrendszer: az auto a sablonargumentum-levezetést
használja, a decltype pedig a deklarált típust vagy a
kifejezés értékkategóriáját. A különbségtétel nem akadémikus: a rosszul
megválasztott alak néma másolatokhoz, elveszett const-hoz
vagy érvénytelen élettartamú referenciához vezet. A referenciák és a
const C++-beli szemantikáját itt előfeltételként
használjuk, részletesen lásd Reference, pointer és
const C++-ban; a deklarációs szintaxis C-s alapjait Deklarációk, typedef és típusminősítők.
Definíció. Az auto helyőrzővel írt
deklarációban a fordító a deklarált típusból egy „mintát” képez, és a
kezdőértékből a sablonargumentum-levezetés szabályai szerint tölti ki. A
minta alakja dönti el, hogy a kezdőérték referenciája és minősítői
megmaradnak-e: ha a mintában szerepel &,
&& vagy const, akkor az adott
tulajdonság megjelenik a levezetett típusban, ha nem, akkor elveszik. Ez
a „template deduction for dummies” elv: auto-t
helyettesítve egy template<class T> void f(T)
paraméterébe ugyanazt kapjuk.
Következmény, amit a legtöbb kezdő elront: a csupasz
auto soha nem lesz referencia, és a
kezdőérték top-level const/volatile
minősítője elvész. A referencia és a const
megőrzéséhez a deklarációban kell jelezni.
| deklaráció | kezdőérték | levezetett típus |
|---|---|---|
auto a = i; |
int i |
int |
auto b = ci; |
const int ci |
int |
auto c = ri; |
int& ri |
int |
auto& d = ci; |
const int ci |
const int& |
auto&& e = ci; |
const int ci |
const int& |
auto&& f = 42; |
prvalue int |
int&& |
const auto& g = i; |
int i |
const int& |
auto* h = &i; |
int i |
int* |
Az auto&& alak külön figyelmet érdemel: lvalue
kezdőértéknél lvalue referenciát, rvalue-nál rvalue referenciát ad
(forwarding reference), ezért gyűjtőnévként használható ott, ahol nem
akarunk dönteni a két eset között.
Kidolgozott példa. A következő program minden
állítását static_assert-tel ellenőrzi, így a levezetett
típus nem „szemre”, hanem fordítási időben ellenőrizhető. A kód C++20-at
igényel, és a <type_traits> fejlécet használja.
#include <type_traits>
int main() {
int i = 0;
const int ci = 1;
int& ri = i;
const int& cri = ci;
auto a = ci; // int: a const elvész
auto b = ri; // int: referencia -> érték, másolat
auto& c = ri; // int&: a tényleges i-re hivatkozik
auto& d = ci; // const int&: const megmarad
auto* p = &ci; // const int*
auto&& e = ci; // const int&
static_assert(std::is_same_v<decltype(a), int>);
static_assert(std::is_same_v<decltype(b), int>);
static_assert(std::is_same_v<decltype(c), int&>);
static_assert(std::is_same_v<decltype(d), const int&>);
static_assert(std::is_same_v<decltype(p), const int*>);
static_assert(std::is_same_v<decltype(e), const int&>);
static_assert(std::is_same_v<decltype(cri), const int&>);
}Fordítás és ellenőrzés (a tanuló saját laborjában; itt nem futtattuk):
g++ -std=c++20 -Wall -Wextra -Wpedantic -O2 -o deduce deduce.cpp && ./deduce
# vagy
clang++ -std=c++20 -Wall -Wextra -Wpedantic -O2 -o deduce deduce.cpp && ./deduceHa bármelyik static_assert hamis, a program nem fordul
le: ez erősebb ellenőrzés annál, mintha a futáskor nyomtatnánk
típusneveket. Megjegyzendő korlát: a decltype itt deklarált
típusra kérdez rá, ezért a static_assert a deklaráció
helyességét igazolja, nem azt, hogy a mögöttes futásidejű érték mit
tartalmaz.
Két további, gyakran előforduló eset. Az
auto x = {1, 2, 3}; alakban a kezdőérték
std::initializer_list<int>, nem int és
nem tömb; ez a auto kivételes szabálya. Függvény
visszatérési típusaként az auto minden return
utasításból ugyanazt a típust köteles levezetni, és braced-init-listből
nem lehet visszatérési típust levezetni.
Definíció. A decltype(e) a zárójelben
álló kifejezés típusát adja vissza, de a kifejezést nem értékeli ki
(unevaluated operand). A szabály kétágú:
e zárójel nélküli azonosító-kifejezés (változó,
paraméter, structured binding tag), akkor az eredmény a
deklarált típus, a referencia és a const
megtartásával.T&, xvalue → T&&, prvalue →
T.A zárójel tehát nem kozmetika. decltype(i) az
int, decltype((i)) viszont
int&, mert a zárójelezett alak nem azonosító-kifejezés,
hanem lvalue kifejezés.
int i = 0;
const int ci = 1;
decltype(i) a = i; // int
decltype((i)) b = i; // int& <- a plusz zárójel számít
decltype(ci) c = ci; // const int
decltype((ci)) d = ci; // const int&
decltype(i + 1) e = 0; // int (prvalue)
decltype(i = 1) f = i; // int& (az értékadás lvalue)Ez a szabály teszi lehetővé, hogy generikus kódban a visszatérési
típus tökéletesen kövesse a paramétert, ugyanakkor könnyű vele meglepni
magunkat: egy makróba tett decltype((x)) hirtelen
referenciát ad, ahol értéket vártunk.
Definíció. A decltype(auto) helyőrző a
kezdőértékre (vagy a return operandusára) a
decltype szabályait alkalmazza az auto
szabályai helyett. Célja a tökéletes visszaadás: referenciát adó
kifejezésből referenciát, értékből értéket csinál.
A veszély pontosan itt van: a decltype(auto) nem
nyújtja meg a lokális változó élettartamát, csak a típust
követi.
// TANÍTÁSI PÉLDA – nem definiált viselkedés, nincs garantált kimenet
decltype(auto) bad() {
int local = 42;
return (local); // (local) lvalue -> int&, a local kilépéskor megszűnik
}
// Helyes változat: érték visszaadása
int good() {
int local = 42;
return local; // int, másolat
}
// decltype(auto)-val is helyes, mert itt név áll zárójel nélkül
decltype(auto) also_good() {
int local = 42;
return local; // decltype(local) == int
}A bad() eredményét használó hívó kód érvénytelen
élettartamú objektumra hivatkozik; ez a dangling
esete, és UB, nem „véletlen szemét”, amit meg lehet figyelni. A javítás
mindig szerződés kérdése: ha a függvény referenciát ad vissza, annak a
hívó élettartamához kell kötnie, nem a függvény lokálisához.
Definíció. Az auto [a, b] = expr;
deklaráció egy rejtett változót (e, a backing object) hoz
létre expr-ből, majd az a, b
neveket az e részeihez köti (tömb esetén elemekhez,
std::tuple-szerű típusnál get<> hívás
eredményéhez, osztálynál nyilvános tagokhoz). Az a,
b nem önálló objektumok, hanem az
e alterületeit jelölő nevek; típusukat az e
típusa és a hozzáférés módja határozza meg.
Ebből következik a gyakorlati szabály, két külön kérdéssel. (1)
Mi az e? Ha auto-t írunk, az
e a forrás másolata; ha auto& vagy
const auto& áll, az e referencia, és a
nevek az eredeti objektumot aliasolják. (2) Mit tartalmaz az
e? A másolat csak a forrás értéktagjait
választja le. Ha a forrás referenciát vagy pointert tárol —
std::tie(a, b) eredménye,
std::tuple<int&>, pointertag —, akkor a másolat
is ugyanarra az objektumra hivatkozik, és a neveken át végzett írás a
külső objektumot módosítja. Az auto tehát csak akkor
„választja le a forrást”, ha annak minden tagja érték, mint a lenti
std::pair<const std::string, int> esetén.
// binding_alias.cpp — C++20: auto [..] másolata is aliasolhat
#include <cassert>
#include <tuple>
int main() {
int a = 1, b = 2;
auto [x, y] = std::tie(a, b); // e: tuple<int&, int&> másolat
x = 10; // a másolt tuple referenciáján át a-t írja
assert(a == 10);
std::tuple<int&> t{b};
auto [r] = t; // e: tuple<int&> másolat, r mégis b-re hivatkozik
r = 20;
assert(b == 20);
struct P { int v; int* p; };
P src{1, &a};
auto [v, p] = src; // e: P másolat; v független, p ugyanoda mutat
v = 5; *p = 30;
assert(src.v == 1 && a == 30);
}Buildjegyzőkönyv (lab-gcc profil).
g++ -std=c++20 -Wall -Wextra -Wpedantic binding_alias.cpp
figyelmeztetés nélkül fordul, és a futás egyik assert-et
sem sérti: az x, r, *p írása a
külső a, b, a objektumot
változtatta, a v írása nem hatott a
src.v-re.
#include <map>
#include <string>
void demo(std::map<std::string, int>& m) {
auto [k, v] = *m.begin(); // e: másolat a pair-ről
v = 99; // csak a másolatot módosítja, m változatlan
auto& [k2, v2] = *m.begin(); // e: referencia a map elemére
v2 = 99; // most már a tároló eleme változik
for (const auto& [key, value] : m) {
(void)key; (void)value; // olvasás, másolás nélkül
}
}A háttérben álló objektum élettartama a deklaráció hatóköre: ha
expr ideiglenes volt, és referenciát kötünk hozzá a
deklarációban, a nyelv megnyújtja az e élettartamát; ha
viszont az expr egy függvényhívás eredményeként
referenciát ad vissza egy már megszűnt objektumra, a
binding ezt nem tudja megjavítani.
A range-for hátterében a szabvány egy auto&&
kötést készít a tartományra, majd iterátorokkal járja be. A
ciklusváltozó deklarációja ezért ugyanazt a döntést kéri, mint az
auto:
#include <string>
#include <vector>
struct Metric { std::string name; long value; };
void bump(std::vector<Metric>& v) {
for (auto m : v) { ++m.value; } // HIBA: a másolatot növeli
}
void bump_fixed(std::vector<Metric>& v) {
for (auto& m : v) { ++m.value; } // helyes: a tényleges elem
}
long total(const std::vector<Metric>& v) {
long sum = 0;
for (const auto& m : v) { sum += m.value; } // olvasás, másolás nélkül
return sum;
}A for (auto m : v) alak helyes, ha csak olvasunk és az
elem kicsi, de minden iterációnál másol; nagy
std::string-et tartalmazó struktúránál ez mérhető költség.
A for (auto& m : v) nem másol, viszont konstans
tartományon nem fordul le, ezért olvasáshoz a
const auto& a helyes választás. Élettartam
szempontjából a ciklus saját maga által létrehozott ideiglenes
tartományát a nyelv a ciklus végéig életben tartja (C++20-ban így
definiált); a C++23 ezt a szabályt tovább pontosítja a range-init
kifejezésben keletkező ideiglenesekre, ennek részletei verziófüggők, és
itt nem tárgyaljuk őket. Az viszont nem megengedett következtetés, hogy
egy függvényből visszakapott referencia automatikusan megnyúlik:
const auto& r = makeVector()[0]; után r
érvénytelen, mert az ideiglenes vektor a teljes kifejezés végén
megszűnik.
A using kulcsszóval típusálneveket vezethetünk be; ez a
typedef modern megfelelője, és sablonoknál az egyetlen
olvasható alak. A using nem hoz létre új típust, csak nevet
ad egy létezőnek, ezért a levezetés szempontjából átlátszó.
#include <cstddef>
#include <vector>
using Size = std::size_t;
using Callback = void (*)(int); // függvénypointer alias
template<class T> using Vec = std::vector<T>; // sablon-alias
Size count(const Vec<int>& v); // olvashatóbb, mint std::size_tA using és az auto együtt különösen hasznos
hosszú visszatérési típusoknál, illetve ott, ahol a típusnév a fejlécben
zajt jelent. A fejlécben továbbra is érdemes kiírni a típust vagy aliast
használni, mert az auto a deklaráció helyén derül ki, és a
fejlécet olvasó számára nem látszik.
const/referencia.
auto x = ci; után x módosítható
int, és nem aliasol. Javítás:
const auto& x = ci;, ha csak olvasni akarunk, vagy
auto&, ha módosítani.for (auto m : v) nagy elemeknél pazarló;
auto& vagy const auto& a szándék
kifejezése.decltype((x)) referencia-visszaadás.
Generikus kódban a zárójel hirtelen referenciát ad; ha értéket akarunk,
decltype(x) vagy std::decay_t.decltype(auto) lokális referenciával.
A return (local); dangling referenciát ad; a helyes
megoldás érték visszaadása, vagy a referencia élettartamának dokumentált
külső biztosítása.auto [k, v] = *it; után a v módosítása nem hat
a tárolóra; auto& [k, v] = *it; kell.Az auto a sablonargumentum-levezetés szabályait követi:
csupasz alakban elveszti a referenciát és a top-level
const-ot, ezért a szándékot a mintában (&,
const&, &&) kell kifejezni. A
decltype zárójel nélküli névre a deklarált típust adja,
zárójelezett kifejezésre az értékkategóriából következtet, és nem
értékeli ki az operandust. A decltype(auto) a
decltype szabályait viszi a visszatérési típusba, ami
hasznos forwardingnál, de nem nyújtja meg a lokális élettartamát. A
structured binding mögött rejtett backing object áll, a range-for
ciklusváltozója pedig ugyanazt a másolat-referencia döntést igényli. A
pontos típus ellenőrzésének olcsó eszköze a
static_assert(std::is_same_v<...>).
A szabvány pontos paragrafusaira ebben az egységben nem hivatkozunk ellenőrzött forrásként: a megadott forrásjegyzet ehhez a részhez nem tartalmaz tételt, ezért a fenti állítások a bemutatott kód és a levezetési szabályok szintjén értelmezendők, nem hivatalos szöveghely-igazolással.
int i = 0; const int ci = 1; int& ri = i; const int& cri = ci;.
Határozza meg a következő deklarációk levezetett típusát, és írjon
mindegyikhez static_assert-et: auto a = ci;,
auto& b = ci;, auto&& c = ci;,
auto&& d = 42;, decltype(ri),
decltype((ri)), decltype(ci + 1).bump és total
függvényeket tartalmazó példát alakítsa át úgy, hogy sehol ne
keletkezzen felesleges másolat, a módosító változat tényleg módosítson,
és a const tartományon is forduljon le. Indokolja minden
auto-alak választását.decltype(auto) firstPositive(const std::vector<int>& v) {
for (const auto& x : v)
if (x > 0) return (x);
return 0;
}using
aliasokat egy olyan fejlécben, amely
std::map<std::string, std::vector<int>> típust
használ paraméterként és visszatérési típusként, és indokolja, miért nem
auto-t ír a fejlécbe.1. megoldás. a típusa int
(a const elvész, mert a mintában nincs const).
b típusa const int& (a
auto& megőrzi a const-ot). c
típusa const int&: az auto&&
lvalue kezdőértéknél lvalue referenciát ad, a const
megmarad. d típusa int&&, mert
prvalue-ból rvalue referencia lesz. decltype(ri) az
int& (deklarált típus), decltype((ri))
szintén int&, mert a zárójelezett név lvalue; a két
alak itt egybeesik, de nem minden esetben. decltype(ci + 1)
int, mert az összeg prvalue. Ellenőrzési feltétel: a
static_assert-ek forduljanak le -std=c++20
módban, és a program ne tartalmazzon futásidejű tesztet, mert a kérdés
tisztán típusszintű.
2. megoldás. A módosító változat
for (auto& m : v), mert a ciklusváltozónak a tároló
elemét kell aliasolnia. Az összesítő változat
for (const auto& m : v), mert csak olvasunk, így nincs
másolat és a const tartomány is elfogadott. Ha a
Metric nagyon kicsi és olcsón másolható (például két
int), a for (auto m : v) is védhető, de ezt
mérés vagy dokumentált költségmodell nélkül nem állítjuk. Ellenőrzési
feltétel: a módosító függvény után a tároló elemei tényleg megváltoztak,
az összesítő pedig const std::vector<Metric>&
paraméterrel is fordul.
3. megoldás. A return (x); zárójelezett
lvalue kifejezés, ezért a decltype(auto) visszatérési
típusa const int&. A x viszont a
ciklusváltozó, amelynek élettartama a ciklus végén lejár, tehát a
visszaadott referencia dangling; a hívó oldali használat UB. A
return 0; ág ezzel szemben int-et adna, ami a
két ág eltérő visszatérési típusa miatt önmagában is problémás. Javítás:
adjuk vissza az értéket, például
std::optional<int> firstPositive(const std::vector<int>& v)
alakban, amely a „nincs találat” esetet is kifejezi, vagy
int visszatéréssel és külön találati logikával. Fordítási
időben a hiba nehezen fogható meg, mert a típus legális; a gyakorlati
védelem a -Wall -Wextra figyelmeztetés, a code review és a
statikus elemzés, futásidőben pedig az AddressSanitizer jelzi a
stack-use-after-scope hibát. Ez utóbbi eszköz viselkedése verzió- és
beállításfüggő, ezért konkrét kimenetet itt nem állítunk.
4. megoldás. Például:
#include <map>
#include <string>
#include <vector>
using IntVec = std::vector<int>;
using Table = std::map<std::string, IntVec>;
Table load(const Table& input);A fejlécben azért nem auto, mert a visszatérési típus a
deklaráció helyén derül ki, a fejlécet olvasónak viszont a szerződést
kell látnia, és a fordítónak is stabil szignatúrára van szüksége a külön
fordított egységekhez. Az alias nem új típus, ezért az
IntVec és a std::vector<int>
felcserélhető, a levezetést nem befolyásolja. Ellenőrzési feltétel: a
fejléc önállóan include-olható, és a fordítása -std=c++20
módban figyelmeztetés nélkül sikerül.