A few weeks ago I had a discussion with a team mate about the difference in writing default constructed class instances in C++. The point was, how to write the construction instruction correctly and if it effects the member initialization.

At first a simple test class:

# include <string>
# include <iostream>

class ConstructorTest {
  public:
    std::string val = "default";
    ConstructorTest():val("constructed") {}
};

For (default) constructing a class instance on stack you currently have two possibilities:

ConstructorTest t1;
ConstructorTest t2{};

For heap construction there is one more:

ConstructorTest *p = new ConstructorTest();
// ...
delete p;

But what is difference? Lets try it:

ConstructorTest t1;
std::cout << "1: " << t1.val << std::endl;

ConstructorTest t2{};
std::cout << "2: " << t2.val << std::endl;

ConstructorTest *p = new ConstructorTest();
std::cout << "3: " << p->val << std::endl;
delete p;

Not really surprisingly the output is

1: constructed
2: constructed
3: constructed

As seen in all three variants the default constructor is called. Removing the default constructor definition leads to using the default values as given by the class definition:

class ConstructorTest {
  public:
    std::string val = "default";
};

And the output with the test from above will be:

1: default
2: default
3: default

In the upper example the only class member val was always initialized by defining a default value directly at the class definition. In fact this is a construction call in every case, so you could also write:

class ConstructorTest {
  public:
    std::string val {"default"};
};

There is absolutely no difference, in both cases the constructor gets called, and even if it looks like, it is not an assignment operation! If the initialization is done by copy or move constructor depends on the member type - if possbile, move is preferred.

And you should always init members (if possible).

Lets use another integer member i:

class ConstructorTest {
  public:
    int i;
};

int main() {
    ConstructorTest t1;
    std::cout << "1: " << t1.i << std::endl;
    ConstructorTest t2{};
    std::cout << "2: " << t2.i << std::endl;
}

What happens now? Depending on your compiler i may be a random value, here an example using MSVC 2022:

1: 2031432360
2: 4218822

The problem in this case isn’t the missing constructor of our class but the missing initialization of the member i. And here it comes to which default construction convention is the better one:

class ConstructorTest {
  public:
    int i{};
};
1: 0
2: 0

By using the empty curly brackets you can default construct every type - no matter if regular, complex or whatever - as long as it is default constructible.

So a good general rule is to always use curly brackets for initialization, even if not necessary.