What Are the Differences Between C-Like, Constructor, and Uniform Initialization

What are the differences between C-like, constructor, and uniform initialization?

First, I would recommend looking at the following talk by Herb Sutter, in which he gives some recommendations about the subject. The brace-initialization discussion starts at around 23:00.

When you are talking about primitive data types, all 3 yield the same result. I personally prefer sticking with the old int x = 0 syntax, but it comes down to personal preference.

For class types, brace initialization and old-school constructor initialization are not completely interchangeable. For example:

vector<int> v (100); // Creates a 100-element vector
vector<int> v {100}; // Creates a 1-element vector, holding the value 100.

This is because std::vector has a constructor that explicitly defines std::initializer_list as its only argument. Keep in mind that

auto var = {1, 2};

creates a std::initializer_list, with var as its identifier.

The thing about initializer lists is that they provide consistency that is a welcome change from what was available beforehand. For example, if you were to initialize an array in C++, you would use:

int arr[] = {1, 2, 3, 4};

But, if you wanted to initialize a vector<int> with the same elements, you either had to:

  1. Initialize the above arr first and then pass arr and arr + 4
  2. Create the vector and push_back() the elements individually or in a loop.

With C++11, you could just use

vector<int> v = {1, 2, 3, 4}; // Same syntax. Nice! Note that the = is optional

Another instance in which brace initialization is helpful is that it provides a workaround to C++'s most vexing parse. From the talk, assume that we have two classes, origin and extents, whose instances can be passed to construct another object of type rectangle. The following statement:

rectangle w(origin(), extents());

doesn't allow you to create a rectangle object using origin and extents temporaries, because that statement is parsed as a function declaration. Tsk tsk. So normally, you would have to do:

origin  o;
extents e;
rectangle w(o, e);

With brace initialization, you can create them on the fly, and

rectangle w {origin(), extents()};

will work as intended, i.e. passed to the constructor which is overloaded with an origin object as it's first argument and an extents object as the second.

The rule is for objects, use brace initialiation unless you have a reason not to.

Initializer list vs Constructor assignment vs variable defining

This excerpt has been taken from "Inside the C++ Object Model" by Stanley B. Lippman.

You must use the member initialization list in the following cases in
order for your program to compile:

1. When initializing a reference member

2. When initializing a const member

3. When invoking a base or member class constructor with a set of arguments

4. A few efficiency cases. (Here the program is correct w/o member initialization list)

For points 1-3, member initialization list is a must.

For point 4, it is not compulsory.

For example(point 4), given :

class Word {
String _name;
int _cnt;
public:
// not wrong, just naive ...
Word() {
_name = 0;
_cnt = 0;
}
};

This implementation of the Word constructor initializes _name once, then overrides the initialization with an assignment, resulting in the creation and the destruction of a temporary String object.

A significantly more efficient implementation would have been coded:

// preferred implementation
Word::Word : _name( 0 )
{
_cnt = 0;
}

Due to this optimisation, a lot of people prefer member initialization list, as a default approach to write constructors.

// some insist on this coding style
Word::Word()
: _cnt( 0 ), _name( 0 )
{}

A reasonable question to ask at this point is, what actually happens to the member initialization list?

The compiler iterates over the initialization list, inserting the initializations in the proper order within the constructor prior to any explicit user code.

For example, the previous Word constructor is expanded as follows:

// Pseudo C++ Code
Word::Word( /* this pointer goes here */ )
{
_name.String::String( 0 );
_cnt = 0;
}

Note : The order in which the list entries are set down is determined
by the declaration order of the members within the class declaration,
not the order within the initialization list. In this case, _name is
declared before _cnt in Word and so is placed first.

So coming back to your question :

class B is fine(since you are using primitive datatypes).

class A will generate the same code as class B

As for class C, the default constructor is first called, and then initialization of width and height would happen. This method should be preferred when there are going to be more than 1 constructor, and for each constructor width and height need to be defaulted to your desired values.

However, since the advent of C++11, and use of {} as uniform initialization, a more recommended approach for writing class C would be :

class C
{
public:
int width {500};
int height {300};
};

Uniform Initialization and default constructor arguments in C++11

That's a bug in the compiler. C{} calls the default constructor to create a temporary which is used to copy-initialize the object c. C(int a = 1, int b = 2) is obviously a default one so it should use that. Does switching the initialization order to the order declared in the class help (probably not, but just a guess)? It seems the intel compiler isn't considering your ctor with default arguments as the default one.

C(int a = 1, int b = 2) : n{0,1,2,3,4}, a_{a}, b_{b} {};

Why uniform initialization works without explicit constructor

The purpose of uniform initialization is to be a single means of initializing any object which can be initialized. It therefore can pick and choose the appropriate initialization mechanism to use internally for a particular type.

Point is an aggregate. In C++98/03, it therefore could be initialized with aggregate initialization, much like an array.

Point p = {3, 4};

That was legal in C++98/03.

In C++11, aggregate initialization is one of the possible ways for uniform initialization to initialize a variable. If the type being initialized is an aggregate, then the members of the braced-init-list are used to initialize it via aggregate initialization. But aggregate initialization in C++98/03 only worked in Typename var = braced-init-list; form. Uniform initialization allows all of these forms to use it as well:

Point p{3, 4};

void Func(const Point &p);
Func({3, 4});

Point Func2()
{
return {3, 4};
}

All of these use uniform initialization to initialize a Point aggregate. However, since it's using uniform initialization syntax, you can later change Point to add a constructor. And as long as you add a constructor that takes two integers (and don't add an initializer_list constructor that takes integers), all your code will work just fine.

Usage of Uniform Initialization Syntax

In case of simple types, like int in your case, there is no difference. However, initialization of std::vector from STL will be completely different

std::vector<int> v1(3,1); // v1 consists of: 1, 1, 1
std::vector<int> v2{3,1}; // v2 consists of: 3, 1

Have a look at this answer if you want to see why generally brace {} initialization is better, however quoting from Scott Meyer's book Effective Modern C++, which I highly recommend:

[...] So why isn’t this Item
entitled something like “Prefer braced initialization syntax”?
The drawback to braced initialization is the sometimes-surprising behavior that
accompanies it. [...]

Uniform Initialization to call a constructor other than initializer list

This code:

MyClass a2{ (1, 2) };

could not call the 2 argument constructor. What is happening is that in the expression (1,2), the 1 is evaluated before the comma operator, it is discarded, and the result of the expression is 2. This obviously calls the initializer_list constructor.

Enable warnings, e.g. with -Wall and the compiler will tell you about this.


Note that the same thing happens in the vector<int> a{ (1, 2) }; call. The 1 is discarded, and the constructor of vector that takes a single argument is invoked.



Related Topics



Leave a reply



Submit