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

Я хотел бы понять, почему стандартные C ++ 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 ++ на это?

4

Решение

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

  A
/ \
B   C
\ /
D

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

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

A
|
B
|
C

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

3

Другие решения

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

Почему 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)

1

Такое поведение из-за 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. Таким образом, существует два производных класса для создания этого единственного объекта, если он допускает создание объекта базового класса промежуточными классами. Эта неоднозначность решается путем предоставления наиболее производному классу ответственности за создание виртуального базового класса.

1