C++ and modern initializations

int x=0; or int x(0); or int x{0};

·

6 min read

in this article, we are going to move to modern c++, after we make sure we understand the deduced type and why it is preferred sometimes to use auto instead of type interface only in the case of bool-where the output of the auto is an object.

Widget W1;    // call default constructor  
Widget W2=W1;   // not an assignment, call copy ctor
W1=W2;        // an assignment, call copy operator=

Previously, in the old C++, we had no way to express STL container holding values while initializing. but now we can creat any container 🫙 holding some values. using braces.

std::vector<int>v{1,2,3,4,5} // now the vector hold number from 1:5

braces can be used to specify the default initialization values for non-static data. but not with parentheses.

class widget{
private:
int x{0}; // fine 
int y=0; // fine 
int z(0); // error
}
// we can use the barces to intialize the atomic objects
std::atomic<int>ati{0};
std::atomic<int>ati(0);
std::atomic<int>ati=0 // error!

Uniform initialize

it is so called so far the uniform, only braces can be used everywhere.


Narrowing conversions.

let's look at an example and check the output of each case.

double x,y,z;
int sum{x+y+z};   // error sum of doubles may not be expressible as int
int sum(x+y+z). // oky narrwo the value 
int sum=x+y+z // still okay

this type of error is protection as int may lead to an overflow resulting in non-expected behavior.

vexing parse

can you produce the output of the following code

// Online C++ compiler to run C++ program online
#include <iostream>
using namespace std;
class widget{
    public:
    widget(int x){

        cout<<"I am constructor";
    }
    widget(){
        cout<<"I am default constructor"<<endl;
    }
};
int main() {
    // Write C++ code here
    widget w1(10);  // call the constructor and print the first const
    widget w2();  // shoudl print I am default cnst. but no, it decalre a function instead. 
// to be able to call the def const you should use braces
widget w3{}; // prunt I am def const. as expected.
    return 0;
}

why braces initialization is good for practice.

  1. prevents implicit narrowing conversions.

  2. immune to most C++'s most vexing parse.

why not called the mother of the initialization

or in other words what are the drawbacks of the braces? there are two cases where most C++ rules have an exception.

  1. std::initializer_lists,

  2. constructor overload.

in constructor calls both parentheses and braces have the same meaning as long as std::initializer_list parameters are not involved:

class Widget {
public:
  Widget(int i, bool b);
  Widget(int i, double d);
};
Widget w1(10, true);  // calls the first ctor
Widget w2{10, true};  // the same 
Widget w3(10, 5.0);   // calls the second ctor
Widget w4{10, 5.0};    // calls the second ctor as excpected

it seems fine so far but if we add a constructor with the parameter of type std::initializer_list then things change a little.

Widget(std::initializer_list<long double> il); // added ...
};
Widget w1(10, true);  // calls the first ctor
 Widget w2{10, true};        // calls the std::initializer_list ctor
                               // 10 and true convert to long double
Widget w3(10, 5.0);            // calls the seconf ctor as excpected
 Widget w4{10, 5.0};            // also calls std::initializer_list ctor and convert 10 and 5.0 to long double

this problem seems radical but even in normal copy and move constructor can be hijacked std::initializer_list constructors:

class Widget {
public:
  Widget(int i, bool b);
  Widget(int i, double d);
  Widget(std::initializer_list<long double> il);
  operator float() const;
 };
widget w5(w4); // calls the copy ctor
widget w6{w4}; // call std::initializer_list constructors and convert w4 to float and float to long double 
widget w7(std::move(w4)); // calls mov ctor
widget w8{std::move(w4)}. // calls the std::initializer_list ctor for the same as w6.

as you can see the std::initializer_list is so strong, it previals even if the best-match std::tializer_list ctor can't be called.

   class Widget {
   public:
     Widget(int i, bool b);                   // as before
     Widget(int i, double d);                 // as before
Widget(std::initializer_list<bool> il); // element type is // now bool
};
Widget w{10, 5.0}; // error requires narrwoing conversions. to convert the 10,5.0 into bool and use the std::initializer_list ctor

Here, compilers will attempt to invoke the function Object() { [native code] } that accepts a std::initializer listbool> rather than the first two constructors, the second of which delivers an exact match on both parameter types. It would be necessary to convert an int (10) and a double (5.0) to bools to call that function Object() { [native code] }. Because braced initializers forbid narrowing conversions, both conversions would be narrowing since bool cannot precisely represent either value. As a result, the call is incorrect and the code is rejected. but there is always a way to overcome this problem by adding a non-conversion method. for example, you can't convert from int or bool to string- I mean narrowing or using static or dynamic casting. so the braces can be used again as normal

   class Widget {
   public:
     Widget(int i, bool b);               // as before
     Widget(int i, double d);             // as before
     // std::initializer_list element type is now std::string
Widget(std::initializer_list<std::string> il);
... // no implicit conversion function

};
Widget w1(10, true); // uses parens, still calls first ctor
Widget w2{10, true}; // uses braces, now calls first ctor
Widget w3(10, 5.0); // uses parens, still calls second ctor
Widget w4{10, 5.0}; // uses braces, now calls second ctor

but this defect or drawback can be used to force a call to std::initalizer_list by invoking braces in the call of ctor.

Widget w4({}); // calls std::initializer_list ctor with empty list
Widget w5{{}}; // ditto

do I use this in everyday life?

more than you think, std::vector has a non-std::initializer_list constructor that allows you to specify the size and the container value of each element. and it also has another std::initializer_list that takes a list of initial values of the vector.

std::vector<int>vec(10,20); // uses non-std::initializer_list ctor: creat 10-elemnt 
                            // std::vector, all elemnts have value of 20

std::vector<int>vec2{10,20} // call std::initializer_list
                            // creat 2-elemnts std::vector, value are 10,20

to make full use of the above rule you need to make sure the default ctor is not affected by braces or parentheses as a user who is not aware of such a restriction. like the usage of the vector as they reviled two ways of initializing a vector with two different ways.

Things to remember

  • Braced initialization is the most widely usable initialization syntax, it prevents narrowing conversions, and it’s immune to C++’s most vexing parse.

  • During constructor overload resolution, braced initializers are matched to std::initializer_list parameters if at all possible, even if other constructors offer seemingly better matches.

  • An example of where the choice between parentheses and braces can make a significant difference in creating a std::vector<numeric type\> with two arguments.

  • Choosing between parentheses and braces for object creation inside templates can be challenging.