Почему виртуальные базовые конструкторы, отличные от заданных по умолчанию, не вызываются, если большинство производных баз явно не вызывает их?

Я хотел бы понять, почему стандарт CY C++ требует, чтобы виртуальные базовые конструкторы, отличные от заданных по умолчанию, не могли быть вызваны промежуточным классом, не являющимся наиболее производным, как в этом коде, при компиляции с помощью -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;
}

Итак, когда скомпилировано БЕЗ -D_WITH_BUG_, код печатает:

$ 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

Но когда скомпилировано с -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

Почему B(int a, int b) вызов A(a) здесь должен игнорироваться? Я понимаю, что стандарт C++ предписывает это, но почему? Что такое рациональное?

Если я создаю экземпляр только объекта B: B b(4,5); это ПОЛУЧАЕТ правильное значение b._a 4; но если B является подклассом C: C c(4,5,6), C::a в конечном итоге становится 1, IFF c НЕ ПРЯМО ПРИЗЫВАЕТ A(a) . Таким образом, значение a B(a,b) отличается, если он является объектом подкласса, чем если он является наиболее производным объектом. Это для меня очень запутанно и неправильно. Есть ли надежда получить достаточно людей, чтобы согласиться изменить стандарт C++ по этому вопросу?

3 ответа

Вся цель виртуального наследования - решить проблему алмазов. Когда у вас есть виртуальный базовый класс, и ваша иерархия выглядит следующим образом:

  A
 / \
B   C
 \ /
  D

Вы должны знать, когда построить A, Вы не можете иметь B построить его, а затем C затем сразу перезаписать его - вам нужно, чтобы он был построен ровно один раз. Хорошо, так когда мы сможем это сделать? Самый простой выбор - заставить самый производный класс сделать это! Поэтому, когда мы инициализируем B подобъект D, он не будет инициализировать его A подобъект, потому что B не самый производный тип.

В вашем случае ваша иерархия все еще линейна:

A
|
B
|
C

но самый производный тип, C, должен инициализировать все виртуальные базы - A а также B, B не будет инициализировать его A подобъект по той же причине, что и в сложном примере.

Такое поведение из-за virtual base class, Поскольку A является виртуальным базовым классом, он создается наиболее производным классом.
Вы можете проверить проблему наследования формы ромба и эту дискуссию по аналогичному вопросу, чтобы понять, почему так должно быть.
Сначала поймите, как проблема формы алмаза решается с помощью виртуального базового класса.
class A { ...}
class B: virtual public A {...}
class C: virtual public A {...}
class D: public B, public C {...}
Когда вы сделаете базовый класс виртуальным, будет один объект базового класса. Все объекты промежуточного производного класса будут ссылаться на один и тот же объект базового класса. Т.е. здесь, если объект D создается, тогда B::A и C::A будут ссылаться на один и тот же объект. Этот единственный объект имеет базовый класс как B, так и C. Таким образом, существует два производных класса для создания этого единственного объекта, если он допускает создание объекта базового класса промежуточными классами. Эта неоднозначность решается путем предоставления наиболее производному классу ответственности за создание виртуального базового класса.

Вряд ли вы получите какую-либо поддержку для изменения языка. Виртуальное наследование полезно только в сценариях множественного наследования.

Почему B(int a, int b) вызов A(a) здесь должен игнорироваться?

Потому что уникальный подобъект A уже создан. Конструктор - это не обычная функция, ее нельзя просто нигде вызывать.

Ты можешь написать

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

который даст тело B::B(int, int) параметр, который передан A::A(int)

Другие вопросы по тегам