Полиморфизм времени выполнения C++, вызывающий неожиданное переопределение при наследовании нескольких классов

Я пытаюсь создать систему с графическим интерфейсом на С++ 11, где, возможно, как и в Годо, каждый «узел» имеет очень конкретную цель, например группировку других узлов, рисование прямоугольника или обнаружение нажатия определенной области экрана. . Некоторые части кода становятся немного повторяющимися, поскольку операторы переключения будут определять тип узла и соответствующим образом приводить его только для того, чтобы в каждом случае вызывать одно и то же имя функции.

Я подумал, что смогу это исправить, немного абстрагируя каждый узел, чтобы класс Rectangle наследовал не только класс BaseNode, но и класс Visibility, чтобы указать, что у него есть функция Draw() для вызова. Сделав Visibility::Draw() виртуальной функцией и переопределив ее в каждом производном классе, я смогу заменить длинный оператор переключения одной строкой, которая преобразует BaseNode* в Visibility* и вызывает Draw() для отображения прямоугольника, круга, изображения, многоугольника. или любую конкретную функциональность, на которую запрограммирован отдельный узел.

Я написал это теоретически хорошее решение, но обнаружил, что оно не вызывает ни Rectangle::Draw(), ни Visibility::Draw(), а вызывает какую-то случайную часть памяти!

В конце концов я определил проблему и написал небольшую демонстрацию, чтобы воспроизвести ее. Приведение pRect к Visibility* и вызов функции Draw() приведет к выполнению Rectangle::BasicFunc(), унаследованного от BaseNode, вместо Rectangle::Draw(), унаследованного от Visibility.

#include <iostream>

class BaseNode {
    public:
        virtual void BasicFunc() {
            std::cout << "BaseNode BasicFunc()" << std::endl;
        }
};

class Visibility {
    public:
        virtual void Draw() {
            std::cout << "Visibility Draw()" << std::endl;
        }
};

class Rectangle : public BaseNode, public Visibility {
    public:
        void BasicFunc() {
            std::cout << "Rectangle BasicFunc()" << std::endl;
        }
        void Draw() {
            std::cout << "Rectangle Draw()" << std::endl;
        }
};

int main() {
    BaseNode* pRect = new Rectangle;
    
    ((Visibility*)pRect)->Draw(); // Calls Rectangle::BasicFunc() instead of Rectangle::Draw()

    return 0;
}

Некоторые другие тесты, которые я провел:

  • Вместо этого приведение pRect к Rectangle* приведет к правильному вызову Rectangle::Draw(), но тогда мы вернемся к громоздкому оператору переключения.
  • Удаление ключевого слова virtual из Visibility::Draw приведет к выполнению вызова Draw()Visibility::Draw(). Правильное имя функции, но неправильный класс.
  • Порядок переопределения функций в классе Rectangle не имеет значения.
  • Инициализация pRect как BaseNode* заставит BasicFunc() всегда работать.
  • Инициализация pRect как Visibility* заставит Draw() всегда работать.
  • Приведение pRect из BaseNode* в Visibility* приведет к тому, что Draw() всегда будет завершаться неудачей и вместо этого вызывать BasicFunc(). (текущий выпуск)
  • Приведение pRect из Visibility* в BaseNode* приведет к тому, что BasicFunc() всегда будет завершаться неудачно и вместо этого вызывать Draw().
  • Инициализация pRect как void* приведет к тому, что BasicFunc() и Draw() вызовут функцию, связанную с первым унаследованным классом Rectangle.

Я совершенно не уверен, как это исправить. Любой совет приветствуется!

Используйте dynamic_cast.

Retired Ninja 19.04.2024 06:32

Надеюсь, урок усвоен: никогда не используйте указатели и ссылки на классы в стиле C.

HolyBlackCat 19.04.2024 07:19
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
2
74
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Запуск с использованием дезинфицирующего средства адресов сразу указывает на проблему:

ошибка времени выполнения: вызов участника по адресу 0x602000000010, который не указывает на объект типа «Видимость».

Проблема здесь в том, что вы не можете напрямую привести объект BaseNode к объекту Visibility, потому что между этими двумя классами нет такой связи. Здесь вы имеете неопределенное поведение.

Поскольку static_cast здесь невозможен, компилятор прибегает к reinterpret_cast, что в данном случае приводит к вызову виртуальной таблицы другого класса, отличного от того, который, по мнению компилятора, он использует.

Одним из возможных обходных путей является использование dynamic_cast:

auto pVisibility = dynamic_cast<Visibility*>(pRect);
if (pVisibility) {
    pVisibility->Draw();
}

Однако использование этого часто можно рассматривать как «запах кода». Вы можете использовать и другие подходы, но это полностью зависит от ваших других дизайнерских соображений.

Кроме того, вам следует добавить виртуальный деструктор в BaseNode, потому что прямо сейчас попытка delete pRect; может вызвать неопределенное поведение:

class BaseNode {
    public:
        virtual ~BaseNode() = default;
        virtual void BasicFunc() {
            std::cout << "BaseNode BasicFunc()" << std::endl;
        }
};

Вот как бы я реализовал что-то вроде этого

  • Сделайте интерфейсы конкретными, моделируя их как чистые абстрактные базовые классы (лучше для модульного тестирования и внедрения зависимостей позже).
  • Моделируйте повторное использование кода, предоставляя реализации по умолчанию для каждого интерфейса. Обычно я использую CRTP/миксин, чтобы позже избежать алмазного наследования (вы можете получить его из конкретных реализаций).
  • Никогда не используйте голое/новое удаление. НЕ рекомендуется больше делать это на C++, см. Основные рекомендации по C++
  • Живая демонстрация здесь: https://onlinegdb.com/LTdAgEQr9

#include <iostream>
#include <memory>

//---------------------------------------------------------------------------------------
// keep interfaces abstract (no implementation yet)

class IBaseNode
{
public:
    virtual ~IBaseNode() = default; // classes with virtual methods must also have virtual destructors.
    virtual void BasicFunc() = 0;
};

// allow mixin of default implementation (CRTP)
// this way you will not fall into the trap
// of creating a design with diamond inheritance later

template<typename base_t>
class IBaseNodeImpl :
    public IBaseNode
{
public:
    virtual void BasicFunc()
    {
        // do not use std::endl; unless you really have to flush
        // in templates you need to use this
        std::cout << "BaseNode BasicFunc(), value = " << this->m_value << "\n";
    }
private:
    int m_value{ 42 };
};

//---------------------------------------------------------------------------------------

class IVisibility
{
public:
    virtual ~IVisibility() = default;
    virtual void Draw() const = 0;
};

template<typename base_t>
class IVisibilityImpl :
    public IVisibility
{
public:
    void Draw() const override
    {
        std::cout << "Visibility Draw()\n";
        if (get_visibility())
        {
            std::cout << "Visible\n";
        }
        else
        {
            std::cout << "Invisible\n";
        }
    }

protected:
    // never expose private members directly
    // for refactorability later 

    void set_visibility(bool is_visible)
    {
        m_is_visible = true;
    }

    bool get_visibility() const noexcept
    {
        return m_is_visible;
    }

private:
    bool m_is_visible{ false };
};

//---------------------------------------------------------------------------------------

class Rectangle final :
    public IBaseNodeImpl<Rectangle>,
    public IVisibilityImpl<Rectangle>
{
public:
    Rectangle()
    {
        std::cout << "Rectangle constructed\n";

        // You can manage visibility from within
        // the rectangle too
        set_visibility(true);
    }

    ~Rectangle()
    {
        std::cout << "Rectangle destructed\n";
    }
};

int main() {
    //BaseNode* pRect = new Rectangle;  <== don't use new in C++ anymore, you should use std::make_unique (to avoid memory leaks)

    std::unique_ptr<IBaseNode> rectangle = std::make_unique<Rectangle>();
    rectangle->BasicFunc();

    auto visibility = dynamic_cast<IVisibility*>(rectangle.get());
    visibility->Draw();

    return 0;
}

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