Search code examples
c++c++11value-initialization

Value initialization of nested classes


By the rules of value initialization. Value initialization occurs:

1,5) when a nameless temporary object is created with the initializer consisting of an empty pair of parentheses or braces (since C++11);

2,6) when an object with dynamic storage duration is created by a new-expression with the initializer consisting of an empty pair of parentheses or braces (since C++11);

3,7) when a non-static data member or a base class is initialized using a member initializer with an empty pair of parentheses or braces (since C++11);

4) when a named variable (automatic, static, or thread-local) is declared with the initializer consisting of a pair of braces.

Trivial example

struct A{
    int i;
    string s;
    A(){};
};

A a{} 
cout << a.i << endl // default initialized value

without explicitly declared constructor and left with defaulted default ctor // compiler generated one we get.

struct A{
    int i;
    string s;

};
A a{};
cout << a.i << endl // zero-initialized value

However using antoher struct.

struct A{
    int i;
    string s;

};

struct B{
    A a;
    int c;
};

B a{};
cout << a.a.i << endl // default initialized , even tho we did not , int struct B , declared A a{}.

The value of a.i is zero-initialized, even without using {} / () construct, which goes against rules ( if i am not mistaken ).

Using same logic on struct B:

struct A{
    int i;
    string s;

};

struct B{
    A a;
    int c;
};

B b;
cout << b.c << endl; // default inicialized

We get behavior according to rules.

The last example:

struct A
{
    int i;
    A() { } 
};

struct B { A a; }; 

std::cout << B().a.i << endl;

B().a.i is also zero-initialized while we explicitly declared constructor and its not deleted.

Why are the these values getting zero-initialized? By rules stated here they should be default-initialized not zero-initialized.

Thanks for answers.


Solution

  • It's the difference between A being an aggregate or not.

    [dcl.init.aggr] (Emphasis mine)

    An aggregate is an array or a class (Clause 9) with no user-provided constructors (12.1), no brace-or-equal initializers for non-static data members (9.2), no private or protected non-static data members (Clause 11),

    So when A has no declared constructors, saying A a{} has the effect of aggregate initialization

    which will construct each member with an empty initialization list:

    If there are fewer initializer-clauses in the list than there are members in the aggregate, then each member not explicitly initialized shall be initialized from an empty initializer list

    So you get int{} and std::string{} which will value-initialize the integer member to zero.

    When you do provide a default constructor, the aggregate property is lost and the int member remains uninitialized, so accessing it is considered undefined behavior.


    To be specific:

    This code is undefined behavior when accessing a.i because you provided a user-defined constructor, so the int i field remains uninitialized after construction:

    struct A{
        int i;
        string s;
        A(){};
    };
    
    A a{} ;
    cout << a.i << endl;
    

    And this code exhibits undefined behavior when accessing b.c because you did not perform list initialization on B b:

    struct B{
        A a;
        int c;
    };
    
    B b;
    cout << b.c << endl;
    

    All the other code is okay, and will zero-initialize the integer fields. In the scenarios where you are using braces {}, you are performing aggregate-initialization.

    The last example is a little tricky because you're performing value-initialization. Since B is an aggregate, it gets zero-initialized ([dcl.init]), in which:

    each base-class subobject is zero-initialized

    So again you're okay accessing the integer member of the A subobject.