C++ 库交叉依赖 - 从 llvm 移植到 gcc

C++ library cross dependencies - porting from llvm to gcc

我正在尝试将我最初在 Mac llvm 上编写的一些 C++ 代码移植到 Windows Cygwin gcc。在这个项目中,我静态 linking 一个带有两个库的 exe(我正在使用 cmake):

add_executable(myexe main.cc)
target_link_libraries(myexe lib1 lib2)

lib1中有一个class,它声明了一个虚方法:

lib1/Class1.h:

class Class1 
{
public:
    void method1();
    virtual void method2();
};

lib1/Class1.cpp:

#include "Class1.h"
void Class1::method1() {
    // do work
}
// Note that method2 is not defined!

Class1::method2 不是从 lib1 调用的,所以这工作正常。

Class1::method2 定义在 lib2:

lib2/Class2.h:

#include "Class1.h"

class Class2
{
private:
    Class1 c1;
public:
    void call_c1();
};

in lib2/Class2.cpp:

#include "Class2.h"

void Class1::method2() {
    // do some other work
}

void Class2::call_c1() {
    c1.method2();
}

当我在 MacOS 下使用 llvm link 编译和 link 时,所有这些工作正常。当我尝试在 Windows/Cygwin 上使用 gcc 构建它时,我 运行 遇到各种 link 错误,例如 undefined reference to vtableundefined reference to 'Class1:method2'。实际错误取决于 target_link_libraries 调用中库的顺序。

是否有任何命令行选项可以传递给 gcc/cmake 以使其正常工作?或者考虑 Windows 上的另一个工具链可能更好?我现在实际上在两个平台上都使用 IntelliJ CLion。

在此先感谢您的帮助。

您在 lib1 中对 Class1 的定义不完整 - 您将其定义为虚方法,而不是纯虚方法。

当您尝试 link lib1 时,虚拟方法必须提供一个实现,否则 linking 将失败 - linker 将无法找到任何实现void Class1::method2() 因为 none 存在。我不确定其他编译器如何允许这样做;可能是其他编译器的bug,或者你的编译环境和你说的不完全一样。

如果您将该方法标记为纯虚拟方法,这会改善这种情况,因为它将允许 lib1 编译(尽管还有更多问题):

class Class1 
{
public:
    void method1();
    virtual void method2() = 0; // mark the method as pure virtual.
}

不过还有一些更大的问题 - 您试图在与 Class1.cpp 不同的文件中定义 Class1 的方法,然后尝试启用 Class2 以在 Class1 中编译和调用此方法。

看起来您确实在尝试使用继承 - 您希望 Class1::method2() 是纯虚拟的,Class2 继承自 Class1,并让 Class2 为 method2() 提供实现。

Class1.h:

class Class1
{
    public:
        void method1();
        virtual void method2() = 0;
};

Class1.cpp:

#include "Class1.h"

void Class1::method1() {
    // do work
}

Class2.h:

#include "Class1.h"

class Class2 : public Class1
{
    public:
        virtual void method2();
};

Class2.cpp:

#include "Class2.h"

//        - Pay close attention to this small but important change.
//        V
void Class2::method2() {
    // do some other work
}

然后,您可以直接调用 method2(),而不是调用 Class2::call_c1():

SomeFile.cpp:

#include "Class2.h"

int main() {
    Class2 someInstance;

    someInstance.method2();
}

编译器可能发出 Class1 的 vtbl,其中包含对 Class1::method2 的引用,指向 lib1。如果 lib2 中的一个编译单元(即目标文件)定义了 method1 而另一个引用了 Class1,那么库就会相互依赖。由于链接器(至少是 GNU ld)在默认情况下以一次性模式工作,因此必须两次指定 lib2,一次在 lib1 之前,一次在 lib1 之后。因此 cmake 指令是 target_link_libraries(myexe lib2 lib1 lib2).

GNU ld 还可以解决一组库之间的所有依赖关系,方法是用 --start-group--end-group 将它们括起来——这会显着降低链接性能。您可以通过 -Wl,--start-group 等通过 gcc 传递它们。不过,我不知道如何让 cmake 做到这一点。

这背后的原因是库不是作为一个整体链接的,而是在编译单元的基础上链接的,因此只有库中实际需要的部分才会出现在可执行文件中。在目前的情况下,从主程序引用 Class2 会在链接 lib2 时导致对 Class1 vtbl 的未解析引用。链接 lib1 满足此引用,但会创建另一个未解析的 Class1::method2,因为此方法的地址是 vtbl 的一部分。所以链接器必须重新检查 lib2 来解决这个问题。

请注意,只有在lib2中引用vtbl的编译单元不是定义Class1::method2的编译单元时才会出现此问题;在那种情况下,符号定义已经存在,不需要第二次遍历 lib2。也许这就是为什么对您的问题的评论表明您的示例工作正常。

链接器以一次性模式工作,从左到右检查库,因为这种类型的相互依赖很少见,而且完整解析会降低性能(由于历史原因,当核心内存稀缺和成堆的穿孔卡片不容易随机访问)。