4

I would like to understand WHY C++ standard mandates that virtual base non-default constructors cannot be invoked by an intermediate NOT most-derived class, as in this code, when compiled with '-D_WITH_BUG_' :

/*  A virtual base's non-default constructor is NOT called UNLESS 
 *  the MOST DERIVED class explicitly invokes it
 */

#include <type_traits>
#include <string>
#include <iostream>

class A
{
public:
    int _a;
    A():  _a(1)
    {
        std::cerr << "A() - me: " << ((void*)this) << std::endl;
    }
    A(int a): _a(a)
    {
        std::cerr << "A(a) - me:" << ((void*)this) << std::endl;
    }
    virtual ~A()
    {
        std::cerr << "~A" << ((void*)this) << std::endl;
    }
};

class B: public virtual A
{
public:
    int _b;
    B(): A(), _b(2)
    {
        std::cerr << "B() - me: " << ((void*)this) << std::endl;
    }
    B(int b) : A(), _b(b)
    {
        std::cerr << "B(b) - me: " << ((void*)this) << std::endl;
    }
    B(int a, int b): A(a), _b(b)
    {
        std::cerr << "B(a,b) - me: " << ((void*)this) << std::endl;
    }
    virtual ~B()
    {
        std::cerr << "~B" << ((void*)this) << std::endl;
    }
};

class C: public virtual B
{
public:
    int _c;
    C(): B(), _c(3)
    {
        std::cerr  << "C()" << std::endl;
    }
    C(int a, int b, int c)
    :
#ifdef _WITH_BUG_    
    B(a,b)
#else
    A(a), B(b)
#endif    
    , _c(c)
    {
        std::cerr  << "C(a,b) - me: " << ((void*)this) << std::endl;    
    }
    virtual ~C()
    {
        std::cerr << "~C" << ((void*)this) << std::endl;
    }  
};
extern "C"
int main(int argc, const char *const* argv, const char *const* envp)
{
    C c(4,5,6);
    std::cerr << " a: " << c._a  << " b: " << c._b << " c: " << c._c 
              <<  std::endl;
    return 0;
}

So, when compiled WITHOUT -D_WITH_BUG_, the code prints:

$ g++ -I. -std=gnu++17 -mtune=native -g3 -fPIC -pipe -Wall -Wextra \
  -Wno-unused -fno-pretty-templates -Wno-register  \
  tCXX_VB.C -o tCXX_VB 
$ ./tCXX_VB
A(a) - me:0x7ffc410b8c10
B(b) - me: 0x7ffc410b8c00
C(a,b) - me: 0x7ffc410b8bf0
a: 4 b: 5 c: 6
~C0x7ffc410b8bf0
~B0x7ffc410b8c00
~A0x7ffc410b8c10

But when compiled with -D_WITH_BUG_ :

$ g++ -I. -std=gnu++17 -mtune=native -g3 -fPIC -pipe -Wall -Wextra \ 
  -Wno-unused -fno-pretty-templates -Wno-register \
  -D_WITH_BUG_ tCXX_VB.C -o tCXX_VB
$ ./tCXX_VB
A() - me: 0x7ffd7153cb60
B(a,b) - me: 0x7ffd7153cb50
C(a,b) - me: 0x7ffd7153cb40
a: 1 b: 5 c: 6
~C0x7ffd7153cb40
~B0x7ffd7153cb50
~A0x7ffd7153cb60

Why must B(int a, int b)'s invocation of A(a) be ignored here ? I understand the C++ standard mandates it, but why ? What is the rational ?

If I instantiate just a B object : B b(4,5) ; this DOES get a correct b._a value of 4; but if B is a subclass of C: C c(4,5,6) C::a ends up being 1, IFF c does not DIRECTLY INVOKE A(a) . So the value of a B(a,b) is different if it is a subclass object than if it is a most-derived object . This to me is very confusing and wrong. Is there any hope of getting enough people to agree to change the C++ standard on this ?

curiousguy
  • 8,038
  • 2
  • 40
  • 58
JVD
  • 645
  • 1
  • 7
  • 17
  • Can you format your code a bit better? the way it is currently written is just a mess. – AresCaelum Jun 28 '17 at 13:39
  • 3
    "virtual base non-default constructors " constructors cannot be virtual though. – Rakete1111 Jun 28 '17 at 13:42
  • 4
    @Rakete1111 I believe it should be read as **virtual base** non-default constructors :) – iehrlich Jun 28 '17 at 13:45
  • Sorry for formatting issues - I don't like using web tools to format code, because I cannot see the format tool icons and display light text on a dark background (any other setting hurts my eyes - I am slightly visually impaired) . So emacs '<^U>-4--m-indent-region' will have to do. And yes, it is 'the non-default constructor of a virtual base class' , not any 'virtual constructor' . – JVD Jun 28 '17 at 13:51
  • 2
    This isn't the problem, but names that begin with an underscore followed by a capital letter (`_WITH_BUG_`) and names that contain two consecutive underscores are reserved for use by the implementation. Don't use them in your code. – Pete Becker Jun 28 '17 at 13:54
  • 2
    The issue here isn't non-default constructors; the rule is that virtual bases are initialized by the constructor for the most-derived type. That applies to default constructors as well as non-default constructors. – Pete Becker Jun 28 '17 at 13:56
  • @PeteBecker OPs problem is exacerbated by `A::A()` being implicitly available for the compiler to insert it at the start of `C::C(int, int, int)`. It would be nicer if the omission was a compile time error – Caleth Jun 28 '17 at 14:19
  • @Caleth -- 'A::A()` is **always** "implicitly available for the compiler to insert", with or without multiple inheritance and virtual bases. You're suggesting that the default constructor **never** be considered as a default when there are other constructors defined in a class. That's pretty drastic. – Pete Becker Jun 28 '17 at 15:12

3 Answers3

3

The entire purpose of having virtual inheritance is to solve the diamond problem. Once you have a virtual base class, and your hierarchy looks like this:

  A
 / \
B   C
 \ /
  D

You need to know when to construct the A. You can't have B construct it and then C then immediately overwrite it - you need it to be constructed exactly once. Okay, so when can we do that? The simplest choice is just: make the most derived class do it! So when we're initializing the B subobject of D, it will not initialize its A subobject because B is not the most derived type.

In your case, your hierarchy is still linear:

A
|
B
|
C

but the most derived type, C, has to initialize all the virtual bases - A and B. B won't initialize its A subobject for the same reason as it didn't in the complicated example.

Barry
  • 286,269
  • 29
  • 621
  • 977
1

This behavior is because of virtual base class. Since A is the virtual base class it is constructed by the most derived class.
you can check about diamond shape inheritance problem and this discussion on similar question to understand why it has to be in this way.
First understand how diamod shape problem is solved by virtual base class.
class A { ...}
class B: virtual public A {...}
class C: virtual public A {...}
class D: public B, public C {...}
When you make the base class as virtual there will be one base class object. The intermediate derived class objects will all refer to the same single base class object. I.e. here if an object of D is created then B::A and C::A both will refer the same object. This single object is of base class of both B and C. So there are two derived classes to construct this single object if it allowed the construction of base class object by intermediate classes. This ambiguity is solved by giving the most derived class the responsibility to construct the virtual base class.

LearningC
  • 3,182
  • 1
  • 12
  • 19
  • correction, that does not really explain the rationale, only that the C++ standard mandates it to be the case. IMHO, once a programmer has declared that B 'is an A', virtually or not, then B should be responsible for constructing its 'A' . – JVD Jun 28 '17 at 14:14
  • I still see no reason why B cannot always construct its A, regardless of whether or not it is a virtual base of some class C . But I must accept this is the way it is ... – JVD Jun 28 '17 at 14:16
  • "its 'A'" -> shared with all the other classes virtually inheriting A in a deriving class – Caleth Jun 28 '17 at 14:16
  • @JVD Here B cannot construct 'A' since A is a virtual base the most derived class C has to construct it. This is how the diamond problem is solved. you can check the post link https://stackoverflow.com/questions/2659116/how-does-virtual-inheritance-solve-the-diamond-multiple-inheritance-ambiguit . If you see there if the intermediate classes constructs the virtual base then all intermediate classes will need to construct the single virtual base class object. – LearningC Jun 29 '17 at 08:40
1

It's unlikely that you'll get any support to change the language. Virtual inheritance is only useful in multiple inheritance scenarios.

Why must B(int a, int b)'s invocation of A(a) be ignored here?

Because the unique A sub-object has already been constructed. A constructor isn't an ordinary function, you can't just call it anywhere.

You can write

C(int a, int b, int c)
    : A(a), B(a, b), _c(c)
    { ... }

which will give the body of B::B(int, int) the parameter that passed to A::A(int)

Caleth
  • 52,200
  • 2
  • 44
  • 75