0
votes

Consider a diamond inheritance graph (i.e., virtual base class). We know from previous questions that on construction the most derived class directly calls the default (0-arg) constructor of the (virtual) base.

But we also know from answers to the previous question (e.g., here that if the "middle" classes in the diamond have constructors that are used by the most-derived class and those constructors "call" non-default constructors of their (virtual) base class (via the initialization list) then that is not respected … though the bodies of the "middle" classes' constructors are executed.

Why is that? I would have thought it should be a compile error. (Detected, of course, when the most-derived class is declared and the diamond is created.)

I'm looking for two things:

  • where in the standard is this specified?
  • does this kind of explicit-yet-ignored code happen anywhere else in the language?

Code sample of what I'm talking about follows its actual and expected outputs below:

B 0arg-ctor
Mf 0arg-ctor
Mt 0arg-ctor
useD

expected output:

ERROR: (line 19) struct `D` creates a diamond inheritance graph where an explicitly
    written invocation of a virtual base class constructor is ignored (for base 
    classes `Mf`and `Mt` and ancestor virtual base class `B`

code:

#include <iostream>
using namespace std;

struct B {
    B() noexcept { cout << "B 0arg-ctor" << endl; };
    B(bool) noexcept { cout << "B 1arg-ctor" << endl; };
};

struct Mf : public virtual B
{
    Mf() : B(false) { cout << "Mf 0arg-ctor" << endl; }
};

struct Mt : public virtual B
{
    Mt() : B(true) { cout << "Mt 0arg-ctor" << endl; }
};

struct D : public Mf, public Mt { };

void useD(D) { cout << "useD" << endl; }

int main()
{
    D d;
    useD(d);
    return 0;
}
4
If you wanted chapter & verse cited from the standard, you should add the language-lawyer tag.Eljay
@Eljay - thanks, done.davidbak
don't have the standard so I'll just leave a comment, but this would severely limit how classes with virtual inheritance can be constructed.kmdreko
You have to understand that in C++ a ctor explicitly calling the default ctor for a Sub direct subobject (T::T() : Sub() { ... }) of class type is exactly the same as not mentioning it at all (T::T() { ... }).curiousguy

4 Answers

1
votes

The rules for initializing bases and members are specified in [class.base.init].

Specifically in p7:

A mem-initializer where the mem-initializer-id denotes a virtual base class is ignored during execution of a constructor of any class that is not the most derived class.

and its complement in p13:

First, and only for the constructor of the most derived class ([intro.object]), virtual base classes are initialized in the order they appear on a depth-first left-to-right traversal of the directed acyclic graph of base classes, where “left-to-right” is the order of appearance of the base classes in the derived class base-specifier-list.

Hence the initializers B(true) and B(false) are ignored when initializing Mf and Mt because they're not the most derived class, and the initialization of D leads with the initialization of B. No initializer for it is provided, so B() is used.


Making this fail to compile would be basically impossible? To start with, consider:

struct Mf : public virtual B { };
struct D : public Mf { };

That initializes B, but implicitly. Do you want that to be an error for Mf since its initialization would be ignored? I assume no - otherwise this language feature would be completely unusuable. Now, what about:

struct Mf : public virtual B { Mf() : B() { } };
struct D : public Mf { };

Is that an error? It basically means the same thing though. What if Mf had members that needed to be initialized and I, as matter of habit, just like listing the base classes?

struct Mf : public virtual B { Mf() : B(), i(42) { } int i; };
struct D : public Mf { };

Okay, you say, you only error if you actually provide arguments. Which is where a different misconception comes in:

We know from previous questions that on construction the most derived class directly calls the default (0-arg) constructor of the (virtual) base.

That's not true (and is not what those answers state). The most derived class initializes the virtual bases - but this initialization does not have to be default. We could've written:

struct D : public Mf, public Mt { D() : B(true) { } };

And really, there's not an interesting distinction between B() and B(true). Imagine the constructor were just B(bool = true), then does it matter whether or not the user provides the argument true? It would be strange if one were an error but not the other, right?

If you keep going down this rabbit hole, I think you'll find that making this an error would be either exceedingly narrow or exceedingly restrictive.

1
votes

[class.mi]/7 - For an object of class AA, all virtual occurrences of base class B in the class lattice of AA correspond to a single B subobject within the object of type AA...


[class.base.init]/7 - ... A mem-initializer where the mem-initializer-id denotes a virtual base class is ignored during execution of a constructor of any class that is not the most derived class.


[intro.object]/6 - If a complete object, a data member, or an array element is of class type, its type is considered the most derived class, to distinguish it from the class type of any base class subobject; an object of a most derived class type or of a non-class type is called a most derived object.

Why is that?

Apart from the obvious; because the standard says so, one possible rationale is that since you only have one base class subobject it doesn't even make sense to allow middle bases to interact with the initialization of the virtual base. Otherwise, which middle base class would you expect to initialize the virtual base, Mt or Mf ?, because for B(false) and B(true) would mean two different way to initialize the same object.

1
votes

Adding a new class to a codebase should not cause well-formed classes to suddenly become invalid. That would be a language disaster. If Derived initializes its virtual base Base, and it is correct code, then the existence of a further-derived class should have no impact on the validity of Derived. Your expectation would almost completely preclude inheritance from any class simply because it happens to use virtual inheritance somewhere, and make virtual inheritance unusable.

But for the citations you requested (from draft n4762):

10.9.2/13:

In a non-delegating constructor, initialization proceeds in the following order: — First, and only for the constructor of the most derived class (6.6.2), virtual base classes are initialized in the order they appear on a depth-first left-to-right traversal of the directed acyclic graph of base classes, where “left-to-right” is the order of appearance of the base classes in the derived class base-specifier-list.

And the second part you asked about, the virtual base initializer in a non-most-derived class is described here, in 10.9.2/7:

A mem-initializer where the mem-initializer-id denotes a virtual base class is ignored during execution of a constructor of any class that is not the most derived class.

0
votes

A virtual base is constructed by the most-derived class, but need not use a default constructor to do so, it can use any accessible constructor. Intermediate bases simply do not factor into the construction of a virtual base.