Why does brace initialization assignment fill variables with garbage?
Asked Answered
S

2

7

I have fallen into the belief that variables are assigned their default values when using brace initialization. But I was wrong.

In the following example:

#include <string>
#include <iostream>

#include <stdint.h>


class A
{
public:
    A() {}
    ~A(){}

    int var1;
    int32_t var2;
    int64_t var3;
    std::string var4;
    double var5;
    float var6;

    std::string info() const {
        return "var1=" + std::to_string(var1) + " " +
               "var2=" + std::to_string(var2) + " " +
               "var3=" + std::to_string(var3) + " " +
               "var4=" + var4 + " " +
               "var5=" + std::to_string(var5) + " " +
               "var6=" + std::to_string(var6) + " " +
               "\n"
               ;
    }
};

int main()
{
    A a;
    std::cout << "Before assigning variables: " << a.info();

    a.var1 = 1;
    a.var2 = 2;
    a.var3 = 3;
    a.var4 = "4";
    a.var5 = 5;
    a.var6 = 6;
    std::cout << "After assigning variables: " << a.info();

    a = {};
    std::cout << "After brace init assignment: " << a.info();
}

Here is the result:

Before assigning variables: var1=0 var2=0 var3=4198240 var4= var5=0.000000 var6=0.000000 
After assigning variables: var1=1 var2=2 var3=3 var4=4 var5=5.000000 var6=6.000000 
After brace init assignment: var1=2114725200 var2=32766 var3=4199416 var4= var5=0.000000 var6=0.000000

To fix it:

  1. If I get rid of the default constructor - the problem goes away.
  2. If a class's member variable is brace-initialized, then it will be assigned 0 or the default value. E.g.:

    class A
    {
    public:
        A() {}
        ~A(){}
    
        int var1{};
        int32_t var2{};
        int64_t var3{};
        std::string var4{};
        double var5{};
        float var6{};
    };
    

Can someone please explain why this happens? What am I missing here?

Starvation answered 27/6, 2019 at 4:30 Comment(1)
class A has a user-defined constructor, so A{} means to call the constructor and nothing more. Maybe you are mixing up this case with other examples where there is no user-defined constructor (in which case A{} can mean to initialize each member variable as if by {}).Leduc
W
9

a = {}; is an assignment, a is assigned from a temporary object constructed from {}. The implicitly-generated assignment will perform member-wise assignment on all the data members, then the point would be how the temporary object is initialized from {}.

This is copy-list-initialization, as the effect, value-initialization is performed.

Otherwise, If the braced-init-list is empty and T is a class type with a default constructor, value-initialization is performed.

As the effect of value-initialization,

1) if T is a class type with no default constructor or with a user-provided or deleted default constructor, the object is default-initialized;

A has a user-provided default constructor, and as the effect of default initialization, that default constructor is used to initialize the temporary object. The user-provided default constructor's body is empty, then for the temporary object, var4 will default-initialized by std::string's default constructor, all the other data members with build-in type will have indeterminate values.

  1. If I get rid of default constructor - the problem goes away.

Then the behavior of value-initialization will change to

(emphasis mine)

2) if T is a class type with a default constructor that is neither user-provided nor deleted (that is, it may be a class with an implicitly-defined or defaulted default constructor), the object is zero-initialized and then it is default-initialized if it has a non-trivial default constructor;

Note the difference here, the temporary object will be zero-initialized at first. Then all the data members with build-in type are initialized to 0 (var4 is still default-initialized).

  1. If class's member variable is brace-initialized, then it will be assigned to 0 or default value.

This is how default initializer list works.

Through a default member initializer, which is simply a brace or equals initializer included in the member declaration, which is used if the member is omitted in the member initializer list

Then all the data members are initialized by the specified initializer; in your sample they're all value-initialized, as the effect, var4 is default-initialized, other members are zero-initialized to 0.

Weissmann answered 27/6, 2019 at 5:46 Comment(0)
A
4
a = {};

This line does not mean that all the variables inside the class get an {} initializer. It instead calls a (not defined, so automatically generated) copy (or move) assignment operator which does a shallow copy/move from an object created with {} (i.e. with uninitialized variables) to the object you have.

var4 seems to be cleared, but it's actually copied/moved from the new object var4, and since std::string has a default constructor, it's empty.

The simple solution to avoid stuff like that is to initialize your non-class variables inside the class, say

class A
{
    int var = 0;
    ...

};
Animation answered 27/6, 2019 at 4:33 Comment(5)
But why does it wipe down string, double and float, but not the ints? :(Starvation
Because it copies from an empty uninitialized object. The string in that object is initialized because it has a default constructor. So var4 is default-constructed. The other variables have an uninitialized state, which means that some may or may not get a 0 value.Animation
How could it be a copy when rhs is a temporary?Lithiasis
@oblivion you can copy from temporariesLeduc
@oblivion that would be a move-assignment if it did. copy-assignment typically receives by value or const reference, which can accept a temporaryLeduc

© 2022 - 2024 — McMap. All rights reserved.