不可能在动态链接的C++库中使用虚函数?

Joh*_*nes 2 c++ virtual-functions dynamic-linking

例如,此代码段错误(请参阅main.cpp中的注释).

hello.h

struct A { virtual ~A() {} };
Run Code Online (Sandbox Code Playgroud)

HELLO.CPP

#include "hello.h"

extern "C" {
    void hello(A*& a) {
        a = new A;
    }
}
Run Code Online (Sandbox Code Playgroud)

main.cpp中:

#include <cassert>
#include <dlfcn.h>

#include "hello.h"

int main() {

    void* handle = dlopen("./hello.so", RTLD_LAZY);
    assert(handle);

    typedef void (*hello_t)(A*& a);
    hello_t hello = (hello_t) dlsym(handle, "hello");
    assert(hello);

    A* a = nullptr;
    hello(a);
    dlclose(handle);

    delete a; // -> segfault
}
Run Code Online (Sandbox Code Playgroud)

g++ -Wall -std=c++11 -g main.cpp -ldl -o main
g++ -Wall -std=c++11 -g -shared -fpic hello.cpp -o hello.so
Run Code Online (Sandbox Code Playgroud)

原因:A包含一个虚拟表.在a分配时,虚拟表包含指向函数段的指针hello.so.调用之后dlclose(),此函数段可能无效,导致指针无效,因此delete a调用函数内存不足.

到目前为止这么清楚.我的问题是,所有假设您dlclose()的库,然后继续使用对象:

  1. 是否无法在.so文件中分配具有虚拟析构函数的类的对象?没有比在堆上实现自己的vtable更好的技巧了吗?
  2. 如果不可能,在共享库中分配您不知道的任何内容是不是很危险?即使你知道std::string保证在没有虚拟析构函数的情况下实现,谁知道它是否包含一个内部对象?

Die*_*Epp 9

动态链接库没有问题.它们并不危险.

但是,您正在演示卸载动态加载的库的问题.您无法卸载库,然后继续使用对该库中数据的引用:数据指针,函数指针,虚函数等.就像在你之后使用指针一样delete:唯一的答案就是在delete你完成对象之前避免调用.

因此,使用动态加载的库dlopen()并且和... dlclose()一样危险......没有什么可以阻止你做错事,你必须知道如何正确使用这些功能.newdelete

在典型的使用中(没有动态加载),动态链接库将在程序执行的整个持续时间内保持驻留状态.因此,如果std::string在动态库中,其功能将始终可用.就像一个静态库.

  • 这里的问题不是堆积.问题是你正在调用一个析构函数,`A :: ~A()`,它在你卸载的库中.不要那样做.如果要使用该库包含的对象(如函数),则必须保持库已加载. (2认同)

Wyz*_*a-- 5

析构函数是虚拟的这一事实在这里并不真正相关,因为不涉及继承。问题很简单,您在关闭库后调用了库中实现的函数(析构函数)。直接调用非虚成员函数也会导致同样的问题,因为该函数的代码在库中并且库已被卸载。

只需保持库打开,直到使用完它实现的对象为止。

  • 在这种情况下它可能会起作用,因为编译器可能会内联简单的析构函数,因此不会发生实际的函数调用,但这是您不能依赖的实现细节。这是未定义的行为,其中“未定义”*有时*意味着“在这个特定的编译器、操作系统、月相等上没有问题”。 (2认同)
  • 唔。C++11 标准第 3.2 节(“一个定义规则”)规定“内联函数应在使用 odr 的每个翻译单元中定义”。如果这意味着保证在“main.cpp”翻译单元中定义“~A”(无论它是否实际内联),那么我的答案可能是不正确的:“virtual”关键字是唯一强制调用的东西到库中而不是“主”程序中的定义。(但这是特定于实现的:标准没有指定虚拟调用是如何实现的。) (2认同)

Mat*_*son 5

您的分析不正确.问题不在于dlclose(handle)析构函数的代码不再存在,这意味着您的代码崩溃了.保持库打开,可以调用析构函数.

但是,在某些情况下,您还会为每个共享库获取不同的堆,因此最好在共享库中保留动态分配对象的构造和销毁.您可以通过第二个函数实现这一目标bye(),做delete你的对象.

通常情况下"动态添加内容"的问题(你的例子中的a std::string不是很好,因为实现应该只分配char元素,但是std::vector在复制元素时可能会分配东西)通过将它封装在对象中来处理分配由共享库中的代码完成.

(您的分析中的细节也是错误的:vtable本身不存储在堆上,但指向它的vptr必须存储在对象中,在这种情况下,它在堆上)