-
10-07-2019 - |
题
假设我们有一个具体的 class Apple
. 。(可以实例化苹果对象。)现在,有人来并得出摘要 class Peach
来自苹果。它是抽象的,因为它引入了新的纯虚函数。Peach 的用户现在被迫从它派生并定义这个新函数。这是常见的模式吗?这样做正确吗?
样本:
现在假设我们有一个具体的class Apple { public: virtual void MakePie(); // more stuff here };
class Peach : public Apple { public: virtual void MakeDeliciousDesserts() = 0; // more stuff here };
class Berry
. 。有人得出摘要 class Tomato
来自贝瑞。它是抽象的,因为它覆盖了 Berry 的虚拟函数之一,并使其成为纯虚拟函数。Tomato 的用户必须重新实现之前在 Berry 中定义的功能。这是常见的模式吗?这样做正确吗?
样本:
笔记:这些名称是人为的,并不反映任何实际的代码(希望如此)。写这个问题并没有损害任何水果。class Berry { public: virtual void EatYummyPie(); // more stuff here };
class Tomato : public Berry { public: virtual void EatYummyPie() = 0; // more stuff here };
解决方案
Apple 的 Re Peach:
- 如果苹果是价值类(即具有复制CTOR,非相同实例可以相等,等等)。有关原因,请参见Meyers更有效的C ++项目33。
- 如果苹果有公共非虚拟驱动器,请不要这样做,否则,当您的用户通过桃子指针删除Apple时,您会邀请不确定的行为。
- 否则,你可能是安全的,因为你没有违反 里氏可替代性. 。桃子就是苹果。
- 如果您拥有 Apple 代码,则更愿意分解出一个公共抽象基类(可能是 Fruit)并从中派生 Apple 和 Peach。
浆果番茄:
- 与上面相同,加上:
- 避免,因为它不寻常
- 如果必须,请记录 Tomato 的派生类必须执行哪些操作才能不违反里氏可替换性。您在 Berry 中重写的函数 - 我们称之为
Juice()
- 提出某些要求并做出某些承诺。派生类的实现Juice()
不能要求更多,承诺也不少。那么 DerivedTomato IS-A Berry 和只知道 Berry 的代码是安全的。
可能,您将通过记录 DerivedTomatoes 必须调用来满足最后一个要求 Berry::Juice()
. 。如果是这样,请考虑使用模板方法:
class Tomato : public Berry
{
public:
void Juice()
{
PrepareJuice();
Berry::Juice();
}
virtual void PrepareJuice() = 0;
};
现在,番茄很有可能是浆果,这与植物学的预期相反。(例外情况是如果派生类的实现 PrepareJuice
强加额外的先决条件 Berry::Juice
).
其他提示
在我看来,这似乎表明设计糟糕。如果你想从一个封闭的库中获取一个具体的定义并扩展它并从中分出一堆东西,可能会被强制,但那时我会认真考虑关于封装而不是继承的指导原则。如果你可以封装的话,你可能应该。
是的,我想的越多,这是一个非常糟糕的主意。
不一定错误,但肯定是臭的。特别是如果你把水果放在阳光下太久。 (而且我认为我的牙医不会喜欢吃混凝土苹果。)
尽管如此,我在这里看到的主要问题不是抽象类派生自具体类,而是REALLY DEEP继承层次结构。
编辑:重新阅读我看到这是两个层次结构。所有的水果都让我混淆了。
如果您使用推荐的继承模型做法<!>“is-a <!>”;那么这种模式几乎不会出现。
一旦你有了一个具体的类,你就是说你可以实际创建一个实例。如果你从那里派生出一个抽象类,那么作为基类属性的东西对于派生类来说就不是真的,派生类应该设置一些不正确的klaxons。
看看你的例子,桃子不是苹果,所以它不应该来自它。来自Berry的番茄也是如此。
这是我通常建议收容的地方,但这似乎不是一个好模型,因为Apple不包含Peach。
在这种情况下,我会考虑公共接口--PieFilling或DessertItem。
有点不寻常,但如果您有基类的其他子类,并且抽象类的子类有足够的常用东西来证明抽象类的存在,例如:
class Concrete
{
public:
virtual void eat() {}
};
class Sub::public Concrete { // some concrete subclass
virtual void eat() {}
};
class Abstract:public Concrete // abstract subclass
{
public:
virtual void eat()=0;
// and some stuff common to Sub1 and Sub2
};
class Sub1:public Abstract {
void eat() {}
};
class Sub2:public Abstract {
void eat() {}
};
int main() {
Concrete *sub1=new Sub1(),*sub2=new Sub2();
sub1->eat();
sub2->eat();
return 0;
}
嗯...通过思考<!>“是什么...... <!>”;几秒钟后,我得出结论并不常见...... 另外,我不会从Berry派生出Apple和Apple的Peach ......你有更好的例子吗?:)
你可以用C ++做很多奇怪的事情......我甚至不能想到它的1%......
关于使用纯虚拟覆盖虚拟,您可能只是隐藏它,这将非常奇怪......
如果你能找到一个将这个函数作为虚函数链接起来的愚蠢的C ++编译器,那么你将获得运行时纯虚函数调用......
我认为这只能用于黑客攻击而且我不知道究竟是什么样的黑客......
要回答你的第一个问题,你可以这样做,因为Apple的用户,如果给出一个源自Peach的具体实例,将不会有任何不同。并且实例不会知道它不是Apple(除非有一些来自Apple的虚拟函数被覆盖而你没有告诉我们)。
我无法想象用纯虚拟功能覆盖虚拟功能会有多大用处 - 甚至合法吗?
一般来说,你想要与Scott Meyers一致<!>“使所有非叶子类抽象<!>”;他的书中的项目。
无论如何,除了那些你所描述的似乎是合法的 - 只是我不能经常看到你需要它。