(Почему) Требуется ли вызов конструктора виртуального базового класса в чисто виртуальном производном классе?

У меня есть иерархия классов со структурой алмаз и базовым классом без конструктора по умолчанию, так как он имеет ссылочные элементы.

Код выглядит следующим образом:

class Base
{
public:
    Base( CustomType& obj ) : refObj_( obj ) {}
    virtual ~Base() {}
private:
    CustomType& refObj_;
};

class Left : public virtual Base
{
public:
    Left() {}; // ERROR: Compiler requests calling Base's constructor
    virtual void leftsMethod() = 0;
};

class Right : public virtual Base
{
public:
    Right( CustomType& obj ) : Base( obj ) {}; // Compiles, but Base( obj ) never gets called here
    virtual void rightsMethod() = 0;
};

class Bottom : public Left, public Right
{
public:
    Bottom( CustomType& obj ) : Base( obj ), Left(), Right( obj ) {}
};

Редактировать: Добавлен виртуальный деструктор базы (он есть в исходном коде)

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

Мой вопрос: почему я все же должен вызывать конструктор Основание в списке инициализации Осталось? Я хотел бы избежать этого и передать ссылку непосредственно из конструктора Нижний только в Основание. (Я использую компилятор "половина С++ 11" MSVC 2010.)

Есть ли другое решение для этой комбинации виртуального наследования и ссылок в базовом классе?

Последовательность. Люди не ожидали бы, что добавление или удаление =0 в объявлении несвязанной функции-члена (т. е. переключение, является ли эта функция-член чисто виртуальной или нет) изменит то, как компилятор обрабатывает конструктор Left (например, компилирует определение конструктора, если функция является чистой). virtual, но не компилируется, если функция виртуальная, но не чистая). Хотя, насколько мне известно, это не является формальным требованием процесса стандартизации, немногим разработчикам, включая членов комитета по стандартизации, нравится идея изменения кода, вызывающего поломку чего-то, не связанного с ним.

Peter 22.03.2019 11:37

@Питер Спасибо! Это ответ, который я искал.

staerkHelmis 22.03.2019 11:51

В связи с этим лучше иметь виртуальную базу только с конструктором по умолчанию. Добавьте интерфейс (чистый виртуальный метод) вместо членов (не по умолчанию).

Jarod42 22.03.2019 12:08

@Peter: Как человек, я нашел более запутанным отсутствие вызова инициализации. Этот чистый абстрактный класс не должен «вызывать» виртуальный базовый класс, это было бы хорошо, IMO.

Jarod42 22.03.2019 12:14

@ Jarod42 Я согласен. Базовый класс - это устаревший код :(

staerkHelmis 22.03.2019 12:14

@ Jarod42 - проблема с требованием, чтобы виртуальная база имела только конструктор по умолчанию, заключается в том, что базовый класс не контролирует, используется ли он в качестве виртуальной базы. Это производный класс, который указывает, является ли база виртуальной. Если код для базового класса внезапно не скомпилируется, потому что какой-то другой код использует его как виртуальную базу, это может сбить людей с толку. В любом случае, я давал свою интерпретацию того, почему поведение такое, какое оно есть, в ответ на вопрос. Если вы хотите возразить, что какое-то другое поведение лучше, свяжитесь с членом комитета по стандартизации и изложите свою позицию.

Peter 22.03.2019 12:24

На самом деле, этот код прекрасно компилируется, начиная с лязг 3.4.1, gcc 7.1 и всех доступных (на godbolt.org) версий msvc. Проблема, описанная OP, возникает только в более ранних версиях. Кто-нибудь знает, что изменилось? @staerkHelmis, может быть, просто переключиться на более новую версию вашего компилятора?

sebrockm 22.03.2019 12:40
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
7
1 160
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

У вас нет конструктора по умолчанию для Base, но Left все равно нужно инициализировать его при создании объекта Left (того, который не является Bottom).

Проблема в том, что у вас есть ссылка в Base, что означает, что у вас нет простого конструктора.

Я бы сказал, что у вас большая проблема с дизайном, так как obj инициализируется Right, поэтому последний должен содержать obj, а не Base.

Также где находится виртуальный деструктор для Base?

class Base
{
public:
    Base() = default;
    ~Base() {}
};

class Left : public virtual Base
{
public:
    virtual void leftsMethod();
};

class Right : public virtual Base
{
    int& refObj_;
public:
    Right( int& obj ) : refObj_( obj ) {}
    virtual void rightsMethod();
};

class Bottom : public Left, public Right
{
public:
    Bottom( int& obj ) : Right( obj ) {}
};

Это не моя точка зрения, извините, я должен был это уточнить. Конструктор по умолчанию не подходит, потому что он может установить ссылку только с другим фиктивным членом. Даже этот конструктор по умолчанию никогда не будет вызываться, поэтому мне интересно, почему он должен быть реализован.

staerkHelmis 22.03.2019 11:35

Но кто-то может наследовать от Left, и вам нужно будет инициализировать ссылку. C++ работает не так, как вы думаете.

Matthieu Brucher 22.03.2019 11:35

Кто-то, кто наследует, скажем, LeftDrc, от Left, должен будет вызвать конструктор Base (явно или неявно). LeftDrc будет вынужден вызывать нестандартный конструктор Base, точно так же, как это необходимо сделать, если он напрямую наследуется от Base.

staerkHelmis 22.03.2019 11:44
"obj инициализируется Right". Нет, он инициализируется Bottom, это точка ОП. Проблема OP в том, что он должен писать абстрактные производные классы как Right, а не просто Left.
Jarod42 22.03.2019 12:22

Хорошо, это проясняет вопрос. Все еще кажется мне плохим дизайном в первую очередь.

Matthieu Brucher 22.03.2019 12:39

Цитата из официальных документов может описать ситуацию:

Practically speaking, this means that when you create a concrete class that has a virtual base class, you must be prepared to pass whatever parameters are required to call the virtual base class’s constructor. And, of course, if there are several virtual base classes anywhere in your classes ancestry, you must be prepared to call all their constructors. This might mean that the most-derived class’s constructor needs more parameters than you might otherwise think.

См. этот Вопросы-Ответы.

Причина сбоя в том, что при инициализации Base есть ссылка на инициализацию в Left. Так что нет конструктора по умолчанию. В противном случае все было бы хорошо.

Matthieu Brucher 22.03.2019 11:32
Ответ принят как подходящий

Помимо проблем дизайна (которые явно существуют здесь), я полностью согласен с вашими рассуждениями о том, что, поскольку Left является абстрактным классом, никакой Left конструктор никогда не должен вызывать какой-либо Base конструктор, и поэтому странно, что это требуется.

На самом деле, я протестировал ваш код с несколькими версиями компилятора, и он компилируется прекрасно работает с gcc 7.1 и новее, с лязг 3.4.1 и новее, а также со всеми доступными версиями msvc.

Итак, я предполагаю, что это была просто ошибка в более ранних версиях компилятора. Кто-нибудь может это подтвердить?

Также обратите внимание, что если вы измените virtual void leftsMethod() = 0; на virtual void leftsMethod() {}, чтобы Left больше не было абстрактным, ошибка возвращается даже с последние версии. И это имеет смысл, так как теперь вы мог создаете экземпляр Left, и, таким образом, конструктор Left может вызвать один из конструкторов Base.

Возможный обходной путь

Если вы не можете переключиться на более новый компилятор и если вы не можете изменить реализацию Base, это может быть для вас рабочим решением:

Определите где-нибудь фиктивный экземпляр CustomType. Затем вы можете предоставить «обязательные» конструкторы по умолчанию, например:

class CustomType{};

class Base
{
public:
    Base( CustomType& obj ) : refObj_( obj ) {}
private:
    CustomType& refObj_;
};

static CustomType dummy;

class Left : public virtual Base
{
protected:
    Left() : Base(dummy) {}; // note here
public:
    virtual void leftsMethod() = 0;
};

class Right : public virtual Base
{
protected:
    Right() : Base(dummy) {}; // and here
public:
    virtual void rightsMethod() = 0;
};

class Bottom : public Left, public Right
{
public:
    Bottom( CustomType& obj ) : Base( obj ), Left(), Right() {}
    virtual void leftsMethod() override {}
    virtual void rightsMethod() override {}
};

void test()
{
    CustomType c;
    Bottom b(c);
}

Это просто для того, чтобы осчастливить компилятор, ваш аргумент о том, что эти конструкторы никогда не будут вызывать Base(dummy), остается в силе.

Однако обратите внимание, что у Base действительно должен быть виртуальный деструктор! Я не знаю, то ли вы просто не включили его сюда для краткости, то ли его действительно нет. Если это не так, очень плохая идея строить иерархию классов поверх него.

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

staerkHelmis 22.03.2019 13:40

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