Я пытаюсь создать систему с графическим интерфейсом на С++ 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
.Я совершенно не уверен, как это исправить. Любой совет приветствуется!
Надеюсь, урок усвоен: никогда не используйте указатели и ссылки на классы в стиле C.
Запуск с использованием дезинфицирующего средства адресов сразу указывает на проблему:
ошибка времени выполнения: вызов участника по адресу 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;
}
};
Вот как бы я реализовал что-то вроде этого
#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;
}
Используйте
dynamic_cast
.