gmb*_*ard 9 c++ vtable type-erasure
最近,我遇到了几个使用"手动"vtable的类型擦除实现 - Adobe ASLany_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 };
Run Code Online (Sandbox Code Playgroud)
我的问题是,这个成语有什么好处?据我所知,它只是重新实现编译器将免费提供的内容.parent_t::invoke调用时不会有额外间接的开销vtbl::invoke.
我猜这可能与编译器能够内联或优化调用vtbl::invoke或某事有关,但我对Assembler不够自在,能够自己解决这个问题.
具有有用vtable的类基本上要求动态分配.虽然你可以做一个固定的存储缓冲区并在那里分配,但这很麻烦; 一旦你离开,你就无法合理控制实例的大小virtual.有了手动vtable,你就可以了.
看看有问题的来源,关于各种结构的大小有很多断言(因为在一种情况下它们需要适合两个双打的数组).
另外,带有手工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操作通过带有1个条目的本地vtable操作).
如果你有一个手动的vtable,你可以创建动态类,你可以在其中分配一个vtable条目并将其指针设置为你选择的任何东西(可能是以编程方式).如果你有10个方法,每个方法可以处于10个状态之一,那么需要10 ^ 10个不同的类和普通的vtable.使用手动vtable,您只需要在某个表中管理每个类的生命周期(因此实例不会超过该类).
作为一个例子,我可以采用一种方法,并在类的特定实例(具有仔细的生命周期管理)或该类的每个实例上添加"run before"或"run after"方法.
生成的vtable也可能以各种方式比编译器生成的更简单,因为它们不是那么强大.例如,编译器生成的vtable处理虚拟继承和动态转换.除非使用,否则虚拟继承情况可能没有开销,但动态强制转换可能需要开销.
您还可以控制初始化.使用编译器生成的vtable,表的状态被定义(或未定义),如标准所示:使用手动滚动的表,您可以确保选择保留的任何不变量.
在C++出现之前,OO模式存在于C语言中.C++只是选择了一些合理的选项; 当你回到伪C风格的手册OO时,你可以访问这些替代选项.你可以装扮(用胶水),使它们看起来像普通的C++类型给临时用户,而在里面他们什么都不是.