{}-Initialization
The initialization of variables was unified in C++11. The rule is quite simple. {}-Initialization is always applicable.
Always applicable
For simplicity reasons I will speak in the rest of the post about {}-Initialization, although I mean uniformed initialization with {}. But before I speak about two crucial implications of the {}-Initialization in safety-critical software I will show a few special use cases. This uses cases that require C++11.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
// uniformInitialization.cpp #include <map> #include <vector> #include <string> // Initialization of a C-Array as attribute of a class class Array{ public: Array(): myData{1,2,3,4,5}{} private: int myData[5]; }; class MyClass{ public: int x; double y; }; class MyClass2{ public: MyClass2(int fir, double sec):x{fir},y{sec} {}; private: int x; double y; }; int main(){ // Direct Initialization of a standard container int intArray[]= {1,2,3,4,5}; std::vector<int> intArray1{1,2,3,4,5}; std::map<std::string,int> myMap{{"Scott",1976}, {"Dijkstra",1972}}; // Initialization of a const heap array const float* pData= new const float[3]{1.1,2.2,3.3}; Array arr; // Defaut Initialization of a arbitrary object int i{}; // i becomes 0 std::string s{}; // s becomes "" std::vector<float> v{}; // v becomes an empty vector double d{}; // d becomes 0.0 // Initializations of an arbitrary object using public attributes MyClass myClass{2011,3.14}; MyClass myClass1= {2011,3.14}; // Initializations of an arbitrary object using the constructor MyClass2 myClass2{2011,3.14}; MyClass2 myClass3= {2011,3.14}; } |
First things first. The direct initialization of the C array, the std::vector, and the std::map (lines 32 – 34) is relatively easy. In the case of the std::map, the inner {}-pairs are the key and value pairs. The following particular use case is the direct initialization of a const C array on the heap (line 36). Special about the array arr in line 39 is that C arrays can be directly initialized in the constructor initializer (line 10). The default initialization in lines 42 to 45 looks entirely innocent. But if I use round brackets instead of curly brackets, the most vexing parse will happen. That does not sound good. Why? Wait for the next section. I directly initialize in lines 48 and 49 the public attributes of the objects. It’s also possible to call the constructor with curly braces (lines 52 and 53).
auto
Always applicable? Yes, but you have to keep a particular rule in mind. If you use automatic type deduction with auto in combination with an {}-initialization, you will get a std::initializer_list.
auto initA{1}; // std::initializer_list<int> auto initB= {2}; // std::initializer_list<int> auto initC{1, 2}; // std::initializer_list<int> auto initD= {1, 2}; // std::initializer_list<int>
This behavior will change very likely in C++17.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
auto initA{1}; // int auto initB= {2}; // std::initializer_list<int> auto initC{1, 2}; // error, no single element auto initD= {1, 2}; // std::initializer_list<int>
I don’t like this change. The C++11 semantic is quite clear to me. I will get an initializer list if I use {}-initialization with auto. With C++17, I have to keep the two exceptions with auto in my head.
- It makes a difference if you use direct or copy initialization.
- It makes a difference if you use {}-initialization with one or more elements.
Most vexing parse
But what does that mean? It seems to be a well-known expression: https://en.wikipedia.org/wiki/Most_vexing_parse. The story is quickly told. Most C++ developers know them personally.
At first, a small program shows the issue.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// parse.cpp #include <iostream> struct MyInt{ MyInt(int i):i(i){} MyInt():i(0){} int i; }; int main(){ MyInt myInt(2011); MyInt myInt2(); std::cout << myInt.i << std::endl; std::cout << myInt2.i << std::endl; } |
MyInt in lines 5 – 9 is a simple wrapper for the natural number i. The constructor in line 6 sets i to an explicit value. Conversely, the default constructor in line 7 sets i to 0. So far, so good. I use both constructors in lines 14 and 15 and display the values of i. Compile and run; that’s all I have to do.
But wait. The program does not compile.
The error message is not so meaningful. The compiler can interpret the expression in line 15 as a call of a constructor or as a declaration of a function. In case of doubt, it interprets the expression as a function declaration. The slightly modified program shows it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// parseResolved.cpp #include <iostream> #include <typeinfo> struct MyInt{ MyInt(int i):i(i){} MyInt():i(0){} int i; }; MyInt myFunction(); int main(){ MyInt myInt(2011); MyInt myInt2(); std::cout << typeid(myInt2).name() << std::endl; std::cout << typeid(myFunction).name() << std::endl; } |
I declare in line 12 the function myFunction that has no arguments and returns a type MyInt. myFunction has the same signature as the function declarations in line 17. Thanks to the typeid operator, the program’s output shows precisely that.
The solutions to the problem are quite easy. If I use curly braces in lines 12 and 17, the compiler will not interpret both lines as the function declaration. I already used this characteristic of the {}-initialization in the first example of this post (line 42 – 45).
But now the main topic of this post, which is so precious for software in safety-critical systems: preventing narrowing.
Preventing narrowing
Narrowing or, more precisely, narrowing conversion is an implicit conversion of arithmetic values, including a loss of accuracy. That sounds extremely dangerous.
The following example shows the issue with the classical initialization for fundamental types. It doesn’t matter whether I use direct initialization or assignment.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// narrowing.cpp #include <iostream> int main(){ char c1(999); char c2= 999; std::cout << "c1: " << c1 << std::endl; std::cout << "c2: " << c2 << std::endl; int i1(3.14); int i2= 3.14; std::cout << "i1: " << i1 << std::endl; std::cout << "i2: " << i2 << std::endl; } |
The output of the program shows two issues. First, the int literal 999 doesn’t fit into the type char; second, the double literal doesn’t fit into the int type.
That is not possible with {}-initialization.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// narrowingSolved.cpp #include <iostream> int main(){ char c1{999}; char c2= {999}; std::cout << "c1: " << c1 << std::endl; std::cout << "c2: " << c2 << std::endl; int i1{3.14}; int i2= {3.14}; std::cout << "i1: " << i1 << std::endl; std::cout << "i2: " << i2 << std::endl; } |
The program is ill-formed because of the narrowing with {}-Initialization the compiler has at least to diagnose a warning. Although the program is ill-formed, the compiler has not rejected it.
But now, the maximum confusion with GCC begins. It makes a difference whether I use GCC 4.7 or GCC 4.8. GCC 4.7 rejected the program; GCC 4.8 only provides a warning. With GCC 5.1 we get an error. Don’t you believe me? Try it out with the online compiler https://gcc.godbolt.org/. The clang++ compiler is much more predictable. Therefore my tip. Compile your program in such a way that narrowing is an error. So did I. I used the flag -Werror=narrowing, and GCC 4.8 complains about the program.
A small remark at the end. The expression char c3{8} is indeed no narrowing because eight fits the type char. The same holds for char c3= {8}.
What’s next?
C++11 got with static_assert the type-traits library two powerful features for checking the source code at compile time. In the next post, I will have a deeper look into static_assert and its combination with the functions of the type-traits library.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
Leave a Reply
Want to join the discussion?Feel free to contribute!