Why do we need the highlighted sentence below in [dcl.init]/(6.2)?

Viewed 97

[dcl.init]/(6.2)

If T is a (possibly cv-qualified) non-union class type, its padding bits are initialized to zero bits and each non-static data member, each non-virtual base class subobject, and, if the object is not a base class subobject, each virtual base class subobject is zero-initialized;

AFAICT, the sentence above is confusing and dispensable. What do they mean by the object that is not a base class subobject?

3 Answers

The writing of a technical specification is a bit like programming. [dcl.init]/6 can therefore be thought of as a kind of function: the function which performs zero-initialization on an object. This function gets called when a complete object is zero-initialized.

Here's some pseudo-code for the general structure of [dcl.init]/6:

void zero_init(T &t)
{
  if(zero_init_1(t)) return; //Executes rule 6.1; returns false if rule 6.1 doesn't apply to T.
  if(zero_init_2(t)) return; //Executes rule 6.2; returns false if rule 6.2 doesn't apply to T.
  if(zero_init_3(t)) return; //Executes rule 6.3; returns false if rule 6.3 doesn't apply to T.
  ...
}

The important thing here is zero_init_2, the code that represents section 6.2:

bool zero_init_2(T &t)
  if(T is class && T is !union)
  {
    zero_init_padding_bit(t);
    for(auto &base : non_virtual_bases(t))
      zero_init(base);
    for(auto &member : members(t))
      zero_init(member);

    if(!is_base_class(t))
      for(auto &base : virtual_bases(t))
        zero_init(base);

    return true; //I did initialization here.
  }
  return false; //No initialization done here.
}

You suggested:

I would replace "and, if the object is not a base class subobject, each virtual base class subobject is zero-initialized" by "and each virtual base class subobject of the most derived object is zero-initialized".

The code for this would be as follows:

bool zero_init_2(T &t)
  if(T is class && T is !union)
  {
    zero_init_padding_bit(t);
    for(auto &base : non_virtual_bases(t))
      zero_init(base);
    for(auto &member : members(t))
      zero_init(member);

    auto &u = get_most_derived_object(t)
    for(auto &base : virtual_bases(u))
      zero_init(base);

    return true; //I did initialization here.
  }
  return false; //No initialization done here.
}

In your suggested version, there is no conditional. So there is no conditional in the pseudo-code either. But zero_init is a function that can invoke zero_init_2; that is, it is recursive.

This means that virtual base classes could be zero-initialized multiple times. Indeed, it would be infinitely recursive on any type with virtual base classes. get_most_derived_object will return the most derived object. The non-union virtual base class subobjects of this object include... the one we're currently initializing. So we will recursively reinitialize ourselves, never terminating.

The original wording ensures that the virtual base classes will only be zero-initialized once. Yours does not.

"The object" is of type T. This sentence simply applies the logic of initializing virtual bases to zero-initialization. It is always the most derived object that initializes virtual bases. For instance.

class A {
  //
};

class B : public virtual A {
  //
};

class C : public B {
  //
};

static B b; // The zero initialization of `b` also zero initializes an `A` sub-object
static C c; // The zero initialization of the 'B' sub-object **does not** include
            // zero-initialization of an `A` sub-object.

The highlighted sentence serves a purpose here, and is not superfluous.

Example A:

#include <iostream>

struct VirtualBase
{
    int x;
};

struct Derived : virtual VirtualBase
{};

struct Complete : Derived, virtual VirtualBase
{
    Complete()
        : VirtualBase{42}
        , Derived()
    {}
};

int main()
{
    Complete c;
    std::cout << c.x;
}

How does the initialization of Complete c; work?

  • We start with default-initialization of the Complete object. This calls the default ctor.
  • The default-ctor starts by initializing the virtual base classes (class.base.init).
    • We do an aggregate-initialization of the VirtualBase subobject and set the int to 42.
  • Then, we initialize the Derived subobject. The form of initialization is Derived() which causes value-initialization of the subobject.

if the object is not a base class subobject, each virtual base class subobject is zero-initialized;

The Derived object we're initializing is a base class subobject of Complete. Hence we do not zero-initialize the VirtualBase base class object of Derived.

Without the subclause "if the object is not a base class subobject", we would have zero-initialized the VirtualBase subobject of Derived. This would have meant to zero-initialize the int member, setting it to 0. Because this happens after we already wrote 42 into it, setting it to 0 would have overwritten the value.


Example B:

#include <iostream>

struct VirtualBase
{
    int x;
};

struct Derived : virtual VirtualBase
{};

struct Complete : Derived, virtual VirtualBase
{
    Complete()
        : Derived()
    {}
};

int main()
{
    Complete c;
    std::cout << c.x;
}

The constructor of Complete lacks any mem-initializer for VirtualBase, so it will default-initialize that virtual base class. Derived on the other hand is value-initialized, which causes zero-initialization. Since Derived is a base class subobject here, it will not zero-initialize the virtual base. It remains uninitialized and we get UB in the cout << c.x.

You get some funny output with gcc and clang but unfortunately I couldn't get the sanitizers to report on it. Live demo

Related