我的印象是,无论何时执行以下操作之一:
virtual void aMethod();
void aMethod();
virtual void aMethod override;
实现公用纯虚拟方法实际上是在破坏二进制兼容性,这意味着如果项目基于DLL的早期版本构建,则由于有新方法可用,因此它将无法加载该项目。
根据我使用Visual Studio 2012进行的测试,这些都没有破坏任何东西。 Dependency Walker报告没有错误,并且我的测试应用程序正在调用适当的方法。
DLL:
class EXPORT_LIB MyClass {
public:
void saySomething();
}
可执行文件:
int _tmain(int argc, _TCHAR* argv[])
{
MyClass wTest;
wTest.saySomething();
return 0;
}
我发现的唯一未定义行为是,如果MyClass正在实现纯虚拟接口(interface),并且从我的可执行文件中调用了一种纯虚拟方法,然后在可执行文件使用的方法之前添加了一个新的纯虚拟方法。在这种情况下,Dependency Walker没有报告任何错误,但是在运行时,它实际上在调用错误的方法。
class IMyInterface {
public:
virtual void foo();
}
在可执行文件中
IMyInterface* wTest = new MyClass();
wTest->foo();
然后,在不重建可执行文件的情况下更改界面
class IMyInterface {
public:
virtual void bar();
virtual void foo();
}
现在,它正在悄悄地调用
bar()
而不是foo()
。遵循我的所有三个假设是否安全?
编辑:
这样做
class EXPORT_LIB MyClass {
public:
virtual void saySomething();
}
执行力
MyClass wTest;
wTest.saySomething();
然后用以下命令重建DLL:
class EXPORT_LIB MyClass {
public:
virtual void saySomething2();
virtual void saySomething();
virtual void saySomething3();
}
正在调用适当的
saySomething()
最佳答案
破坏二进制兼容性并不总是会导致DLL无法加载,在许多情况下,您将最终遭受内存破坏,这可能会或可能不会立即显而易见。这在很大程度上取决于您所做的更改的详细信息以及现在和现在在内存中的布局方式。
DLL之间的二进制兼容性是一个复杂的主题。让我们从您的三个示例开始;
virtual void aMethod();
几乎可以肯定这将导致不确定的行为,这在很大程度上取决于编译器,但是大多数编译器将对虚拟方法使用某种形式的vtable,因此添加新代码将改变该表的布局。
void aMethod();
这对于全局函数或成员函数很好。成员函数本质上只是一个带有隐藏的“this”参数的全局函数。它不会改变任何东西的内存布局。
virtual void aMethod override;
实现公用纯虚拟方法这不会完全导致任何未定义的行为,但是正如您所发现的,它不会做您期望的事情。针对该库的先前版本编译的代码不会知道此函数已被覆盖,因此不会调用新的实现,而是继续调用旧的impl。根据您的用例,这可能是问题,也可能不是问题,它不会引起任何其他副作用。但是我认为,根据您使用的编译器,您的工作量可能会有所不同。因此,最好避免这种情况。
阻止DLL加载的原因是您以任何方式(包括更改参数和范围)更改了导出函数的签名,或者删除了函数。这样一来,动态链接器将无法找到它。仅当使用相关函数作为链接器时,这才适用。仅导入代码中引用的函数。
还有许多其他方法来打破dll之间的二进制兼容性,这超出了此答案的范围。以我的经验,它们通常遵循改变内存中内容的大小或布局的主题。
编辑:我刚刚记得关于KDE Wiki关于C++中二进制兼容性的出色文章,其中包括很好的行事与不行 list 以及解释和解决方法。
关于c++ - 什么时候打破二进制兼容性,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/37149479/