事件系统:类型转换或联合的继承
Event system: Inheritance with type-casting or unions
我目前正在为我的游戏引擎开发一个事件系统。我考虑过两种实现方式:
1. 继承:
class Event
{
//...
Type type; // Event type stored using an enum class
}
class KeyEvent : public Event
{
int keyCode = 0;
//...
}
2. 有联合:
class Event
{
struct KeyEvent
{
int keyCode = 0;
//...
}
Type type; // Event type stored using an enum class
union {
KeyEvent keyEvent;
MouseButtonEvent mouseButtonEvent;
//...
}
}
在这两种情况下,您都必须检查 Event::Type
枚举,以了解发生了哪种事件。
当您使用继承时,您必须强制转换事件以获取它的值(例如,将 Event
强制转换为 KeyPressedEvent
以获取关键代码)。所以你必须将原始指针转换为需要的类型,这通常很容易出错。
当您使用联合时,您可以简单地检索已发生的事件。但这也意味着内存中联合的大小总是和其中包含的最大 class 一样大。这意味着可能会浪费大量内存。
现在回答问题:
是否有关于使用一种方式优于另一种方式的争论?我应该浪费内存还是冒错误投射的风险?
或者是否有我没有想到的更好的解决方案?
最后我只想通过例如一个 KeyEvent
作为一个 Event
。然后我想使用枚举 class 检查 Event::Type
。如果类型等于说Event::Type::Key
我想获取按键事件的数据
您可以使用多态性,我相信如果派生的 classes 共享大量方法,这是一个解决方案,您可以拥有事件的抽象 class,从中派生并实现这些方法,然后您可以将它们放在同一个容器中,如果正确完成,您将不需要转换。
大致如下:
#include <vector>
#include <iostream>
#include <memory>
class Event {//abstract class
public:
virtual void whatAmI() = 0; //pure virtual defined in derived classes
virtual int getKey() = 0;
virtual ~Event(){}
};
class KeyEvent : public Event {
int keyCode = 0;
public:
void whatAmI() override { std::cout << "I'm a KeyEvent" << std::endl; }
int getKey() override { return keyCode; }
};
class MouseEvent : public Event {
int cursorX = 0, cursorY = 0;
public:
void whatAmI() override { std::cout << "I'm a MouseEvent" << std::endl; }
int getKey() override { throw -1; }
};
int main() {
std::vector<std::unique_ptr<Event>> events;
events.push_back(std::make_unique<KeyEvent>()); //smart pointers
events.push_back(std::make_unique<MouseEvent>());
try{
for(auto& i : events) {
i->whatAmI();
std::cout << i->getKey() << " Key" << std::endl;
}
} catch(int i){
std::cout << "I have no keys";
}
}
输出:
I'm a KeyEvent
0 Key
I'm a MouseEvent
I have no keys
这是一个简化版本,您仍然需要处理copy constructors, assignment operators and such。
不同类型的事件通常包含非常不同的数据并且具有非常不同的用法。我会说这不适合子类型化(继承)。
让我们先弄清楚一件事:在任何一种情况下都不需要 type
字段。在继承的情况下,您可以通过强制转换来确定当前对象的子class。当用在指针上时,dynamic_cast
returns 要么是正确子class 的对象,要么是空指针。有一个很好的例子 on cppreference. In the case of a tagged union, you should really use a class that represents a tagged union, like std::variant
,而不是 type
作为您 class.
的成员在工会旁边
如果你要使用继承,典型的做法是将你的事件放在一个 Event
指针的数组(向量)中,并从 Event
class。但是,由于这些是事件,您可能不会根据事件类型执行相同的操作,因此您最终会将当前 Event
对象向下转换为子 classes 之一(KeyEvent
) 然后才从 subclass 提供的接口中使用它(以某种方式使用 keyCode
)。
但是,如果您要使用带标记的联合或变体,则可以直接将 Event
对象放入数组中,而不必处理指针和向下转换。当然,您可能会浪费一些内存,但您的事件 class 有多大?另外,除了不必取消引用指针之外,您可能会因为 Event
对象在内存中靠近在一起而获得性能。
同样,我想说变体比子类型更适合这种情况,因为不同类型的事件通常以非常不同的方式使用。例如,KeyEvent
在数百个中有一个关联的键 ("key code"),没有位置,而 MouseEvent
在三个中有一个关联的按钮,也许五个,但不会更多,作为职位。除了时间戳或 window 接收事件的非常一般的事情之外,我想不出两者之间的共同行为。
Event
似乎更以数据为中心而不是以行为为中心,所以我会选择 std::variant
:
using Event = std::variant<KeyEvent, MouseButtonEvent/*, ...*/>;
// or using Event = std::variant<std::unique_ptr<KeyEvent>, std::unique_ptr<MouseButtonEvent>/*, ...*/>;
// In you are concerned to the size... at the price of extra indirection+allocation
那么用法可能类似于
void Print(const KeyEvent& ev)
{
std::cout << "KeyEvent: " << ev.key << std::endl;
}
void Print(const MouseButtonEvent& ev)
{
std::cout << "MouseButtonEvent: " << ev.button << std::endl;
}
// ...
void Print_Dispatch(const Event& ev)
{
std::visit([](const auto& ev) { return Print(ev); }, ev);
}
我目前正在为我的游戏引擎开发一个事件系统。我考虑过两种实现方式:
1. 继承:
class Event
{
//...
Type type; // Event type stored using an enum class
}
class KeyEvent : public Event
{
int keyCode = 0;
//...
}
2. 有联合:
class Event
{
struct KeyEvent
{
int keyCode = 0;
//...
}
Type type; // Event type stored using an enum class
union {
KeyEvent keyEvent;
MouseButtonEvent mouseButtonEvent;
//...
}
}
在这两种情况下,您都必须检查 Event::Type
枚举,以了解发生了哪种事件。
当您使用继承时,您必须强制转换事件以获取它的值(例如,将 Event
强制转换为 KeyPressedEvent
以获取关键代码)。所以你必须将原始指针转换为需要的类型,这通常很容易出错。
当您使用联合时,您可以简单地检索已发生的事件。但这也意味着内存中联合的大小总是和其中包含的最大 class 一样大。这意味着可能会浪费大量内存。
现在回答问题:
是否有关于使用一种方式优于另一种方式的争论?我应该浪费内存还是冒错误投射的风险?
或者是否有我没有想到的更好的解决方案?
最后我只想通过例如一个 KeyEvent
作为一个 Event
。然后我想使用枚举 class 检查 Event::Type
。如果类型等于说Event::Type::Key
我想获取按键事件的数据
您可以使用多态性,我相信如果派生的 classes 共享大量方法,这是一个解决方案,您可以拥有事件的抽象 class,从中派生并实现这些方法,然后您可以将它们放在同一个容器中,如果正确完成,您将不需要转换。
大致如下:
#include <vector>
#include <iostream>
#include <memory>
class Event {//abstract class
public:
virtual void whatAmI() = 0; //pure virtual defined in derived classes
virtual int getKey() = 0;
virtual ~Event(){}
};
class KeyEvent : public Event {
int keyCode = 0;
public:
void whatAmI() override { std::cout << "I'm a KeyEvent" << std::endl; }
int getKey() override { return keyCode; }
};
class MouseEvent : public Event {
int cursorX = 0, cursorY = 0;
public:
void whatAmI() override { std::cout << "I'm a MouseEvent" << std::endl; }
int getKey() override { throw -1; }
};
int main() {
std::vector<std::unique_ptr<Event>> events;
events.push_back(std::make_unique<KeyEvent>()); //smart pointers
events.push_back(std::make_unique<MouseEvent>());
try{
for(auto& i : events) {
i->whatAmI();
std::cout << i->getKey() << " Key" << std::endl;
}
} catch(int i){
std::cout << "I have no keys";
}
}
输出:
I'm a KeyEvent
0 Key
I'm a MouseEvent
I have no keys
这是一个简化版本,您仍然需要处理copy constructors, assignment operators and such。
不同类型的事件通常包含非常不同的数据并且具有非常不同的用法。我会说这不适合子类型化(继承)。
让我们先弄清楚一件事:在任何一种情况下都不需要 type
字段。在继承的情况下,您可以通过强制转换来确定当前对象的子class。当用在指针上时,dynamic_cast
returns 要么是正确子class 的对象,要么是空指针。有一个很好的例子 on cppreference. In the case of a tagged union, you should really use a class that represents a tagged union, like std::variant
,而不是 type
作为您 class.
如果你要使用继承,典型的做法是将你的事件放在一个 Event
指针的数组(向量)中,并从 Event
class。但是,由于这些是事件,您可能不会根据事件类型执行相同的操作,因此您最终会将当前 Event
对象向下转换为子 classes 之一(KeyEvent
) 然后才从 subclass 提供的接口中使用它(以某种方式使用 keyCode
)。
但是,如果您要使用带标记的联合或变体,则可以直接将 Event
对象放入数组中,而不必处理指针和向下转换。当然,您可能会浪费一些内存,但您的事件 class 有多大?另外,除了不必取消引用指针之外,您可能会因为 Event
对象在内存中靠近在一起而获得性能。
同样,我想说变体比子类型更适合这种情况,因为不同类型的事件通常以非常不同的方式使用。例如,KeyEvent
在数百个中有一个关联的键 ("key code"),没有位置,而 MouseEvent
在三个中有一个关联的按钮,也许五个,但不会更多,作为职位。除了时间戳或 window 接收事件的非常一般的事情之外,我想不出两者之间的共同行为。
Event
似乎更以数据为中心而不是以行为为中心,所以我会选择 std::variant
:
using Event = std::variant<KeyEvent, MouseButtonEvent/*, ...*/>;
// or using Event = std::variant<std::unique_ptr<KeyEvent>, std::unique_ptr<MouseButtonEvent>/*, ...*/>;
// In you are concerned to the size... at the price of extra indirection+allocation
那么用法可能类似于
void Print(const KeyEvent& ev)
{
std::cout << "KeyEvent: " << ev.key << std::endl;
}
void Print(const MouseButtonEvent& ev)
{
std::cout << "MouseButtonEvent: " << ev.button << std::endl;
}
// ...
void Print_Dispatch(const Event& ev)
{
std::visit([](const auto& ev) { return Print(ev); }, ev);
}