寻找继承/多态性替代方案的任何理由



我经常需要能够迭代一组具有相似但不相同功能的对象(例如,想象一组Task对象,它们都有自己的Do()函数实现)。

我通常通过拥有所有任务都源自的基本Task类(带有虚拟Do())来实现这一点。然后,我可以在vector<Task*>vector<unique_ptr<Task>>中保存这些集合。

有什么理由(或者确实有可行的方法)以不同的方式做到这一点吗?

编辑:

为了简单起见,我纯粹使用(假想的)Task对象作为示例。在现实中,当前实际项目的一个典型案例是UI组合框架。在每个布局更新过程中,从根容器遍历一个"视觉树",并根据子属性(如偏移、对齐、大小等)递归地排列其子控件(子容器控件具有其他控件作为子控件)。父对象根据其类型和配置对其子对象进行不同的定位(想想WPF的Canvas、Grid、StackPanel等)。

通过在容器和其他动态/用户引发的行为之间拖放视觉元素,树在运行时不断变化,控件本身是一个不断扩展的家族(通过插件支持新的控件类型)。

如果你想对"一组任务"尽可能通用,你可以使用std::function:

std::vector<
std::function<void()>
> tasks;

这样,您的任务就不必全部继承自Task。甚至是物体。

void printHello() { cout << "Hellon"; }
tasks.push_back(printHello);
tasks.push_back([]{ /* do stuff */ });
struct Object // doesn't inherit from anything
{
void operator()() const {
// do other stuff
}
};
tasks.push_back(Object{});

这种技术被称为"类型擦除"。

您已经描述了一些本质上是动态多态性的规范用例。它经常被用作OOP中的一个例子,因为很明显,动态多态性在这种一般情况下是非常(最?)合适的。

但使用替代方案或其变体肯定有一些原因。这些原因大多是"特殊情况"。

一种常见的变体是使用某种形式的动态多态性包装到值语义类中。该家族中的一个变体是使用类型擦除(例如std::function)。这之所以更好,是因为在某些情况下它可以更轻量级(例如,"Do()"函数的无状态函子)。另一个原因也是,您可能不想将类绑定到"Task"基类,或者您可能有现有的类,希望以非侵入性的方式(不更改它们的继承)来适应此目的。

另一种选择是完全不使用虚拟函数和继承。例如,如果您的一组"派生类"很小,并且包含非常相似的类(相同的数据成员),那么通过在向量中按值存储这些对象,您可能会获得一些性能优势(即,内存访问模式将更直接、更高效地打包在缓存中)。如果"Do()"函数的实现只有很小的变化,那么在一个可以按值存储的类中简单地实现这些不同的行为(例如,在Do函数中使用switch语句)可能是值得的。

当然,如果您不需要运行时机制,那么就不应该使用动态多态性,而应该使用静态多态性。

但总的来说,我想说,动态多态性的替代方案在其他情况下更合适,而不是你刚刚提出的"不同对象的集合"场景。例如,当你需要算法中的多态行为(mixin、策略、访问者等)时,那么有更多的理由求助于替代方案。

"有什么不同的原因吗?">

是的,有:
请三思,如果您真的需要运行时多态性,那么在特定情况下,这可能会对性能造成不必要的影响。

如果所有的Task实现在编译时都是已知的,那么使用静态多态性可能会很好。请参阅CRT模式,如何实现静态多态性。


"你能详细说明吗?">

好吧,我会试试(因为你实际上要求一个virtual接口,因为已知的插件接口不需要编译时间):

您可以有一个(纯)virtual基本接口,由一些CRTP基类实现:

struct TaskInterface {
virtual void Do() = 0;
virtual ~TaskInterface() {}
};

可通过CRTP:实现

template<class Impl>
class TaskBase 
: public TaskInterface {
virtual void Do() {
DoDerivedImpl();
}
protected:
void DoDerivedImpl() {
static_cast<Impl*>(this)->DoImpl();
}
void DoImpl() {
// Issue a static_assert error here, that there's no appropriate overridden
// implementation of DoImpl() available:
static_assert
( static_cast<Impl*> (this)->DoImpl != TaskBase<Impl>::DoImpl
, "TaskBase requires an appropriate implementation of DoImpl()");
}
};

class TaskType1 : public TaskBase<TaskType1> {
public:
void DoImpl() {
cout << "TaskType1::DoImpl()" << endl;
}
};
class TaskType2 : public TaskBase<TaskType2> {
public:
void DoImpl() {
cout << "TaskType2::DoImpl()" << endl;
}
};
class TaskType3 : public TaskBase<TaskType3> {
// Missing DoImpl()
};
int main() {
std::vector<TaskInterface*> tasks;
TaskType1 t1;
TaskType2 t2;
// TaskType3 t3; // Uncomment to see compile time errors
tasks.push_back(&t1);
tasks.push_back(&t2);
// tasks.push_back(&t2);
for(std::vector<TaskInterface*>::iterator it = tasks.begin();
it != tasks.end();
++it) {
(*it)->Do();
}
}

有关定期编译实现的信息,请参阅LIVE DEMO
请参阅LIVE DEMO了解未注释的TaskType3用法。


优点是,您可以轻松地为多个接口使用多个mixin实现,以设置最终的"插件">类。

相关内容

最新更新