最近,我遇到了几个使用"手动滚动"的类型擦除实现。 vtable - Adobe ASL's any_regular_t
就是一个例子,虽然我已经看到它在Boost ASIO中使用过(对于完成例程队列)。
基本上,父类型传递一个指针,该指针指向一个完整的子类型中定义的函数指针的静态类型,类似于下面的...
struct parent_t;
struct vtbl {
void (*invoke)(parent_t *, std::ostream &);
};
struct parent_t {
vtbl *vt;
parent_t(vtbl *v) : vt(v) { }
void invoke(std::ostream &os) {
vt->invoke(this, os);
}
};
template<typename T>
struct child_t : parent_t {
child_t(T val) : parent_t(&vt_), value_(val) { }
void invoke(std::ostream &os) {
// Actual implementation here
...
}
private:
static void invoke_impl(parent_t *p, std::ostream &os) {
static_cast<child_t *>(p)->invoke(os);
}
T value_;
static vtbl vt_;
};
template<typename T>
vtbl child_t<T>::vt_ = { &child_t::invoke_impl };
我的问题是,这个成语的优势是什么??据我所知,它只是重新实现编译器将免费提供的内容。 parent_t::invoke
调用vtbl::invoke
时,不会有额外间接的开销。
我猜测它可能与编译器能够内联或优化对vtbl::invoke
或其他东西的调用有关,但我觉得不舒服足够的Assembler能够自己解决这个问题。
答案 0 :(得分:8)
具有有用vtable的类基本上要求动态分配。虽然你可以做一个固定的存储缓冲区并在那里分配,但这很麻烦;一旦你去virtual
,你就无法合理控制实例的大小。使用手动vtable,即可。
瞥了一眼有问题的来源,关于各种结构的大小有很多断言(因为在一种情况下它们需要适合两个双打的数组)。
也是一个&#34;班级&#34;手工制作的vtable可以是标准布局;如果你这样做,某些种类的铸造变得合法。我没有看到在Adobe代码中使用它。
在某些情况下,它可以完全与vtable分开分配(正如我在基于视图的类型擦除时所做的那样:我为传入类型创建自定义vtable,并为其存储void*
,然后将我的界面发送到所说的自定义vtable)。我没有看到在Adobe代码中使用它;但是any_regular_view
作为any_regular
的伪引用可能会使用此技术。我将它用于can_construct<T>
或sink<T>
,function_view<Sig>
甚至move_only_function<Sig>
等所有权(所有权由unique_ptr
处理,通过本地vtable进行操作1条)。
如果你有一个手工制作的vtable,你可以创建动态类,你可以在其中分配一个vtable条目并将其指针设置为你选择的任何东西(可能是以编程方式)。如果你有10个方法,每个方法可以处于10个状态之一,那么需要10 ^ 10个不同的类和普通的vtable。使用手工制作的vtable,您只需管理每个课程即可。在某个地方的桌子上的生命(所以实例不会比这个课程更长)。
作为一个例子,我可以采用一种方法,并在&#34;之前添加一个&#34;运行。或&#34;追赶&#34;方法,在类的特定实例(具有仔细的生命周期管理),或在该类的每个实例上。
结果vtable也可能以各种方式比编译器生成的vtable更简单,因为它们并不强大。例如,编译器生成的vtable处理虚拟继承和动态转换。除非使用,否则虚拟继承情况可能没有开销,但动态强制转换可能需要开销。
您还可以控制初始化。使用编译器生成的vtable,表的状态被定义(或未定义),如标准所示:使用手动滚动,您可以确保选择的任何不变量。
在C ++出现之前,OO模式存在于C语言中。 C ++只是选择了一些合理的选项;当你回到伪C风格的手册OO时,你可以访问这些替代选项。你可以装扮(用胶水),以便他们像普通的C ++类型一样看起来给临时用户,而在里面他们只是。