我正在尝试创建一些只包含数据成员(没有函数)的类但我希望它们是多态的 - 我的意思是我将通过指向基类的指针传递对象,我需要能够dynamic_cast
将它们转换为特定的派生类型(如果实例不是给定类型,则结果值为NULL
。)
举个例子,我有一个项目:
struct Item {
int x, y;
}
我还有一个移动的项目,另一个包含文本的项目:
struct MovingItem: virtual public Item {
int speedX, speedY;
}
struct TextItem: virtual public Item {
std::string text;
}
据推测,我必须使用上面的虚拟继承,因为我还想要一个移动并有文本的项目,但我只想要顶级Item
中的一组坐标:
struct MovingTextItem: virtual public MovingItem, virtual public TextItem {
}
这些都可以正确定义,但是当我尝试dynamic_cast
Item *
来查看它是什么类型时,我的编译器会抱怨源类型不是多态的。
void example(Item *i) {
MovingTextItem *mti = dynamic_cast<MovingTextItem *>(i); // error!
}
如果我使用虚函数而不是数据成员重新实现整个事情,这将有效,但这似乎是浪费,因为我永远不需要覆盖任何东西。
我能想到的唯一解决方法是将type
成员添加到基础Item
类,并检查该成员而不是使用dynamic_cast
,如果它的类型正确则请改用static_cast
。 (缺点是我必须知道某处的所有对象类型,因此分配的type
值不会发生冲突。)
这是最好的解决方案,还是有另一种方式?
为了论证,想象一下我将每个对象类型写入文件。 MovingItem
转到一个文件,TextItem
转到另一个文件,MovingTextItem
转到这两个文件。因此,有一个实现每个接口的基类将无法工作,除非我能以某种方式告诉使用哪些接口,以便将它们写入正确的文件。
答案 0 :(得分:6)
如果添加虚函数,则类将获得vtable,以便dynamic_cast
起作用。一个无操作的虚拟析构函数就可以了。 (正如托尔斯滕所指出的那样,在你的情况下甚至可能需要非POD成员。)
但是,根据我自己的经验(我多年来一直反对这一点!)我会强烈建议在这种特殊情况下继承。请改用聚合。 Prefer composition over inheritance?您可能需要创建几个转发器,但这样获得的灵活性得到了回报(就以后能够在不影响整个系统的情况下更改实现而言)。
考虑 速度和的项目,此外,文本和是一个位置,而不是 < / em>一个以某种方式具有速度和文本的项目,并通过一些魔法继承该位置。如果您以后想要一个速度快但没有已知位置的物品会怎样?
要在聚合方案中实现结果,请定义构成有关特定功能的合同的“接口”。也不需要虚拟继承:“接口”只是具有纯虚函数的类。另请参阅:What does it mean to "program to an interface"?(如果您需要可维护的软件,则另一条重要规则。)
您可以定义四个“接口”(仅具有纯虚函数的类):Position
,Speed
,Text
和Item
。 Item
继承自前三个,并且不定义任何函数本身。您为前三个接口提供“参考实现”。 “参考impl。” for Item
有三个数据成员(前三个接口的ref.inpl。)并转发到这些实现。现在您可以使用例如Position
只要你需要有位置的东西,就不用知道究竟是什么。 dynamic_cast
也有效。
您还可以定义没有虚拟方法的接口ItemBase
,并从此接口继承Position
,Speed
和Text
。这将允许您通过相同的基本类型访问所有功能,并动态测试子接口的可用性。我认为仍然不需要virtual
继承......
答案 1 :(得分:2)
您甚至可以在基类中添加虚拟析构函数。然后可以使用dynamic_cast。您还可以查看访问者模式。如果您有一个很小且很少更改的数据类型集并且对这些数据类型有更丰富的操作集,那么该模式可能非常有用。
答案 2 :(得分:1)
假设您可以这样做。你有MovingItem
的速度。你有TextItem
字符串。而且你甚至可能拥有MovingTextItem
字符串和速度,它使用来自MovingItem
和TextItem
的多个虚拟继承。
你把所有这些都放在std::vector<Item*>
中。好的,很好。
你是如何使用它的?
每个需要Item*
的代码是否需要使用dynamic_cast
来实现它所需的实际类型(如果它不是正确的类型则退出)?或者这些函数的调用者是否执行dynamic_cast
?无论哪种方式,dynamic_cast
的很多只是为了使用这些值。
但那是次要的。真正的问题是:你如何删除他们?
请参阅,除非Item
有virtual
析构函数,否则在任何delete
上调用Item*
都会非常糟糕。没有虚拟析构函数,C ++无法调用任何派生的类的析构函数。所以你会以某种方式获得任何Item*
项的实际类型。
这将需要大量dynamic_cast
次操作。每次添加新的派生Item
类时,您都需要在列表中添加另一个dynamic_cast
项检查。
或者你可以给Item
一个虚拟析构函数,让C ++完成它的工作。这样,您不需要一系列dynamic_cast
来查找要删除它的实际对象类型。更好的是,因为它有一个虚拟析构函数,Item
将是一个虚拟类型,C ++将允许你执行你似乎想要做的所有dynamic_cast
。
当然,您使用dynamic_cast
这一事实是一个直接的危险信号,您的设计迫切需要修改。
答案 3 :(得分:0)
为了记录,我最终重新设计了这个,以便有一个Item
类型,带有标志以指示哪些字段有效:
enum ItemType {HasSpeed = 1, HasText = 2};
struct Item {
int type;
int x, y;
int speedX, speedY;
std::string text;
}
如果速度和文本字段都有效,我只需设置type = HasSpeed | HasText
。也许有点老派,但是很多更简单!