Odd behaviour of final on a virtual function
当final关键字添加到虚拟函数声明中时,我遇到了一个奇怪的情况,它的定义在一个单独的.cpp文件中。请考虑以下示例:
国际标准化组织
1 2 3 4 5 6 7 8
| class IClass //COM-like base interface
{
protected:
virtual ~IClass(){} //derived classes override this
public:
virtual void release() final;
}; |
dllmain.cpp(共享库)
1 2 3 4 5 6 7 8 9
| #include"IClass.hpp"
...
void IClass::release()
{
delete this;
}
... |
main.cpp(独立可执行文件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| //various includes here
...
int main(int argc, char** argv)
{
/* From"IGameEngine.hpp"
class IGameEngine : public IClass
{
...
};
*/
IGameEngine* engine = factoryGameEngine();
...
engine->release();
return 0;
} |
实际上,GCC 4.9.2将报告一份undefined reference to 'IClass::release()'。我的目标是让IClass::release()不可重写,同时将其实现隐藏在游戏引擎的共享库中。有什么建议吗?
- 虚拟函数总是使用ODR,除非它们是纯的。我相信在这种情况下,链接器可以发出一个错误。
- 我不能在我的GCC4.9.2上复制这个问题。请添加如何构建程序(编译器选项等)
- 您是否从DLL中导出了函数?@我错觉他们的ODR使用性是实现定义的。
- @DYP:dll是使用这个makefile构建的,主二进制使用相同的编译器标志,但以下链接器标志:-static-libgcc -static-libstdc++ -mwindows。
- @雅克:你是说江户十一〔一〕号?作为接口,它不是从dll导出的,只导出工厂函数。我希望final不会影响虚拟表的一致性。
- @bit2shift iirc,在Windows中,您需要从一个dll显式导出函数。例如,使用非标准__declspec(dllexport)
- @yakk[basic.def.odr]/p5:"如果虚拟成员函数不是纯的,则使用odr。"我认为是否存在链接器错误取决于实现。
- @DYP COM(和类似的)接口不是从DLL中"declspec(dllexport)",请阅读此答案。工厂函数是唯一的dll导出。
- @DYP上面的注释IGameEngine* engine以简短的形式描述了它。如果你想要详细描述,请看这里
- 对不起的。太累了:(但我觉得有趣的是:final是否要求可以链接函数而不是通过动态调度调用?
- 我得出的结论是,如果使用final,实施需要与声明一起进行。有点缺憾。
- 我认为这不在标准范围内(不包括链接库)。但从实现者的角度来看,这似乎是有道理的:final方法不需要动态调度,因此链接器将尝试直接调用它们。这是不可能的,因为它不是从dll导出的。
- 回答你的问题,似乎是这样。但我想测试一个场景,其中dll的iclass::release()与可执行文件不同。
- 在基类中最后一个虚拟函数的意义是什么?为什么不抛弃虚拟和最终的方面呢?
- Abstract接口对导出有什么影响?上面的代码不包含抽象接口——它有一个包含代码的类。该代码必须在编译或运行时共享(使用编译时动态加载器/存根)。如何与客户机代码共享它?你打算通过vtable"曝光"它吗?
- @tonyd一个可以通过动态调度访问的函数,但不能被重写。一个对所有派生接口通用的函数,用于调用实现的析构函数。
- @Yakk去看看com是如何工作的。
- 特别是,去看看我是怎么做到的。
- 你的代码不是COM,对吧?没有IDL,没有Coclass工厂?您只想使用稍微相似的运行时行为?关于纯虚拟方法的到answrs的链接比无用更糟糕。问题是,头文件告诉客户机,对->release()的调用保证被调度到一个特定的方法:因此客户机直接调用该方法,而忽略vtable,您会得到一个链接器错误。如果没有final,客户机认为它必须在调用它的方式中使用vtable,因此它不能访问方法的符号,没有链接器错误。
- 在你的main里打电话给engine->IClass::release();。现在无论有没有final你都会得到链接器错误。在final之前没有链接器错误的事实在某种意义上是偶然的:这是因为没有人直接调用方法。对于final,大家都知道engine->release()意味着engine->IClass::release(),后者更快,所以他们称之为engine->IClass::release()。Boom,链接器错误。如果你从外部继承了IClass,我想也会发生类似的事情。
对GCC使用final进行了一些挖掘,发现了标记为final get"disvirtualized"的虚拟函数,这是一个优化步骤,旨在通过使用静态调度加快虚拟调用,并可能嵌入它们。
这就解释了链接器错误,因为它试图将IClass::release()链接到可执行文件中,但未能在本地找到它。
这种"脱离现实"行为也出现在Clang上,但不太可能发生在MSVC上。++
部分相关建议
如果需要通过指向对象抽象类(或抽象基类)的指针释放对象:
抽象基类需要纯虚拟析构函数
在类之外提供析构函数的默认定义(空范围)
像往常一样,在所有派生类上实现析构函数
如果您还处理共享库:
从库中导出一对malloc/free函数
重写库头文件中的非数组new/delete运算符及其各自的std::nothrow版本
调用上面的malloc/free,不使用overriden操作符
由于接口实现将驻留在库中,因此为您认为客户机可构造的每个接口导出一个工厂函数。只需确保异常不会通过客户机和库之间的间隙传播。这样,客户机应用程序就可以在库的CRT分配的对象上使用delete,而不必麻烦。
< /块引用>