Home > Net >  Trying to understand default constructors and member initialisatioon
Trying to understand default constructors and member initialisatioon

Time:03-20

I am used to initialising member variables in class constructors, but I thought I'd check out if default values are set by default constructors. My tests were with Visual Studio 2022 using the C 20 language standard. The results confused me:

#include <iostream>

class A
{
public:
    double r;
};

class B
{
public:
    B() = default;
    double r;
};

class C
{
public:
    C() {}
    double r;
};

int main()
{
    A a1;
    std::cout << a1.r << std::endl; // ERROR: uninitialized local variable 'a1' used

    A a2();
    std::cout << a2.r << std::endl; // ERROR: left of '.r' must have class/struct/union

    A* pa1 = new A;
    std::cout << pa1->r << std::endl; // output: -6.27744e 66

    A* pa2 = new A();
    std::cout << pa2->r << std::endl; // output: 0

    B b1;
    std::cout << b1.r << std::endl; // ERROR: uninitialized local variable 'b1' used

    B b2();
    std::cout << b2.r << std::endl; // ERROR: left of '.r' must have class/struct/union

    B* pb1 = new B;
    std::cout << pb1->r << std::endl; // output: -6.27744e 66

    B* pb2 = new B();
    std::cout << pb2->r << std::endl; // output: 0

    C c1;
    std::cout << c1.r << std::endl;  // output: -9.25596e 61

    C c2();
    std::cout << c2.r << std::endl; // ERROR: left of '.r' must have class/struct/union

    C* pc1 = new C;
    std::cout << pc1->r << std::endl; // output: -6.27744e 66

    C* pc2 = new C();
    std::cout << pc2->r << std::endl; // output: -6.27744e 66
}

Thanks to anyone who can enlighten me.

CodePudding user response:

A a2();, B b2(); and C c2(); could also be parsed as declarations of functions returning A/B/C with empty parameter list. This interpretation is preferred and so you are declaring functions, not variables. This is also known as the "most vexing parse" issue.

None of the default constructors (including the implicit one of A) are initializing r, so it will have an indeterminate value. Reading that value will cause undefined behavior.

An exception are A* pa2 = new A(); and B* pb2 = new B();. The () initializer does value-initialization. The effect of value-initialization is in these two cases that the whole object will be zero-initialized, because both A and B have a default constructor that is not user-provided. (Defaulted on first declaration doesn't count as user-provided.)

In case of C this doesn't apply, because C's default constructor is user-provided and therefore value-initialization will only result in default-initialization, calling the default constructor, which doesn't initialize r.

CodePudding user response:

MyType name(); // This is treated as a function declaration
MyType name{}; // This is the correct way

CodePudding user response:

Lets see what is happening on case by case basis in your given example.

Case 1

Here we consider the statements:

A a1; //this creates a variable named a1 of type A using the default constrcutor
std::cout << a1.r << std::endl; //this uses the uninitialized data member r which leads to undefined behavior

In the above snippet, the first statement creates a variable named a1 of type A using the default ctor A::A() synthesized by the compiler. This means that the data member r will default initialized. And since r is of built-in type, it will have undeterminate value. Using this uninitilized variable which you do when you wrote the second statement shown in the above snippet is undefined behavior.

Case 2

Here we consider the statements:

A a2(); //this is a function declaration 
std::cout << a2.r << std::endl; //this is not valid since a2 is the name of a function

The first statement in the above snippet, declares a function named a2 that takes no parameters and has the return type of A. That is, the first statement is actually a function declaration. Now, in the second statement you're trying to access a data member r of the function named a2 which doesn't make any sense and hence you get the mentioned error.

Case 3

Here we consider the statements:

A* pa1 = new A; 
std::cout << pa1->r << std::endl; // output: -6.27744e 66

The first statement in the above snippet has the following effects:

  1. an unnamed object of type A is created on the heap due to new A using the default constructor A::A() synthesized by the compiler. Moreover, we also get a pointer to this unnamed object as a result.
  2. Next, the pointer that we got in step 1 above, is used as an initializer for pa1. That is, a pointer to A named pa1 is created and is initialized by the pointer to the unnamed object that we got in step 1 above.

Since, the default constructor was used(see step 1) this means that the data member r of the unnamed object is default initilaized. And since the data member r is of built in type, this implies that it has indeterminate value. And using this uninitialized data member r which you do in the second statement of the above code snippet, is undefined behavior. This is why you get some garbage value as output.

Case 4

Here we consider the statements:

A* pa2 = new A();
std::cout << pa2->r << std::endl;

The first statement of the above snippet has the following effects:

  1. An unnamed object of type A is created due to the expression new A(). But this time since you have used parenthesis () and since class A does not have an user provided default constructor, this means value initialization will happen. This essentially means that the data member r will be zero initialized. This is why/how you get the output as 0 in the second statement of the above snippet. Moreover, a pointer to this unnamed object is returned as the result.

  2. Next, a pointer to A named pa2 is created and is initialized using the pointer to the unnamed object that we got in step 1 above.


Exactly the same thing happens with the next 4 statements related to class B. So i am not discussing the next 4 statements that are related to class B since we will learn nothing new from them. The same thing will happen for them as for the previous 4 statement described above.


Now will consider the statements related to class C. We're not skipping over these 4 statements because for class C there is a user-defined default constructor.

Statement 5

Here we consider the statements:

C c1;
std::cout << c1.r << std::endl;

The first statement of the above snippet creates a variable named c1 of type C using the user provided default constructor A::A(). Since this user provided default constructor doesn't do anything, the data member r is left uninitialized and we get the same behavior as we discussed for A a1;. That is, using this uninitialized variable which you do in the second statement is undefined behavior.

Statement 6

Here we consider the statements:

C c2();
std::cout << c2.r << std::endl;

The first statement in the above snippet is a function declaration. Thus you'll get the same behavior/error that we got for class A.

Statement 7

Here we consider the statements:

C* pc1 = new C;
std::cout << pc1->r << std::endl;

The first statement in the above snippet has the following effects:

  1. An unnamed object of type C is created on the heap using the user provided default constructor A::A() due to the expression new A. And since the user provide default constructor does nothing, the data member r is left uninitialized. Moreover, we get a pointer to this unnamed object as result.

  2. Next, a pointer to C named pc1 is created and is initialized by the pionter to unnamed object that we got in step 1.

Now the second statement in the above snippet, uses uninitialized data member r which is undefined behavior and explains why you are getting some garbage value as output.

Statement 8

Here we consider the statements:

C* pc2 = new C();
std::cout << pc2->r << std::endl;

The first statement of the above snippet has the following effects:

  1. An unnamed object of type C is created on the heap due to new C(). Now since you have specificed parenthesis () this will do value-initialization. But because this time we've a user-provide default constructor, value-initialization is the same as default-initialization which will be done using the user-provide default constructor. And since the user provide default constructor does nothing, the data member r will be left uninitialized. Moreover, we get a pointer to the unnamed object as result.

  2. Next, a pointer to C named pc2 is created and is initialized by the pionter to unnamed object that we got in step 1 above.

Now the second statement in the above snippet, uses uninitialized data member r which is undefined behavior and explains why you are getting some garbage value as output.

  • Related