DLL + 导出类 + 模板成员函数 = 未解决的外部符号。有什么办法可以修复吗?

4

首先,这不是一个重复的问题,因为1)这是一个链接器问题,编译器已经通过了,因为我已经显式实例化了。2)这不是关于模板类,而是模板成员函数。3)我对代码结构有一些限制,所以一些现有的技巧不适用。我在这里搜索了我的标题和前几个线程(4083239120330521253206191284887636940394),都是关于模板类的,而不是模板成员函数。其他一些线程实际上是在讨论实例化失败,所以实际上是编译器问题,但我已经尝试过显式实例化并且编译已经成功通过了,再次重申。所以我希望你能稍微抑制一下关闭我的问题的冲动,这就是我的线程。

环境:

  • Windows 10 版本 1803
  • Visual Studio 2015 更新 3
  • 在 VS 中使用 Debug x64 模式

源代码:

有两个项目:

1)DllProject,构建为 dll,包含两个源文件:Dll.h 和 Dll.cpp。

Dll.h:

#pragma once

#ifdef _WINDLL
#define API_TYPE __declspec(dllexport)
#else
#define API_TYPE __declspec(dllimport)
#endif

class API_TYPE AClass {
public:
    template <class T> void Func(T& data);
    template <class T> void CallFunc(T& data) {
        Func<T>(data);
    }
};

Dll.cpp:

#include "Dll.h"

template <class T> void AClass::Func(T& data) {
    data++;
}

template void AClass::Func<float>(float&);  //attempt to explicitly instantiate

2)ExeProject是一个exe文件,包含Exe.cpp。

Exe.cpp:

#include "Dll.h"

int main() {
    AClass a;
    float f = 0.f;
    a.CallFunc<float>(f);
}

正如您所见,我想要的是仅调用dll中定义的模板成员函数CallFunc,它又调用了另一个核心模板成员函数Func来完成真正的工作。我不需要在exe中直接调用Func,因此也不需要在dll中导出它。只有CallFunc是需要导出的API,并且它能够正常工作。DllProject项目可以正确编译,ExeProject项目也可以无问题编译。但在链接时出现错误:

1>------ Build started: Project: ExeProject, Configuration: Debug x64 ------
1>Exe.obj : error LNK2019: unresolved external symbol "public: void __cdecl AClass::Func<float>(float &)" (??$Func@M@AClass@@QEAAXAEAM@Z) referenced in function "public: void __cdecl AClass::CallFunc<float>(float &)" (??$CallFunc@M@AClass@@QEAAXAEAM@Z)
1>C:\tmp\DllProject\x64\Debug\ExeProject.exe : fatal error LNK1120: 1 unresolved externals
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========

我已经在Dll.cpp的第8行显式实例化了模板成员函数Func,所以编译可以通过。但是,在CallFunc内部无法链接到来自dll中的这个实例化函数。我认为函数是在dll中实例化的,而不是在exe中,因为我为该类指定了__declspec(dllimport)。我不知道为什么CallFunc找不到Func;它就在它的正上方。
这些源代码是从一个大型开源项目中隔离和最小化的(DllProject对应库本身,而ExeProject用于测试代码),因此模板成员函数的实现和声明分别位于两个文件中,在大型项目中是不可避免的,并且代码结构不能更改。如其名称所示,DllProject将作为dll构建,尽管将所有内容构建和链接为静态库也没有任何问题。我已经进行了很多搜索,但是在这个论坛和其他论坛中的帖子要么将模板实现移入类声明中,要么通过#include .tpp文件将其移入头文件中,都违反了上述限制,或者建议以各种方式显式实例化,我认为我已经做到了,因为编译已经通过。
我尝试了以下方法:
1)在Release配置下编译

2) 使用inline(甚至__forceinline

3) 在类中的Dll.h文件中进行明确的特化:

class API_TYPE AClass {
public:
    template <class T> void Func(T& data);
    template<> void AClass::Func<float>(float&);
    template <class T> void CallFunc(T& data) {
        Func<T>(data);
    }
};

4) 将显式模板特化放在 Dll.h 类外:

class API_TYPE AClass {
...
};
template<> void AClass::Func<float>(float&);

6)在Dll.h文件中将显式实例化放在类的外部:

class API_TYPE AClass {
...
};
template void AClass::Func<float>(float&);

7)在Func的声明、模板实现和显式实例化中添加API_TYPE

template <class T> void API_TYPE Func(T& data);

但是它们都无法正常工作并报告相同的错误。
8)在Dll.h类中放置显式实例化:
class API_TYPE AClass {
public:
    template <class T> void Func(T& data);
    template <class T> void CallFunc(T& data) {
        Func<T>(data);
    }
    template void AClass::Func<float>(float&);
};

编译错误:错误 C2252:模板的显式实例化只能发生在命名空间作用域

9)将显式特化放入 Dll.cpp 中:

编译错误:错误 C2910:无法显式特化 'AClass::Func'

我希望这足以证明我的努力。那么,在上述限制条件下,有没有修复“未解决的外部符号”的可能性?如果您忘记了或根本没有阅读它,则该限制为

Func 的模板实现必须与其声明分开, 即不能在类声明或头文件中。

如果您认为我不知道模板函数应该被实例化,我想再重申一下,我已经明确地实例化了 Func,并尝试了许多种方式。所以编译器完全没有问题,但是链接器会抛出"未解析的外部符号"错误。那么为什么链接器找不到已经实例化的模板成员函数呢?我还使用 dumpbin 检查了导入库 DllProject.dll 的输出符号。符号 ??$Func@M@AClass@@QEAAXAEAM@Z 确实驻留在其中,就像地球向东旋转一样事实。那么您知道如何积极追踪链接器的行为,以找出它为什么无法找到函数位置,而不是盲目猜测吗?非常感谢。

“都是关于模板类的,而不是模板成员函数。” 这并没有太大的区别,定义必须出现在头文件或显式链接的单元中,以涵盖所有实例化的类型。 - πάντα ῥεῖ
你在exe项目中尝试使用显式实例化了吗? - john
我认为使用显式实例化的第6个变体应该有效。也许可以使用导出指令,例如 template API_TYPE void AClass::Func<float>(float&); - user7860670
我不需要在exe中直接调用Func,因此我不需要在dll中导出它。CallFunc 是一个内联函数,因此可执行文件将实例化和编译 CallFunc 并因此调用 Func 而不是库。因此必须导出。 - Acorn
你是否实际链接了库?对我来说,template API_TYPE void AClass :: Func<float>(float&); 这个变量很好用。 - user7860670
显示剩余3条评论
1个回答

4

№6使用导出指令应该可以工作:

template API_TYPE void AClass::Func<float>(float&);

显式实例化告诉编译器这个变量在某个翻译单位中被实例化,而导出指令则通知链接器应该导出/导入它。


首先,非常感谢您的回答!这个问题困扰了我很多天。我想补充两点评论:1)仅将__declspec(dllexport)添加到类是不够的。要导出在.cpp中单独实现的实例化模板成员函数,我们仍然必须将__declspec(dllexport)添加到模板函数声明中,以便链接器能够找到它。... - user5280911
尽管我已经尝试了这种方法(#7),但是我没有运气在我的机器上看到它工作。然后我找出了发生了什么事情。因为我正在使用VS,所以导入库是通过引用->DllProject链接的,而不是通过在链接器->附加依赖项列表中明确指定。设置此引用的属性也是我许多尝试之一,但其中一个未正确设置,我匆忙离开,因为我认为错误可能是由于某些C++语言特定原因引起的。因此,保持头脑清醒非常重要,特别是在解决像这样微妙的问题时 :-) - user5280911
再补充一点:3)导出定向的显式实例化放在哪里都无所谓,可以放在.h或.cpp文件中。关键是在函数声明中需要使用API_TYPE,即使包含它的类已经用同样的方式限定了。 - user5280911

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接