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

Я хотел бы понять, ПОЧЕМУ стандарт 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). Таким образом, значение B (a, b) отличается, если это объект подкласса, чем если он является наиболее производным объектом. Для меня это очень сбивает с толку и неправильно. Есть ли какая-нибудь надежда на то, что достаточно людей согласится изменить стандарт C ++ по этому поводу?


person JVD    schedule 28.06.2017    source источник
comment
Можете ли вы немного лучше отформатировать свой код? то, как это сейчас написано, просто беспорядок.   -  person AresCaelum    schedule 28.06.2017
comment
Виртуальные базовые конструкторы, не являющиеся конструкторами по умолчанию, не могут быть виртуальными.   -  person Rakete1111    schedule 28.06.2017
comment
@ Rakete1111 Я считаю, что это следует читать как виртуальные базовые конструкторы, отличные от конструкторов по умолчанию :)   -  person iehrlich    schedule 28.06.2017
comment
Приносим извинения за проблемы с форматированием - мне не нравится использовать веб-инструменты для форматирования кода, потому что я не вижу значки инструментов форматирования и не показываю светлый текст на темном фоне (любой другой параметр вызывает у меня боль в глазах - я слегка слабовидящий). Так что emacs '‹^ U› -4- ‹ESC› -m-indent-region' должен будет сделать. И да, это «конструктор виртуального базового класса не по умолчанию», а не какой-либо «виртуальный конструктор».   -  person JVD    schedule 28.06.2017
comment
Это не проблема, но имена, начинающиеся с символа подчеркивания, за которым следует заглавная буква (_WITH_BUG_), и имена, содержащие два последовательных символа подчеркивания, зарезервированы для использования реализацией. Не используйте их в своем коде.   -  person Pete Becker    schedule 28.06.2017
comment
Проблема здесь не в конструкторах, не являющихся конструкторами по умолчанию; Правило состоит в том, что виртуальные базы инициализируются конструктором для наиболее производного типа. Это относится как к конструкторам по умолчанию, так и к конструкторам, отличным от конструкторов по умолчанию.   -  person Pete Becker    schedule 28.06.2017
comment
Проблема с OP @PeteBecker усугубляется тем, что A::A() неявно доступен для компилятора, чтобы вставить его в начало C::C(int, int, int). Было бы лучше, если бы упущение было ошибкой времени компиляции   -  person Caleth    schedule 28.06.2017
comment
@Caleth - 'A :: A () `всегда неявно доступен компилятору для вставки, с множественным наследованием и виртуальными базами или без них. Вы предлагаете, чтобы конструктор по умолчанию никогда не рассматривался как конструктор по умолчанию, если в классе определены другие конструкторы. Это довольно резко.   -  person Pete Becker    schedule 28.06.2017


Ответы (3)


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

  A
 / \
B   C
 \ /
  D

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

В вашем случае ваша иерархия по-прежнему линейна:

A
|
B
|
C

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

person Barry    schedule 28.06.2017

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

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

person Caleth    schedule 28.06.2017
comment
для меня это глупо, что программист должен помнить, что их явный вызов B (a, b) игнорируется. - person JVD; 28.06.2017
comment
Другой вариант - не наследовать виртуально - person Caleth; 28.06.2017

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

person LearningC    schedule 28.06.2017
comment
исправление, которое на самом деле не объясняет причины, только то, что стандарт C ++ требует, чтобы это было так. IMHO, как только программист объявил, что B 'является A', виртуально или нет, тогда B должен нести ответственность за построение своего 'A'. - person JVD; 28.06.2017
comment
Я до сих пор не вижу причин, по которым B не всегда может построить свой A, независимо от того, является ли он виртуальной базой некоторого класса C. Но я должен признать, что это так ... - person JVD; 28.06.2017
comment
его 'A' - ›совместно со всеми другими классами, фактически наследующими A в производном классе - person Caleth; 28.06.2017
comment
@JVD Здесь B не может построить 'A', поскольку A является виртуальной базой, и самый производный класс C должен ее построить. Так решается алмазная проблема. вы можете проверить ссылку на сообщение stackoverflow.com/questions/2659116/. Если вы видите, что промежуточные классы создают виртуальную базу, тогда всем промежуточным классам необходимо будет создать единый объект виртуального базового класса. - person LearningC; 29.06.2017