0

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;
}
curiousguy
  • 8,038
  • 2
  • 40
  • 58
davidbak
  • 5,775
  • 3
  • 34
  • 50
  • 1
    If you wanted chapter & verse cited from the standard, you should add the `language-lawyer` tag. – Eljay Jan 03 '19 at 23:13
  • @Eljay - thanks, done. – davidbak Jan 03 '19 at 23:16
  • 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 Jan 04 '19 at 00:04
  • 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 Jan 04 '19 at 21:25

4 Answers4

1

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.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • This is a good answer - but to be a _great_ answer (IMO) it would address the case I'm considering but you didn't mention: that the middle class's initializer list did _explicitly_ mention the 1-arg constructor and that constructor wasn't called. (Above you show the middle class listing the 0-arg constructor - which _is_ called.) My question is about this: the programmer explicitly wrote `: B(true)` or `:B(false)` and that is ignored. Even though they're right there for the reader to see. I'm confused that actual written code is silently ignored. Do you see how your examples miss that? – davidbak Jan 04 '19 at 00:43
  • @davidbak As I said, there's not a meaningful distinction between having written `B(true)` and `B()`. What do you expect to happen? What if `B` weren't default constructible - what you're suggesting would make the whole existence of `Mf` ill-formed – Barry Jan 04 '19 at 01:00
  • 1
    @davidbak And yes, virtual bases are confusing. – Barry Jan 04 '19 at 01:01
1

[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.

Jans
  • 11,064
  • 3
  • 37
  • 45
1

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.

Chris Uzdavinis
  • 6,022
  • 9
  • 16
  • But that isn't what's happening: "Adding a new class to a codebase should not cause well-formed classes to suddenly become invalid.". I'm suggesting the new class is the invalid one. (In my example `Mf` and `Mt` are perfectly fine - and the error wouldn't be reported there. The "new class" is `B`.) – davidbak Jan 04 '19 at 00:44
  • 1
    Given at the time the new class is compiled in its own .cpp file, the compiler may not even know how its base constructor is implmeneted (if its definition appears in a separate .cpp file, for example.) – Chris Uzdavinis Jan 04 '19 at 00:47
  • 1
    Also, if the new, most derived class was suddenly an error, that would mean you could never inherit from any class that has a virtual base, because it already has such an initialization from that base's constructor. If the virtual base is not explicitly initialized, then the derived class author chose it to be default constructed. If you explicitly initialize it, that's only to pass parameters. But why should one be ok to derived from and not the other? What if it has multiple constructors and you merely wan to select a different one? Why should the default ctor be treated special? – Chris Uzdavinis Jan 04 '19 at 00:49
  • @ChrisUzdavinis Yes but "then the derived class author chose it to be default constructed" does not have to be the case. In a variant of C++ ("C+=1.1") you could handle ctor-init if a virtual base like overriding a virtual function, with either no overriding, overriding, or final. – curiousguy Jan 04 '19 at 21:36
  • @curiousguy I still see little value in making such leeway. Given that a single class could have the same virtual base through different parents that each initialize the base differently, what should be done when they want to be "final" yet have conflicting initializers (or even use different constructor of the base)? This seems to me a road paved in misery, and what we have is about as good as it gets, even if it's not entirely satisfying. – Chris Uzdavinis Jan 04 '19 at 21:52
  • @ChrisUzdavinis Do you really believe that a language that doesn't permit inheritance from any set of classes (such that each one can be derived) is broken? If so C++ is broken, as useful inheritance from an arbitrary set of classes isn't always possible. There are such things as programming tools that cannot be used together. Specifically, with virtual inheritance, if two classes expect a common base to be initialized according to some convention, and these are incompatible, you can't inherit from both. – curiousguy Jan 05 '19 at 00:41
  • (...) A class that insists on initializing a virtual base in the ctor-init-list with a final-ctor-init probably has a very strong relationship with its base subobject; that is incompatible with other classes with such strong relationship. Usually virtual bases are interface classes and there is no such relationship. Unlike simplistic examples (`class B {}; class L : virtual public B {}; class R : virtual public B {}; class D : public L, public R {}; `), real life uses of virtual inheritance usually aren't symmetric. – curiousguy Jan 05 '19 at 00:58
  • @ChrisUzdavinis You oppose final-ctor-init in ctor-init-lists (C+=1.1), do you also oppose final overriders in C++? They also restrict the possible derived classes. Do you oppose language that support contract programming? Contracts on a virtual declaration (contract on the declaration not just the particular implementation) restrict possible overriders. – curiousguy Jan 05 '19 at 01:09
0

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.

SoronelHaetir
  • 14,104
  • 1
  • 12
  • 23
  • Yes I know that but I find it odd that explicitly written code is silently ignored. My question is why is it "do not factor into" instead of "compile time error"? – davidbak Jan 03 '19 at 23:15
  • It's not an error because it is possible to instantiate an object of the intermediate base. Whether that makes sense depends on the code base. Doing otherwise would preclude being able to instantiate a virtual base with a non-default constructor. What you are guaranteed when it comes to virtual bases is that (just like for a non-virtual base) the base is constructed by the time member initialization starts for the current class, but you have no guarantee that the virtual base constructor was invoked by the current class' constructor. – SoronelHaetir Jan 04 '19 at 05:26