我正在经历基于 vtable 的多态性的缺点。
Vtable Overhead:每个具有虚函数的类都需要有一个 vtable,其中包含指向其虚拟函数的函数指针。 这会增加对象的大小并消耗额外的 记忆力。
动态转换开销:使用dynamic_cast向下转换指针时 对于派生类,有额外的运行时开销 执行类型检查。
对象切片:将派生对象存储在基础容器中时 类对象,可能会发生对象切片。
指针的管理:防止对象切片,更好的管理 多态对象,我们必须使用指针,这可能需要 堆分配。
基于上述缺点,我想到了以下设计来实现运行时多态性。
class B; class C;
class A{
public:
int common_data1; //common properties
int common_data2;
int type; //type of child stored by the following variant. e.g. 0 for B, 1 for C. we cal also use enum instead of int
std::variant<B,C> child;
void common();
void default_behavior();
void specific()
{
switch(type){
case 0:
std::get<B>(child).specific();
break;
case 1:
std::get<C>(child).specific();
break;
default:
default_behavior();
}
}
};
class B : public A{
public:
int b_data;
void b_fun();
void specific();
};
class C : public B{
public:
int c_data;
void c_fun();
void specific();
};
似乎消除了上述缺点。
std::variant
。尽管使用
std::variant
会带来额外的开销,但与前面提到的缺点相比,它似乎提供了更好的替代方案。
C++ 专家能否指导我进行此设计并提供他们的深刻见解?
vtable 方法的优点是它是标准的,C++ 程序员应该知道它,并且它避免了类中的
type
和 switch-cases 之类的事情。
您当前的实现将无法编译,因为
std::variant
无法使用不完整的类型进行实例化。然而,一般来说,std::variant
可以是基于值类型的替代基于继承的多态性。但它确实有它自己的问题。
如果你像这样编写代码,它应该可以编译。
#include <variant>
struct Common {
int common_data1; // Common properties
int common_data2;
void common();
};
struct B: Common {
int b_data;
void specific();
};
struct C: Common {
int c_data;
void specific();
};
struct A: std::variant<B, C> {
using Base = std::variant<B, C>;
using Base::Base;
void common() {
std::visit([](auto& self) { self.common(); }, *this);
}
void specific() {
std::visit([](auto& self) { self.specific(); }, *this);
}
};
与基于继承的多态性相比的缺点:
A
。A
的所有用户都需要了解所有可能的实现(B
、C
...)来实例化 variant
。与基于继承的多态性相比的优点:
switch
的 std::variant
/std::visit
的调度可能比通过隐藏在 vtable 指针后面的函数指针更快。你的方法基本上只是带有小胡子的vtables。每个类有一个 vtable。当调用虚函数时,我们会得到编译器秘密存储在代表实例运行时类型的类中的一些值。该值可能只是一个指针/数字,与您的示例完全相同。然后,我们通过查找与我们的运行时类型相对应的正确表(vtable)来“调度”该值。然后在表中查找正确的函数。就性能而言,这相当于大约 2 个指针间接寻址。你的 switch 语句基本上只是单个函数的 vtable。
您的方法具有类似的开销(内存和性能方面)。 “对象切片”也存在同样的问题,因为您通过将不同类型的事物存储在同一向量中来抽象出类型。唯一的好处是您不需要使用指针来实现多态性。
IMO 更好的方法是将不同类型的事物存储在不同的列表/向量中。不再有调度,不再有“对象切片”,不再有指针。它的缓存效率也更高,并且更易于阅读。