A C++ inicializálás az a terület, ahol a legtöbb tapasztalt, más
nyelvről érkező mérnök is meglepődik: ugyanaz a négy-öt karakter
látszólag mást jelent skalár, aggregate, std::vector vagy
saját osztály esetén. DevOps-környezetben ez nem elméleti szépséghiba:
konfigurációs struktúrák, buffer-méretek, socket-portok és flag-ek
inicializálása közvetlenül hibás viselkedéshez, néha nem definiált
viselkedéshez (undefined behavior, UB) vezet. Ez az alfejezet a C++20-as
alapot feltételezi, és a C-ből ismert objektum-/tárolási modellre épül;
a korábbi Inicializálás, indeterminate érték és
reprezentáció (5.3) és az Overload, konverzió és
explicit interfész (19.5) ismeretét rövid felidézésként
használja.
A tárgyalás határait itt az inicializálási formák jelentik. A konstruktorok és destruktorok sorrendjét, a részleges konstrukciót, valamint a temporary lifetime részletes eseteit a következő alfejezetek (Osztály, konstruktor és invariáns (20.2), Destruktor és részleges konstrukció (20.3), Temporary, value category és dangling (20.4)) tárgyalják; itt csak a megértéshez szükséges mértékben utalunk rájuk. A példák C++20-at feltételeznek; a C++23-as eltéréseket külön jelölöm, és a konkrét GCC/Clang-viselkedést a tanuló laborjában kell ellenőrizni. Ehhez a generáláshoz nem kaptam ellenőrzött forrásjegyzetet, ezért a szabványparagrafusokra itt nem teszek kattintható hivatkozást, és a „fordítottam/teszteltem” állítást sem használom.
A C++ inicializálási kategóriái nem teljesen függetlenek: a szintaxis sokszor egy általánosabb szabályt indít el, amelynek része a zero initialization, a value initialization vagy éppen az aggregate initialization. A következő táblázat a leggyakoribb eseteket foglalja össze; a részletek utána következnek.
| Forma | Tipikus szintaxis | Skalár típus | Osztálytípus | Fontos megjegyzés |
|---|---|---|---|---|
| default initialization | T x;, new T |
nincs inicializálás; indeterminált | default konstruktor | lokális skalárnál olvasása UB |
| value initialization | T(), T{}, new T() |
nulla | ha a default ctor nem user-provided: zero-init + default-init; különben default-init | T x{} gyakori „nullázó” alak |
| zero initialization | statikus tárolású T x; |
nulla vagy null pointer | tagok nullázása | static/thread_local esetén a program indulása előtt |
| direct initialization | T x(args);, T x{args}; |
konverzió | konstruktor, explicit is | static_cast<T>(args) is ide tartozik |
| copy initialization | T x = expr;, T x = {args}; |
konverzió | csak nem explicit konstruktor | függvényargumentum, return is |
| list initialization | T x{...};, T x = {...}; |
narrowing tilos | initializer_list preferencia, aggregate | {} és () eltér |
| aggregate initialization | aggregate T x{...}; |
tagok sorrendben | DMI vagy zero/value-init | C++20: designated initializer |
A default initialization pontos definíciója: az objektum
deklarációjában nincs initializer, illetve new T esetén
nincs zárójel. Osztálytípusnál a default konstruktor hívódik; ha nincs
hívható default ctor, a kód ill-formed. Tömbnél minden elem
default-initializálódik. Skalárnál — például int x; — a
nyelv nem ír elő értéket: az objektum indeterminált értékű lesz, és ha
ezt kiolvassuk, UB történik. Ugyanez igaz a new int
kifejezésre is; a new int() viszont value-initializál, azaz
nulláz.
A value initialization akkor lép fel, amikor T(),
T{}, new T(), new T{} alakot
használunk, illetve amikor egy tagot a mem-initializer listában üres
zárójelpárral inicializálunk. Skalárnál az eredmény nulla. Osztálynál a
szabály attól függ, hogy a default konstruktor user-provided-e: ha nem
az, akkor először zero initialization, majd default initialization
történik; ha user-provided, akkor „csak” a konstruktor fut. Ez a
megkülönböztetés magyarázza, miért lesz
std::vector<int> v(3); elemei nulla értékűek,
miközben egy saját, user-provided default konstruktorral rendelkező
osztály T{} esetén nem feltétlenül nullázza a tagokat.
A zero initialization nem önállóan kiírt szintaxis, hanem bizonyos
helyzetekben automatikusan végrehajtott előzetes lépés: statikus és
thread-local tárolási idő esetén a program indulása előtt, valamint a
value initialization részeként. Skalárnál nullát, pointer esetén null
pointert, osztály esetén a tagok nullázását jelenti. Fontos, hogy ez nem
azonos a default initializationnal: static int x;
nullázódik, a lokális int x; nem.
A direct initialization a T x(args);,
T x{args};, T(args) és
static_cast<T>(args) alakokat fedi. Lényege, hogy a
konstruktorok közül az összes figyelembe vehető, beleértve az
explicit konstruktorokat is. Copy initialization esetén
viszont a T x = expr; és a T x = {args};
alakot használjuk, valamint ilyen történik függvényargumentum átadásakor
és visszatérési értéknél; itt az explicit konstruktorok nem
játszanak szerepet. Ezért a T x = T(args); alak nem tud
olyan explicit konstruktort hívni, mint a T x(args);.
A list initialization a kapcsos zárójeles alakok gyűjtőneve:
direct-list-initialization (T x{...};) és
copy-list-initialization (T x = {...};). A
lista-inicializálás előnye, hogy tiltja a narrowing conversiont,
elkerüli a „most vexing parse” problémát, és aggregate esetén aggregate
initializationt indít. Hátránya, hogy az initializer_list
konstruktorokat előnyben részesíti, ami a megszokott ()-es
hívásnál meglepőbb eredményt adhat.
Nézzük a klasszikus std::vector-példát:
#include <vector>
int main() {
std::vector<int> a(3, 7); // három darab 7-es
std::vector<int> b{3, 7}; // két elem: 3 és 7
std::vector<int> c(3); // három value-initialized int: 0, 0, 0
std::vector<int> d{3}; // egy elem: 3
}A különbség oka, hogy a b{3, 7} listában a
std::initializer_list<int> konstruktor viabilis,
ezért a lista-inicializálás első fázisa azt választja. A
d{3} ugyanígy egy elemű listát ad, nem három nullát. Aki a
std::vector<int> v{10}; alakot „tíz nulla”
jelentéssel írja, egy elemű, 10-es értékű vektort kap. Ugyanez a minta
jelenik meg std::string esetén is:
#include <string>
int main() {
std::string a(5, 'x'); // "xxxxx"
std::string b{5, 'x'}; // két karakter: char(5) és 'x'
}A b esetében az
initializer_list<char> konstruktor lesz a nyertes; az
5 int konstans kifejezésként elfér char-ban,
ezért a narrowing nem tiltja ki. Ha viszont
std::string c{300, 'x'}; szerepelne, a 300 nem férne el
char-ban, így a lista-inicializálás ill-formed lenne. Ez
jól mutatja, hogy az initializer_list preferencia nem
„mindig jó”, hanem a lista típusától és a konverzióktól függ.
Aggregate a C++20 szerint az a tömb vagy osztály, amelynek nincs felhasználó által deklarált vagy örökölt konstruktora, nincs private/protected nem statikus adattagja, nincs virtuális függvénye, és nincs virtuális, private vagy protected bázisosztálya. Aggregate inicializálásnál a kapcsos zárójelben lévő értékek sorrendben az aggregate elemeit inicializálják, és a kimaradó elemek default member initializerrel (DMI), ennek hiányában üres listából kapnak értéket. Hogy mi számít elemnek, az aggregate fajtájától függ:
A bázis tehát saját elem: a D{{1}, 2} listában a
{1} a B bázisé, a 2 a
D::y-é. Brace elision miatt a D{1, 2} is
ugyanezt jelenti — az első 1 itt sem D::y
inicializálója, hanem a B::x-é.
// aggregate_base.cpp — C++20: bázis a tagok előtt
#include <cassert>
struct B { int x; };
struct D : B { int y; };
int main() {
D d{{1}, 2}; // első elem: a B bázis ({1}), második: D::y
assert(d.x == 1 && d.y == 2);
D e{1, 2}; // brace elision: az 1 a B::x-be kerül, nem D::y-ba
assert(e.x == 1 && e.y == 2);
// D f{.y = 2}; // hiba: a bázis nem nevezhető meg, és designatorral
// csak közvetlen nem statikus tag inicializálható
}Buildjegyzőkönyv (lab-gcc profil).
g++ -std=c++20 -Wall -Wextra -Wpedantic aggregate_base.cpp
figyelmeztetés nélkül fordul, a futás egyik assert-et sem
sérti. A designated initializer csak közvetlen nem statikus adattagot
nevezhet meg: a D{.x = 1, .y = 2} GCC 13.3 és Clang 22
alatt is hibát ad
('D' has no non-static data member named 'x'), a
D{.y = 2} viszont fordul, a kihagyott bázis üres listából
inicializálódik (GCC -Wextra alatt
missing initializer figyelmeztetéssel).
struct Point {
int x;
int y;
};
Point p1{1, 2}; // x = 1, y = 2
Point p2{1}; // x = 1, y = 0
Point p3{}; // x = 0, y = 0
Point p4{.x = 3, .y = 4}; // C++20 designated initializerA verziófüggés itt valós és könnyen megbotlik rajta az ember.
C++11-ben a DMI-t tartalmazó osztály még nem számított aggregate-nek;
C++14 óta igen. C++17-ben a nyilvános, nem virtuális bázisosztály már
megengedett aggregate esetén; C++20 viszont minden felhasználó által
deklarált konstruktort kizár, nem csak a user-provided vagy explicit
konstruktorokat. C++20-ban jelentek meg a designated initializerek és a
zárójeles aggregate initialization is (Point p(1, 2);),
amelyek C++17-ben még nem elérhetőek. A designated initializer C++20-as
szabályai szigorúbbak a C99-esnél: a megnevezett tagok sorrendje a
deklarációs sorrend kell legyen, és a C-s beágyazott, illetve
tömb-designatorok nem részei a C++20 nyelvnek. C++23 esetén a fordító és
a standard library támogatását külön kell ellenőrizni; a fenti példák
C++20-as baseline-t feltételeznek.
A DMI és az aggregate initialization együtt különösen hasznos konfigurációs struktúráknál:
struct Config {
int timeout_ms = 1000;
bool verbose = false;
};
Config c1; // default initialization: DMI-k érvényesülnek
Config c2{}; // value/list initialization: DMI-k érvényesülnek
Config c3{500}; // aggregate init: timeout_ms = 500, verbose a DMI-ből falseA c3 esetében a verbose kimarad, ezért a
DMI-ját kapja, nem indeterminált értéket. Ez a minta DevOps-eszközökben
gyakori: a „default érték” és a „felülírás” egy helyen, olvashatóan
jelenik meg. Fontos azonban, hogy ha az osztályhoz később felhasználó
által deklarált konstruktort adunk, C++20-ban az aggregate státusz
megszűnik, és a Config c3{500}; jelentése megváltozhat;
ilyenkor a konstruktorok és a {}/() különbség
újraellenőrzendő.
A „most vexing parse” (szó szerint „legbosszantóbb elemzés”) az a C++ szabály, amely szerint ami függvénydeklarációként is értelmezhető, azt függvénydeklarációként kell értelmezni. A legegyszerűbb eset:
struct S {
S(int);
};
S s1(1); // objektum, direct initialization
S s2(); // függvénydeklaráció: s2 visszatérési típusa S, nincs paramétere
S s3{}; // objektum, value/list initializationA S s2(); tehát nem default konstruktorral létrehozott
objektum, hanem függvénydeklaráció. A {} használata ezt a
kétértelműséget megszünteti. Egy összetettebb, gyakran idézett
példa:
#include <iterator>
#include <vector>
// Ez függvénydeklarációt deklarál, nem vektort!
std::vector<int> v(std::istream_iterator<int>(std::cin),
std::istream_iterator<int>());
// Egyértelmű objektumkonstrukció:
std::vector<int> w{std::istream_iterator<int>(std::cin),
std::istream_iterator<int>()};A w esetében a lista-inicializálás először az
initializer_list<int> konstruktort vizsgálja; mivel
az iterátorok nem konvertálhatók int-té, ez nem viabilis,
így a második fázisban a range konstruktor hívódik. Ez a példa jól
mutatja, hogy a {} nem „varázsszer”, hanem egy másik
overload-halmazt indít el; a lényeg, hogy a deklaráció egyértelmű
legyen.
A list initialization egyik legfontosabb biztonsági szabálya, hogy a
narrowing conversion ill-formed. A szabvány fő esetei: lebegőpontosból
egészbe; long double-ből
double/float-ba vagy double-ből
float-ba; egészből vagy unscoped enumerációból
lebegőpontosba; egészből vagy unscoped enumerációból olyan egész
típusba, amely nem tudja az összes forrásértéket ábrázolni; valamint
pointer vagy pointer-to-member típusból bool-ba. A szabály
alól vannak constant expression kivételek: ha a forrás constant
expression, az érték belefér a cél típusba, és visszakonvertálva az
eredeti értéket adja, akkor az adott átalakítás nem számít narrowingnak.
A double → float esetén a tartományba esés is
elég, a pontos ábrázolás nem követelmény.
// C++20: ezek list initializationban ill-formedak
int i{2.0}; // lebegőpontos -> egész
char c{1000}; // 1000 nem fér el char-ban
std::vector<int> v{1, 2.0}; // 2.0 -> int
// Ezek viszont megengedettek
constexpr int n = 42;
char c2{n}; // 42 elfér char-ban, és visszakonvertálva 42
float f{1.1}; // double -> float, constant expression, tartományban
unsigned u{42}; // int -> unsigned, 42 visszakonvertálva 42A constant expression kivétel levezetése röviden: a narrowing
tilalmának célja, hogy a lista-inicializálás ne veszítsen el információt
csendben. Ha a forrás értéke fordítási időben ismert, és a cél típusban
ábrázolható, valamint a visszakonvertálás ugyanazt az értéket adja,
akkor a konverzió nem veszít információt; ezért a szabvány megengedi. A
float f{1.1}; esetében a double érték nem
ábrázolható pontosan float-ban, de a szabály a tartományba
esést nézi, nem a bitpontos egyezést. A char c{1000};
esetében viszont a 1000 nem fér el, ezért a kivétel nem
alkalmazható.
A gyakorlatban a fordítók a narrowingöt általában hibaként
diagnosztizálják -std=c++20 mellett, de a diagnosztika
formája és a figyelmeztetés/hiba státusz fordító- és flagfüggő lehet. A
szabvány szerint a kód ill-formed, azaz diagnosztikát kell kapnia; a
„lefordult, tehát legális” következtetés nem biztonságos. A C++23 ezen a
területen nem változtatta meg alapvetően a list-initialization narrowing
szabályát, de a konkrét fordítót és library-t mindig ellenőrizni
kell.
A C++ nem minden változót nulláz automatikusan. Lokális, automatikus
tárolási idejű skalárnál az int x; indeterminált értéket
hagy, és az érték kiolvasása UB. Ugyanez igaz a new int
kifejezésre, míg a new int() nulláz. Statikus tárolási
időnél viszont a nyelv garantálja a zero initializationt:
int global; // zero-initialized: 0
void f() {
int local; // indeterminált; olvasása UB
int zero{}; // value-initialized: 0
int* p = new int; // indeterminált int
int* q = new int(); // nullázott int
delete p;
delete q;
}A default member initializer (DMI) segít elkerülni a „elfelejtett tag
inicializálása” hibát. A DMI akkor érvényesül, ha a konstruktor nem
inicializálja azt a tagot, illetve aggregate initializationnál, ha a tag
kimarad. C++11-ben a DMI még kizárta az aggregate státuszt, C++14 óta
viszont nem; ezért a Config példa C++14/17/20 alatt
aggregate marad, feltéve, hogy a C++20-as aggregate-feltételek többi
része teljesül.
struct Endpoint {
std::string host;
int port = 8080;
bool tls = false;
};Itt a port és a tls DMI-t kap; a
host viszont std::string, amelynek van default
konstruktora, ezért Endpoint e; esetén üres string lesz. Ha
a host skalár lenne, például int fd;, akkor a
DMI hiányában default initializationnál indeterminált maradna.
A következő példa egyetlen fordítási egységben mutatja a fő formákat. A kód C++20-at feltételez; a designated initializer miatt C++17-tel nem fordul. A buildparancsot a példa után adom meg; a kimenet „elvárt/illusztratív”, nem mért eredmény.
#include <iostream>
#include <string>
#include <vector>
struct Point {
int x;
int y;
};
struct Config {
int timeout_ms = 1000;
bool verbose = false;
};
int main() {
int a{}; // value-init: 0
int b = 42; // copy-init
int c(43); // direct-init
int d{44}; // direct-list-init
Point p1{1, 2}; // aggregate init
Point p2{.x = 3, .y = 4}; // C++20 designated
Point p3{}; // minden tag nulla
Config cfg1; // default-init, DMI-k érvényesülnek
Config cfg2{}; // value/list-init, DMI-k érvényesülnek
Config cfg3{500}; // aggregate init: timeout_ms = 500
std::vector<int> v1(3, 7); // három 7-es
std::vector<int> v2{3, 7}; // két elem: 3, 7
std::cout << a << ' ' << b << ' ' << c << ' ' << d << '\n';
std::cout << p1.x << ',' << p1.y << ' '
<< p2.x << ',' << p2.y << ' '
<< p3.x << ',' << p3.y << '\n';
std::cout << cfg1.timeout_ms << ' '
<< cfg2.timeout_ms << ' '
<< cfg3.timeout_ms << ' ' << cfg3.verbose << '\n';
std::cout << v1.size() << ' ' << v2.size() << '\n';
}g++ -std=c++20 -Wall -Wextra -Wpedantic -Wconversion init_forms.cpp -o init_forms
# vagy
clang++ -std=c++20 -Wall -Wextra -Wpedantic -Wconversion init_forms.cpp -o init_formsA várt kimenet alakja: 0 42 43 44, majd
1,2 3,4 0,0, majd 1000 1000 500 0, végül
3 2. Ez a példa nem bizonyítja a fordító viselkedését
minden targeten; a tanuló laborjában a tényleges kimenetet és a
warningokat rögzíteni kell.
Az első és leggyakoribb hiba a
std::vector<int> v{10}; „tíz nulla” szándékkal való
használata. Javítás: std::vector<int> v(10);, ha
méretre van szükség, vagy std::vector<int> v(10, 0);,
ha explicit nullázás a cél. A második: S s(); után azt
várni, hogy objektum jön létre; ez függvénydeklaráció. Javítás:
S s; vagy S s{};. A harmadik:
int x; lokális változó olvasása; javítás
int x{};, illetve ha a „nincs érték” állapot kell,
std::optional<int> (a későbbi optional, variant és visit (31.1) témája). A
negyedik: list initialization használata olyan helyen, ahol az
initializer_list konstruktor átveszi az irányítást;
ilyenkor tudatosan ()-t vagy explicit típust kell
választani. Az ötödik: C++20-as designated initializert C++17-es
buildben használni; a buildprofil és a -std= kapcsoló
ellenőrzése kötelező. A hatodik: azt feltételezni, hogy a DMI minden
konstruktorban érvényesül; valójában ha a konstruktor inicializálja a
tagot, a DMI nem lép be.
A C++ inicializálását négy kérdés mentén érdemes olvasni: van-e
initializer, az () vagy {} alakot
használjuk-e, osztály- vagy skalártípusról van-e szó, és aggregate-e a
típus. A default initialization skalárnál nem ad értéket; a value
initialization nulláz, illetve a nem user-provided default konstruktor
előtt zero initializationt végez. A direct initialization az explicit
konstruktorokat is figyelembe veszi, a copy initialization nem. A list
initialization narrowingöt tilt, elkerüli a most vexing parse-ot, de az
initializer_list konstruktorokat előnyben részesíti. Az
aggregate initialization verziófüggő: C++14 óta a DMI, C++17 óta a
nyilvános bázis, C++20 óta a designated initializer és a zárójeles
aggregate init a fő változások. A biztonságos minta: konfigurációs
aggregate-eknél DMI, skalároknál {}, és minden
{}/() választásnál tudatos overload- és
narrowing-ellenőrzés.
Az alábbi kódban minden deklarációhoz írd le, melyik inicializálási
forma érvényesül, és milyen értéket kapnak a tagok/az objektum.
Feltételezd, hogy a kód függvényen belül van, C++20-ban fordít, és a
Point aggregate.
struct Point { int x; int y; };
Point a;
Point b{};
Point c{1};
Point d(1, 2);
std::vector<int> v1(3);
std::vector<int> v2{3};Készíts egy Endpoint aggregate-et
std::string host;, int port = 8080;,
bool tls = false; tagokkal. Mutasd meg három inicializálási
módon: default initialization, üres list initialization és egy olyan
aggregate initialization, ahol csak a host és a
port kap értéket. Magyarázd el, mi történik a
tls-sel. Ezután adj hozzá egy
Endpoint(std::string, int) konstruktort, és indokold, miért
változik meg C++20-ban az aggregate státusz, valamint hogyan kell
módosítani a példát.
Az alábbi részlet több okból sem fordul C++20-ban. Azonosítsd a
hibákat, írd le a diagnosztika várható okát, és javítsd a kódot a
szándék szerint: S objektumot akarunk létrehozni,
x egész legyen 3, a vektor pedig két elemet tartalmazzon:
1-et és 2-t.
struct S { S(int); };
S s();
int x{3.14};
std::vector<int> v{1, 2.5};A Point a; default initialization: mivel
Point aggregate és nincs felhasználó által deklarált
konstruktora, az implicit default konstruktor fut, de a skalár tagok
default initializationja nem ad értéket. Lokális objektumnál
a.x és a.y indeterminált; olvasásuk UB. A
Point b{}; üres list initialization: aggregate
initialization üres listával, a tagok value initializationt kapnak, így
x = 0, y = 0. A Point c{1};
aggregate initialization: x = 1, a kimaradó y
value initializationnal 0 lesz. A
Point d(1, 2); C++20-ban zárójeles aggregate
initialization, x = 1, y = 2; C++17-ben ez nem
fordulna, mert ott még nincs parenthesized aggregate initialization. A
std::vector<int> v1(3); méret-konstruktor: három
value-initialized int, azaz 0, 0, 0. A
std::vector<int> v2{3}; list initialization: az
initializer_list<int> konstruktor viabilis, ezért egy
elemű vektor lesz, értéke 3. Ellenőrzési feltétel: a
v1.size() == 3, v2.size() == 1, és a
v2[0] == 3; a Point a értékeit nem szabad
kiolvasni.
A kiinduló struktúra:
#include <string>
struct Endpoint {
std::string host;
int port = 8080;
bool tls = false;
};
Endpoint e1; // default-init: host üres, port=8080, tls=false
Endpoint e2{}; // value/list-init: ugyanaz
Endpoint e3{"example.org", 443}; // host, port; tls a DMI-ból falseAz e3 esetében a tls kimarad, ezért a DMI
érvényesül, nem indeterminált érték. Ha hozzáadjuk az
Endpoint(std::string, int) konstruktort, akkor C++20-ban a
Endpoint már nem aggregate, mert van felhasználó által
deklarált konstruktora. Ekkor a
Endpoint e3{"example.org", 443}; a konstruktort hívja, és a
tls csak akkor lesz false, ha a konstruktor
nem inicializálja, de a DMI érvényesül; ha a konstruktor inicializálja,
a DMI nem lép be. A példát úgy kell módosítani, hogy vagy megtartjuk az
aggregate-et (nincs saját konstruktor), vagy a konstruktorban explicitté
tesszük a kívánt defaultot. Ellenőrzési feltétel:
e1.port == 8080, e3.tls == false, és a
konstruktoros változatban a tls értéke a konstruktor
viselkedésétől függ, nem automatikusan az aggregate
initializationtól.
A S s(); most vexing parse: függvénydeklarációt
deklarál, nem S objektumot. Javítás: S s{0};
vagy S s(0);, illetve ha default konstruktor is van,
S s{};. Az int x{3.14}; list initialization, a
double → int átalakítás narrowing, ezért
ill-formed. Javítás a szándék szerint: int x = 3; vagy
int x{3};; ha a 3.14 értéket akarjuk megtartani,
double x{3.14}; kell. A
std::vector<int> v{1, 2.5}; szintén narrowing a
2.5 → int miatt; javítás:
std::vector<int> v{1, 2};, vagy ha a 2.5 is kell,
std::vector<double> v{1.0, 2.5};. Ellenőrzési
feltétel: a javított kód C++20-ban
-std=c++20 -Wall -Wextra -Wpedantic mellett fordul, a
v.size() == 2, és a v elemei a szándékolt
típusban tárolódnak; a S s() helyett tényleg objektum jön
létre.