我试图分析实现多态性的各种方法之间的权衡。我需要一个对象列表,它们在成员函数中有一些相似之处和一些差异。我看到的选项如下:
我的理解是,选项3中列表中的指针查找将比选项2的成员函数查找花费更长时间,因为成员函数的保证接近。
这些选项有哪些优点/缺点?我的首要任务是性能优于可读性。 有多态的其他方法吗?
答案 0 :(得分:3)
在每个对象中都有一个标志,每个函数都有一个switch语句。标志的值将每个对象定向到每个函数的特定部分
好的,如果基于标志的代码变化很小,这可能是有意义的。 这最小化了必须适合高速缓存的(重复)代码量,并避免了任何函数调用间接。在某些情况下,这些好处可能会超过交换声明的额外成本。
在对象中有一个成员函数指针数组,这些指针在构造时分配。然后,我调用该函数指针以获取正确的成员函数
您可以保存一个间接(到vtable),但也可以使对象更大,从而减少缓存的适应性。不可能说哪个会占据主导地位,所以你只需要描述一下,但这不是一个明显的胜利
有一个包含多个派生类的虚基类。这样做的一个缺点是我的列表现在必须包含指针,而不是对象本身
如果你的代码路径不同,那么将它们完全分开是合理的,这是最干净的解决方案。如果需要对其进行优化,可以使用专用的分配器来确保它们是顺序的(即使容器中没有顺序),也可以使用类似于Boost.Any的聪明包装器将对象直接移动到容器中。你仍然会得到vtable间接,但我更喜欢这个#2,除非分析显示它真的是一个问题。
因此,在您决定之前,您应该回答几个问题:
并且,在你回答了这些之后,你应该只是简介。
答案 1 :(得分:2)
使用switch
语句,如果要添加新类,则需要修改打开类的所有位置,这可能位于代码库中的不同位置。您的代码库之外可能还有一些地方需要修改,但也许您知道在这种情况下不是这种情况。
在每个成员中有一个成员函数指针数组,唯一的缺点是你为每个对象复制了那个内存。如果您知道只有一个或两个“虚拟”功能,那么这是一个不错的选择。
对于虚函数,你是正确的,你必须堆它们(或手动管理内存),但它是最可扩展的选项。
如果您不是可扩展的,那么(1)或(2)可能是您的最佳选择。与往常一样,唯一的方法是衡量。我知道许多编译器在某些情况下会通过跳转表实现switch语句,跳转表基本上与虚函数表相同。对于少量case
语句,他们可能只使用二进制搜索分支。
测量!
答案 2 :(得分:2)
实现更快的多态性的一种方法是通过CRTP idiom and static polymorphism:
template<typename T>
struct base
{
void f()
{
static_cast<T*>( this )->f_impl();
}
};
struct foo : public base<foo>
{
void f_impl()
{
std::cout << "foo!" << std::endl;
}
};
struct bar : public base<bar>
{
void f_impl()
{
std::cout << "bar!" << std::endl;
}
};
struct quux : public base<quux>
{
void f_impl()
{
std::cout << "quux!" << std::endl;
}
};
template<typename T>
void call_f( const base<T>& something )
{
something.f();
}
int main()
{
foo my_foo;
bar my_bar;
quux my_quux;
call_f( my_foo );
call_f( my_bar );
call_f( my_quux );
}
输出:
FOO!
吧!
QUUX!
静态多态性performs far better than virtual dispatch,因为编译器知道在编译时将调用哪个函数,并且它可以内联所有内容。
即使它提供动态绑定,它也不能以通用的异构容器方式执行多态,因为基类的每个实例都是不同的类型。
但是,这可以通过boost::any
等方式实现。