Mélyvíz: A C++ és a zárójelek
A C++ egy régi nyelv, az 1980-as években született meg. 1998-ban sikerült szabványosítani, ezért ez a változat a C++98 nevet viseli, ez a klasszikus C++. Azóta több új szabványt is bevezettek: a C++11, C++14 és a C++17-et, és tervezetként a C++20 is elkészült. A C++11-től kezdve beszélhetünk a modern C++-ról. Mivel a C++ egy olyan viszonylag régi nyelv, amit folyamatosan modernizálnak és közben igyekeznek a visszamenőleges kompatibilitást megőrizni, ez rányomja a bélyegét: elég bonyolult és többféle szintaktika él egymás mellett.
Nézzük meg ezt egy látszólag egyszerű témán keresztül: kezdőértékadás változóknak (inicializálás). Először vegyünk egy szimpla int változót:
int x(0); // az x legyen nulla
int y = 0; // az y legyen nulla
int z{ 0 }; // a z is legyen nulla
int v = { 0 }; // a v is legyen nulla
Mind a négy változat helyes, lefordul és ugyanazt csinálja. Kicsit túl sok lehetőségünk van ugyanarra, nem? Ezért a C++11-től kezdve bevezették az egységes inicializálást (uniform initialization), amely szerint használjuk mindig a { }
-t kezdőértékadáskor. Ez a forma használható osztályokon belül is:
class Widget {
private: int x{ 0 }; // az x nem-statikus adattag legyen nulla
};
Nem másolható objektumoknál is:
std::atomic<int> ai{ 0 };
További előnyei is vannak:
Nem engedi meg a szűkítő konverziót, mert az adatvesztéshez vezethet:
double x, y, z
//x,y,z-be kerül valami érték
int sum1{ x + y + z }; //hiba: double -> int konverzió lenne
Érdekességként: a fenti példában a ( )-es és az =-t használó kezdőértékadás megengedi, hogy x+y+z belerakjuk a sum1-ba.
Elkerülünk vele egy bosszantó hibát (most vexing parse):
Widget w1(10); //konstruktorhívás a 10 kezdőértékkel
Widget w2(); //NEM konstruktorhívás!
C++-ban a második sor nem paraméter nélküli konstruktorhívást jelent, hanem egy Widget visszatérési értékű, w2 nevű függvény deklarációját. Ha viszont ezt a formát használjuk:
Widget w{ 10 };
Widget w3{};
Itt már a második is konstruktorhívás.
Tehát úgy tűnik, a {}
-es forma tökéletes, használjuk mindig ezt és kész! Sajnos azért nem ilyen egyszerű az élet… Nézzük meg ezt a két sort, ahol std::vector
generikus tárolót használunk, ami egy dinamikusan nyújtózkodó tömb:
std::vector<int> v1(10, 20);
std::vector<int> v2{ 10, 20 };
Itt is van ()
és {}
forma. Vajon itt is ugyanazt jelentik? Hát nem... A v1 vectornál a kétparaméteres konstruktort hívjuk meg és ez létrehoz egy 10 elemű vectort, amiben 10 darab 20-as szám lesz. A v2 vectornál viszont az inicializációs listás konstruktor hívódik meg (initializer_list) és kételemű vector jön létre, amiben a 10 és a 20 lesz eltárolva.
Az inicializációs listás konstruktor nagyon „erőszakos”, ha van ilyen az osztályban, akkor elnyomja még a jobban illeszkedő többi konstruktort is, amennyiben az objektum után {}
áll. Ez történik a következő példában is:
class Widget {
public: Widget(int i, bool b); // 1.
Widget(int i, double d); // 2.
Widget(std::initializer_list<long double> il); // 3.
};
Widget w1(10, true); // az első konstruktort hívja meg
Widget w2{ 10, true }; // a 3.-at hívja meg és a 10-ből és a true-ból long double-t csinál
Widget w3(10, 5.0); // A 2. hívja meg
Widget w4{ 10, 5.0 }; // ez is a 3.-at hívja meg
Ez a jelenség főleg a sablonosztályok íróit állítja lehetetlen döntés elé. Akár ()
-lel, akár {}
-lel hozzák létre a sablonban a szükséges objektumokat, a sablon használatakor mindkettő okozhat gondot a felhasználónak. A C++ programozók egy része a {}
-re szavaz, a fentebb leírt előnyök miatt, de tudja, hogy pl. a vector használatánál jobb a ()
vagy a std::vector v2 = { 10, 20 };
forma. A programozók másik része a ()
-et részesíti előnyben, mert így továbbviheti a klasszikus C++ szintaktikát, nem fog bezavarni az inicializációs listás konstruktor és persze amikor elkerülhetetlen, akkor használja a {}
-t: std::vector<int> v2{ 10, 20,30,40 };
vagy std::vector<int> v2 = { 10, 20, 30, 40 };
Kicsit olyan ez, mit a szóköz kontra tabulátor vita...
Forrás: Scott Meyers: Effective Modern C++
A szerzőről: https://progsuli.hu/ki-vagyok/