我想扩展一个类以包含额外的数据和功能(我想要多态行为)。使用继承和多重继承似乎很明显。
阅读了各种帖子后继承(尤其是多重继承)可能会有问题,我已经开始研究其他选择了:
以下继承示例是否有建议的方法?这是继承合理的情况吗? (但我不想将默认函数放在基类中)
#include <iostream>
//================================
class B {
public:
virtual ~B() { }
void setVal(int val) { val_ = val; }
// I'd rather not have these at base class level but want to use
// polymorphism on type B:
virtual void setColor(int val) { std::cout << "setColor not implemented" << std::endl; }
virtual void setLength(int val) { std::cout << "setLength not implemented" << std::endl; }
private:
int val_;
};
//================================
class D1 : virtual public B {
public:
void setColor(int color) {
std::cout << "D1::setColor to " << color << std::endl;
color_ = color;
}
private:
int color_;
};
//================================
class D2 : virtual public B {
public:
void setLength(int length) {
std::cout << "D2::setLength to " << length << std::endl;
length_ = length;
}
private:
int length_;
};
//================================
// multi-inheritance diamond - have fiddled with mixin
// but haven't solved using type B polymorphically with mixins
class M1 : public D1, public D2 {
};
//================================
int main() {
B* d1 = new D1;
d1->setVal(3);
d1->setColor(1);
B* m1 = new M1;
m1->setVal(4);
m1->setLength(2);
m1->setColor(4);
return 0;
}
答案 0 :(得分:1)
原始示例代码疑似问题
您的示例存在许多问题。
首先,您不必在基类中提供函数体。请改用纯虚函数。
其次,您的类D1和D2都缺少功能,因此它们应该是抽象的(这将阻止您从它们创建被剥夺的对象)。如果您确实为基类使用纯虚函数,则第二个问题将变得清晰。编译器将开始发出警告。
像对new D1
一样实例化D1,是糟糕的设计,因为D1没有setLength方法的真正功能实现,即使你给它一个“假”&#39;身体。给它一个假人&#39;身体(没有做任何有用的事情)因此掩盖了你的设计错误。
所以你的评论(但我不想将默认函数放在基类中)证明了正确的直觉。必须这样做表明有缺陷的设计。 D1对象无法理解setLength,而其继承的公共接口承诺它可以。
而且:如果使用得当,多重继承没有错。它非常强大和优雅。但你必须在合适的地方使用它。 D1和D2是B的部分实现,所以抽象,并从两者继承将确实给你一个完整的实现,所以具体。
开始时可能是一个好的规则:只有当你看到迫切的需要时才使用多重继承。但如果你这样做,那就非常有用了。与例如相比,它可以防止相当一些丑陋的不对称和代码重复。像Java一样禁止它的语言。
我不是树医生。当我使用电锯时,我会危及我的腿。但这并不是说链锯不是很有用。
假人放在哪里:请不要忘记继续......
[OP首次评论后编辑]
如果您从B派生出一个类D1,它将打印出未实现的setLength&#39;如果你调用它的setLength方法,调用者应该如何反应?它不应该首先调用它,如果D1不是从具有这种方法的B派生的,那么调用者就可以知道它,纯虚拟或非虚拟。然后很明显,它只是不支持这种方法。拥有B基类让D1在多态数据结构中感到宾至如归,其元素类型,B *或B&amp ;,向用户承诺其对象正确支持getLength,而他们不会这样做。
虽然在你的例子中并非如此(但也许你把事情遗漏了),当然可能有充分的理由从B中派生出D1和D2.B可能拥有最终接口或其实现的一部分D1和D2都需要的派生类。
假设B有一个方法setAny(key,value)(在字典中设置一个值),D1和D2都使用,D1在setColor中调用它,D2在setLength中调用它。 在这种情况下,使用公共基类是合理的。在这种情况下,B根本不应该有虚拟方法setColor或setLength,既不是假人也不是纯粹的。您应该在D1类中使用setColor,在D2类中使用setLength,但在B类中都不能同时使用。
面向对象设计中有一条基本规则:
不要剥夺继承权
引入“不适用的方法”的概念&#34;在一个具体的课堂上,这正是你正在做的事情。现在这样的规则不是教条。但违反这一规则几乎总是指出一个设计缺陷。
一个数据结构中的所有B只对让他们做一个他们都明白的技巧很有用......
[OP的第二次实施后的EDIT2]
OP希望拥有一个可以保存从B派生的任何类的对象的地图。
这正是问题开始的地方。为了找到如何存储指针和对象的引用,我们不得不问:什么是用于存储的存储。如果一个地图,比如mapB用于存储指向B的指针,那么必须有一些点。通过数据存储,有趣的是检索数据并使用它做一些有用的事情。
通过使用日常生活中的列表,让我们更简单一些。假设我有一个1000人的personList,每个人都有他们的fullName和phoneNumber。现在说我的厨房水槽有问题。事实上,我可以通过列表阅读,打电话给每个人并问:你可以修理我的厨房水槽。换句话说:你支持repairKitchenSink方法吗?或者:你是否有机会成为班级水管工的实例(你是水管工)吗?但后来我花了很长时间打电话,也许在打了500个电话之后,我很幸运。
现在我的personList上的所有1000个人都支持talkToMe方法。因此,每当我感到孤独时,我都可以呼叫该列表中的任何人并调用该人的talkToMe方法。但他们不应该都有一个repairKitchenSink方法,甚至不是一个纯虚拟或虚拟变体做其他事情,因为如果我将这种方法称为一个类Burglar的人,他可能会响应这个电话,但是以一种意想不到的方式。
所以类Person不应该包含方法repairKitchenSink,即使不是纯虚拟方法。因为它永远不应该被称为personList的迭代的一部分。在迭代plumberList时应该调用它。此列表仅保存支持repairKitchenSink方法的对象。
仅在适当情况下使用纯虚拟功能
他们可能会以不同的方式支持它。换句话说,在类Plumber中,方法repairKitchenSink可以例如是纯粹的虚拟。例如,可能有是2个派生类,PvcPlumber和CopperPlumber。 CopperPlumber将通过调用lightFlame实现(代码)repairKitchenSink方法,然后调用solderDrainToSink,而PvcPlumber将实现它作为applyGlueToPvcTube和glueTubeToSinkOutlet的连续调用。但是两个管道工子类都以不同的方式实现repairKitchenSink。只有这样才能证明在他们的基类Plumber中使用纯虚函数repairKitchenSink。当然,一个类可能来自Plumber,它没有实现该方法,比如WannabePlumber类。但是因为它是抽象的,你不能从中实例化对象,这很好,除非你想要湿脚。
Person可能有许多不同的子类。它们例如代表不同的职业,或不同的政治偏好,或不同的宗教。如果一个人是民主党的Budhist Plumber,那么他(M / F)可能属于继承自Democrat,Budhist和Plumber等级的派生类。使用继承甚至打字,如政治偏好或宗教信仰那样不稳定,甚至职业和那些无穷无尽的组合,在实践中都不会有用,但它只是一个例子。在现实中,职业,宗教和政治参考可能是属性。但这并没有改变这里重要的观点。如果某个类不支持某个操作,那么它不应该在一个表明它的数据结构中。
除了personList之外,还有plumberList,animistList和democratList,你一定要打电话给那个理解你对方法inviteBillToPlayInMyJazzBand或者visitTheTreeInMyBackyard的调用的人。
列表不包含对象,它们只包含指针或对象的引用。因此,我们的民主派Budhist Plumber包含在personList,democratList,budhistList和plumberList中并没有错。列表就像数据库索引。不包含记录,他们只是引用它们。您可以在一个表上拥有许多索引,您应该这样做,因为索引很小并且使数据库运行速度快。
同样适用于多态数据结构。即使personList,democratList,budhistList和plumberList变得如此之大以至于你的内存不足,解决方案通常不只是有一个personList。因为那时你会因为性能问题和代码复杂性问题而交换你的内存问题,这通常会更糟糕。
所以,回到你的评论:你说你希望你所有的派生类都在B的列表中。很好,但仍然B的接口应该只包含为列表中的所有内容实现的方法,因此没有虚拟方法。这就像通过图书馆并浏览所有书籍,寻找支持teachMeAboutTheLovelifeOfGoldfishes方法的书籍。
说实话,在告诉你这一切的时候,我一直犯了一个死罪。我一直在卖一般事实。但在软件设计中,这些并不存在。我一直试图把它们卖给你,因为我现在已经教了OO设计了30年,我想我已经认识到你被卡住了。但是对于每条规则都有很多例外。尽管如此,如果我已经正确地理解了你的问题,在这种情况下,我认为你应该选择单独的数据结构,每个数据结构只包含对象,这些对象实际上可以在你遍历特定数据结构时发挥作用。
点是方圆
正确使用多态数据结构(包含指针或对不同对象类型的引用的数据结构)的部分混淆来自于关系数据库的世界。 RDB使用平面记录表,每个记录具有相同的字段。由于某些字段可能不适用,因此称为“约束”字样。被发明。在C ++类中,Point将包含字段x和y。 Class Circle可以继承它并另外包含field&#39; radius&#39;。 Class Square也可以继承Point,但包含field&#39; side&#39;除了x和y。在RDB世界中,约束而非字段是继承的。因此,圆将具有约束半径== 0.并且方形将具有约束边== 0.并且点将继承两个约束,因此它将满足作为正方形和圆形的条件:点是正方形圆圈,在数学中的确如此。请注意,与C ++相比,约束继承层次结构是颠倒的。这可能令人困惑。
一般认为继承与专业化密切相关,但无法帮助的是什么。虽然这种情况经常发生,但并非总是如此。在许多情况下,C ++继承是扩展而不是专业化。这两者经常重合,但Point,Square,Circle示例表明这不是一般事实。
如果使用继承,C ++ Circle应该从Point派生,因为它有额外的字段。但是圆圈肯定不是一种特殊类型的点,反之亦然。顺便说一下,在许多实用的库中,Circle将包含一个Point类对象,它包含x和y,而不是从它继承,绕过了整个问题。
欢迎来到设计选择的世界
你遇到的是一个真正的设计选择,一个重要的选择。正如你所做的那样,非常认真地思考这样的事情,并在实践中尝试所有这些,包括所谓的错误&#39;那些,会让你成为程序员,而不是编码员。
答案 1 :(得分:0)
让我首先说明你想要做的是一种设计气味:很可能你实际上想要实现的目标可以以更好的方式实现。遗憾的是,我们无法知道您实际想要实现的目标,因为您只告诉我们您希望如何实现它。
但无论如何,您的实现很糟糕,因为方法报告“未实现”到程序的用户,而不是调用者。调用者无法对不执行预期操作的方法做出反应。更糟糕的是,你甚至没有将它输出到错误流,而是输出到常规输出流,所以如果你在任何产生常规输出的程序中使用该类,那么输出将被你的错误信息中断,可能会混淆程序进一步在管道中。)
这是一种更好的方法:
#include <iostream>
#include <cstdlib> // for EXIT_FAILURE
//================================
class B {
public:
virtual ~B() { }
void setVal(int val) { val_ = val; }
// note: No implementation of methods not making sense to a B
private:
int val_;
};
//================================
class D1 : virtual public B {
public:
void setColor(int color) {
std::cout << "D1::setColor to " << color << std::endl;
color_ = color;
}
private:
int color_;
};
//================================
class D2 : virtual public B {
public:
void setLength(int length) {
std::cout << "D2::setLength to " << length << std::endl;
length_ = length;
}
private:
int length_;
};
class M1 : public virtual D1, public virtual D2 {
};
//================================
int main() {
B* d1 = new D1;
p->setVal(3);
if (D1* p = dynamic_cast<D1*>(d1))
{
p->setColor(1);
}
else
{
// note: Use std::cerr, not std::cout, for error messages
std::cerr << "Oops, this wasn't a D1!\n";
// Since this should not have happened to begin with,
// better exit immediately; *reporting* the failure
return EXIT_FAILURE;
}
B* m1 = new M1;
m1->setVal(4);
if (D2* p = dynamic_cast<D2*>(m1))
{
p->setLength(2);
}
else
{
// note: Use std::cerr, not std::cout, for error messages
std::cerr << "Oops, this wasn't a D1!\n";
// Since this should not have happened to begin with,
// better exit immediately; *reporting* the failure
return EXIT_FAILURE;
}
if (D1* p = dynamic_cast<D1*>(m1))
{
p->setColor(4);
}
else
{
// note: Use std::cerr, not std::cout, for error messages
std::cerr << "Oops, this wasn't a D1!\n";
// Since this should not have happened to begin with,
// better exit immediately; *reporting* the failure
return EXIT_FAILURE;
}
return 0;
}
或者,您可以利用您的方法共享一些统一性的事实,并使用常用方法来设置所有:
#include <iostream>
#include <stdexcept> // for std::logic_error
#include <cstdlib>
#include <string>
enum properties { propValue, propColour, propLength };
std::string property_name(property p)
{
switch(p)
{
case propValue: return "Value";
case propColour: return "Colour";
case propLength: return "Length";
default: return "<invalid property>";
}
}
class B
{
public:
virtual ~B() {}
// allow the caller to determine which properties are supported
virtual bool supportsProperty(property p)
{
return p == propValue;
}
void setProperty(property p, int v)
{
bool succeeded = do_set_property(p,v);
// report problems to the _caller_
if (!succeeded)
throw std::logic_error(property_name(p)+" not supported.");
}
private:
virtual bool do_set_property(property p)
{
if (p == propValue)
{
value = v;
return true;
}
else
return false;
}
int value;
};
class D1: public virtual B
{
public:
virtual bool supportsProperty(property p)
{
return p == propColour || B::supportsProperty(p);
}
private:
virtual bool do_set_property(property p, int v)
{
if (p == propColour)
{
colour = v;
return true;
}
else
return B::do_set_property(p, v);
}
int colour;
};
class D2: public virtual B
{
public:
virtual bool supportsProperty(property p)
{
return p == propLength || B::supportsProperty(p);
}
private:
virtual bool do_set_property(property p, int v)
{
if (p == propLength)
{
length = v;
return true;
}
else
return B::do_set_property(p, v);
}
int length;
};
class M1: public virtual D1, public virtual D2
{
public:
virtual bool supportsProperty(property p)
{
return D1::supportsProperty(p) || D2::supportsProperty(p);
}
private:
bool do_set_property(property p, int v)
{
return D1::do_set_property(p, v) || D2::do_set_property(p, v);
}
};