20.1 Az inicializálás különböző alakjai

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.

20.1.1 Az inicializálás formáinak térképe

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.

20.1.2 Direct, copy és list initialization

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.

20.1.3 Aggregate initialization és verziófüggése

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 initializer

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

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

20.1.4 Most vexing parse és egyértelmű deklaráció

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 initialization

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

20.1.5 Narrowing tiltása és kivételei

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 42

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

20.1.6 Inicializálatlan skalár és default member initializer

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.

20.1.7 Kidolgozott példa: init_forms.cpp

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_forms

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

20.1.8 Tipikus hibák és javításuk

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.

20.1.9 Összefoglaló

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.

20.1.10 Feladatok

20.1.10.1 1. feladat: inicializálási formák osztályozása

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};

20.1.10.2 2. feladat: konfigurációs struktúra és DMI

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.

20.1.10.3 3. feladat: diagnosztika és javítás

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};

20.1.11 Megoldások

20.1.11.1 1. feladat megoldása

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.

20.1.11.2 2. feladat megoldása

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 false

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

20.1.11.3 3. feladat megoldása

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.