类层次结构的一个常见错误是将基类中的方法指定为虚拟,以便继承链中的所有覆盖执行某些操作,并且忘记传播调用基于实现。
class Container
{
public:
virtual void PrepareForInsertion(ObjectToInsert* pObject)
{
// Nothing to do here
}
};
class SpecializedContainer : public Container
{
protected:
virtual void PrepareForInsertion(ObjectToInsert* pObject)
{
// Set some property of pObject and pass on.
Container::PrepareForInsertion(pObject);
}
};
class MoreSpecializedContainer : public SpecializedContainer
{
protected:
virtual void PrepareForInsertion(ObjectToInsert* pObject)
{
// Oops, forgot to propagate!
}
};
我的问题是:是否有一种好的方法/模式可以确保在调用链的末尾始终调用基础实现?
我知道有两种方法可以做到这一点。
您可以使用成员变量作为标志,在虚拟方法的基本实现中将其设置为正确的值,并在调用后检查其值。这需要使用公共非虚方法作为客户端的接口,并使虚方法受到保护(这实际上是一件好事),但它需要专门为此目的使用成员变量(需要如果虚方法必须是常量,则是可变的。
class Container
{
public:
void PrepareForInsertion(ObjectToInsert* pObject)
{
m_callChainCorrect = false;
PrepareForInsertionImpl(pObject);
assert(m_callChainCorrect);
}
protected:
virtual void PrepareForInsertionImpl(ObjectToInsert* pObject)
{
m_callChainCorrect = true;
}
private:
bool m_callChainCorrect;
};
class SpecializedContainer : public Container
{
protected:
virtual void PrepareForInsertionImpl(ObjectToInsert* pObject)
{
// Do something and pass on
Container::PrepareForInsertionImpl(pObject);
}
};
另一种方法是用不透明的“cookie”参数替换成员变量并执行相同的操作:
class Container
{
public:
void PrepareForInsertion(ObjectToInsert* pObject)
{
bool callChainCorrect = false;
PrepareForInsertionImpl(pObject, &callChainCorrect);
assert(callChainCorrect);
}
protected:
virtual void PrepareForInsertionImpl(ObjectToInsert* pObject, void* pCookie)
{
*reinrepret_cast<bool*>(pCookie) = true;
}
};
class SpecializedContainer : public Container
{
protected:
virtual void PrepareForInsertionImpl(ObjectToInsert* pObject, void* pCookie)
{
// Do something and pass on
Container::PrepareForInsertionImpl(pObject, pCookie);
}
};
在我看来,这种方法不如第一种方法,但它确实避免使用专用的成员变量。
还有哪些其他可能性?
答案 0 :(得分:22)
你已经想出了一些聪明的方法来实现这一点,(正如你所承认的那样)膨胀课程的成本,并添加代码来解决对象的责任而不是程序员的缺陷。
真正的答案是不要在运行时这样做。这是程序员错误,而不是运行时错误。
在编译时执行:如果语言支持,则使用语言构造,或者使用模式强制执行它(例如,,模板方法),或者使编译依赖于传递的测试,并设置测试来强制执行它。
或者,如果传播失败导致派生类失败,则让它失败,并使用异常消息通知派生类的作者他未能正确使用基类。
答案 1 :(得分:13)
您正在寻找的只是非虚拟接口模式。
它类似于你在这里所做的,但基类实现是保证被调用,因为它是可以被调用的唯一实现。它消除了上述示例所需的混乱。并且通过基类的调用是自动的,因此派生版本不需要进行显式调用。
Google“非虚拟界面”了解详情。
编辑:查找“模板方法模式”后,我发现它是非虚拟接口的另一个名称。我之前从未听过这个名字(我不是GoF粉丝俱乐部的卡片成员)。就个人而言,我更喜欢名称非虚拟接口,因为名称本身实际上描述了模式是什么。
再次编辑:这是NVI执行此操作的方式:
class Container
{
public:
void PrepareForInsertion(ObjectToInsert* pObject)
{
PrepareForInsertionImpl(pObject);
// If you put some base class implementation code here, then you get
// the same effect you'd get if the derived class called the base class
// implementation when it's finished.
//
// You can also add implementation code in this function before the call
// to PrepareForInsertionImpl, if you want.
}
private:
virtual void PrepareForInsertionImpl(ObjectToInsert* pObject) = 0;
};
class SpecializedContainer : public Container
{
private:
virtual void PrepareForInsertionImpl(ObjectToInsert* pObject)
{
// Do something and return to the base class implementation.
}
};
答案 2 :(得分:6)
当只有一个级别的继承时,您可以使用公共接口非虚拟的template method pattern并调用虚拟实现功能。然后基数的逻辑进入公共函数,确保被调用。
如果你有多个级别的继承,并希望每个类都调用它的基类,那么你仍然可以使用模板方法模式,但有一个扭曲,使虚拟函数的返回值只能由 base
因此derived
将被强制调用基本实现以返回值(在编译时强制执行)。
这并没有强制每个类调用它的直接基类,它可能会跳过一个级别(我想不出一个强制执行的方法)但是它确实迫使程序员做出有意识的决定,换句话说,它反对不注意而不是恶意。
class base {
protected:
class remember_to_call_base {
friend base;
remember_to_call_base() {}
};
virtual remember_to_call_base do_foo() {
/* do common stuff */
return remember_to_call_base();
}
remember_to_call_base base_impl_not_needed() {
// allow opting out from calling base::do_foo (optional)
return remember_to_call_base();
}
public:
void foo() {
do_foo();
}
};
class derived : public base {
remember_to_call_base do_foo() {
/* do specific stuff */
return base::do_foo();
}
};
如果您需要public
(非virtual
)函数返回值,则内部virtual
应返回std::pair<
return-type ,remember_to_call_base>
。
注意事项:
remember_to_call_base
有一个显式构造函数声明为private,因此只有friend
(在本例中为base
)才能创建此类的新实例。remember_to_call_base
没有明确定义的复制构造函数,因此the compiler will create one具有public
辅助功能,允许从base
按值返回实施remember_to_call_base
在protected
的{{1}}部分声明,如果它在base
部分private
将无法在derived
部分引用所有答案 3 :(得分:4)
完全不同的方法是注册仿函数。派生类将在派生类构造函数中向基类注册一些函数(或成员函数)。当客户端调用实际函数时,它是一个基类函数,然后迭代注册函数。这可以扩展到许多级别的继承,每个派生类只需要关注它自己的功能。
答案 4 :(得分:0)
查看模板method pattern。 (基本思想是你不必再调用基类方法了。)
答案 5 :(得分:0)
一种方法是根本不使用虚拟方法,而是允许用户注册回调,并在执行prepareForInsertion工作之前调用它们。这样就不可能犯这个错误,因为它是确保回调和正常处理都发生的基类。如果你想要很多函数的这种行为,你最终可能会遇到很多回调。如果你真的那么多地使用那个模式,你可能想要研究像AspectJ这样的工具(或者C#的等价物),它们可以自动化这类事物。
答案 6 :(得分:0)
如果您发现可以隐藏虚拟功能并使界面非虚拟,请尝试而不是检查其他用户是否确实调用了您的功能,而只是自己调用它。如果最后调用基本代码,它将如下所示:
class Container
{
public:
void PrepareForInsertion(ObjectToInsert* pObject)
{
PrepareForInsertionImpl(pObject);
doBasePreparing(pObject);
}
protected:
virtual void PrepareForInsertionImpl(ObjectToInsert* pObject)
{
// nothing to do
}
private:
void doBasePreparing(ObjectToInsert* pObject)
{
// put here your code from Container::PrepareForInsertionImpl
}
};