模块不存在:public_关于UE4模块中模板类的问题(error LNK2019)

这几天在UE4中练习数据结构的时候遇到了一个问题,当时的场景是这样的。

应用场景

  • 在UE4中新建了一个ContainerCollection模块。
  • ContainerCollection模块中定义了一个FDoubleLinkList模板类(注意:模板类中的成员函数声明跟定义都要写在DoubleLinkList.h文件内)
template<typename ObjectType>
class CONTAINERCOLLECTION_API FDoubleLinkList
{
    /*实现*/
    public: 
        /*其中的一个成员函数*/
        int32 Append(ObjectType* Data)
        {
            /*实现*/
        }
    
};
  • 在游戏主模块的Build.cs文件中正确的添加模块名。
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "ContainerCollection" });
  • 在游戏主模块的.Target.cs中正确的添加模块名。(xxxEditor.Target.cs也需要)
 ExtraModuleNames.AddRange(new string[] { "ToolLibrary", "ContainerCollection" });
  • 随便在游戏主模块中的某个.h文件中定义一个C++类。我这里是在ToolLibraryGameModeBase.h中定义了一个测试类。
class TOOLLIBRARY_API FMyNode
{
public:
	FMyNode(int32 _in):
		value(_in)
	{}

	int32 value;
};
  • 我在ToolLibraryGameModeBase中声明一个FDoubleLinkList<FMyNode>成员变量,正确引入头文件。然后再BeginPlay里调用一下Append的方法。

ToolLibraryGameModeBase.h文件

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "LinkList/DoubleLinkList.h"
#include "ToolLibraryGameModeBase.generated.h"

UCLASS()
class TOOLLIBRARY_API AToolLibraryGameModeBase : public AGameModeBase
{
	GENERATED_BODY()
	
public:
	virtual void BeginPlay() override;
    
	FDoubleLinkList<FMyNode> listLink;
};

ToolLibraryGameModeBase.cpp文件

void AToolLibraryGameModeBase::BeginPlay()
{
	Super::BeginPlay();
	listLink.Append(new FMyNode(1));

}

然后F7,静静等待。突然,啪一下。给我整个LNK2019LNK2001的链接器报错。(展示其中一部分)

ToolLibraryGameModeBase.cpp.obj : error LNK2019: 无法解析的外部符号 " declspec(dllimport) public: cdecl FDoubleLinkList<class FTest>::FDoubleLinkList<class FTest>(void)" ( imp_??0?$FDoubleLinkList@VFTest@@@@QEAA@XZ),该符号在函数 "public: cdecl AToolLibraryGameModeBase::AToolLibraryGameModeBase(class FObjectInitializer const &)" (??0AToolLibraryGameModeBase@@QEAA@AEBVFObjectInitializer@@@Z) 中被引用1>ToolLibraryGameModeBase.gen.cpp.obj : error LNK2001: 无法解析的外部符号 " declspec(dllimport) public: cdecl FDoubleLinkList<class FTest>::FDoubleLinkList<class FTest>(void)" ( _imp??0?$FDoubleLinkList@VFTest@@@@QEAA@XZ)

当时心就哇凉哇凉的,不过万能的百度还是能救一下我的。


百度结果分析

​ 先上链接。

  1. https://blog.csdn.net/u013891092/article/details/51584345
  2. https://blog.csdn.net/qq_41884002/article/details/99816073
  3. https://blog.csdn.net/sunnylion1982/article/details/8838556
  4. 模板类和模板函数在dll导出

引用一下其中一位大佬的原话。

在编译器中,一个编译单元(.obj文件)是由一个.cpp文件以及该头文件所 include 的头文件所组成.

​ 为了更好的理解这个问题,我再把大佬的解析概述(抄)一下,假设有如下代码。

function.h

template <class T_ELE>
class function
{
public:
    void test();
}

function.cpp

#include "function.h"
#include <iostream>

template <class T_ELE>
void function<T_ELE>::test()
{
    //do anything~
}

mian.cpp

#include "function.h"

void main()
{
    function<int>::test();
}

​ 按照大佬的原话,现在该项目中存在两个编译单元,一个由function.cpp跟它include的两个头文件function.hiostream(头文件包含的头文件暂时忽略)暂且叫 function编译单元 ,还有一个就是main.cpp跟它所包含的fuction.h,暂且叫它 main编译单元 。现在再来一段大佬的原话。

当一个项目中的所有编译单元(.obj文件)都已分离的形式独自进行编译之后,再由连接器将各个单独的编译单元进行连接,从而成为一个可以执行的.exe文件。那么,连接器是如何进行编译单元连接的呢?在C++ 的描述里,程序在编译的过程中会生成三个表:重定向表,导出符号表以及未解决符号表,而其中的导出符号表的作用是将程序中的所有符号与实际的地址联系在一起,而连接器要做的就是通过查找导出符号表中符号的实际地址将各个单独编译单元连接在一起。

​ 好了,现在F7一下,main.cpp 中的调用了使用类模板的test()函数,编译器在编译期间寻找main编译单元内部并没有找到具体的实现方法,只找到了函数的声明,所以编译器会在处于同一个编译单元的其他地方找,也就是在 function.h ,但是只找到函数的声明,也没有找到函数的具体定义。这时寻找函数具体实现方法这个任务就交给了链接器,而连接器在外部的function编译单元function.cpp中 里找到了函数的具体实现方法,如果是普通函数,到这里就应该皆大欢喜了,终于可以跑路了。正所谓模板一时爽,报错火葬场,模板类在你提供具体类型时会在编译器展开(也就是调用一下),生成特定类型的代码(具现化)。这里在main编译单元里调用模板类的函数,所以只有main编译单元自己知道。而在 function编译单元中并没有调用模板类的函数,所以模板类并没有具现化,编译器也不知道这个函数属于什么类型,没法给这个函数分配内存空间。(请自动忽略图片)

034262605fd440eb28ee0b00985ee189.png

​ 最后由大佬的原话来总结叭。

本项目一共有两个编译单元,一个是main,一个是function,而编译过程是每个编译单元单独编译过后再交给连接器进行连接的,也就是说在连接器之前两个编译单元就已经进行了编译,而在主函数main里面调用外部编译单元时,由于另外一个编译单元function在编译时模板类没有被调用而没有得到具现化,从而导致了连接器在函数主函数里调用了模板类函数,但是找不到具体的实现方法的情况,所以就出现开头的错误:LNK2019 无法解析的外部符号。

这里有大佬整理的几个解决方案。

  1. 在主函数包含头文件时将实现模板类的函数也包含进来。原因:一个编译单元内包含了.cpp文件以及被include 的头文件,如果将实现模板类的函数文件.cpp也包含进来,那么主函数调用就给了模板类函数一个具现化的机会。
  2. 将模板类的实现方法写在头文件里面。原因:同上,将实现写在头文件里面,那么主函数调用就给了模板类函数一个具现化的机会。
  3. 在实现模板类的文件中调用一下模板类。原因:调用一下让模板类函数得到具现化。

综上所述,第一阶段已经理清楚了。


等等!怎么还有第一阶段?

是的!还有第二阶段。。。。。。

一开始我就是照着这方案处理过了,可还是有LNK2019的错误?

那为什么还有这样的问题,这就得从盘古开天辟地女娲造人开始说起了......(省略几万字)

第二阶段开始时去看看UE4的模块化机制。

查看官方文档这几句重要的话。

正如引擎本身由一组模块构成一样,每个游戏也是由一个或多个游戏性模块构成的。这些模块类似于引擎以前的版本中的包的概念,它们都是一组相关类的容器。在虚幻引擎 4 中,由于游戏逻辑都可以通过 C++ 实现,所以模块实际上是 DLL 文件。
您可以创建一个主要的游戏模块,然后在创建多个额外的游戏相关的模块。您可以针对这些新模块创建 *.Build.cs 文件,然后把到这些模块的引用添加到您的游戏的 Target.cs 文件 (OutExtraModuleNames 数组)中。在C++代码中,请确保为您的游戏模块使用适当的宏。至少有一个模块必须使用 IMPLEMENT_PRIMARY_GAME_MODULE 宏,而所有其他模块应该使用 IMPLEMENT_GAME_MODULE 宏。虚幻编译工具将会自动发现这些模块,并编译额外的游戏DLL文件。

原来每一个模块都会被编译成一个DLL。

那好说,直接百度 C++DLL中的模板类

得到的回答都是类似

模板类是一个编译链接期间才实例化的类。只有用到才实例化,标准没有支持对模板类的导出。
模板是“源代码”级别的代码复用,而不是目标代码级别的代码复用。
我想你的DLL里如果没有用到这个模板,编译器就不知道该如果对模板类生成可用的代码。编译器也不会对每一个去实现一个函数加到你的dll里,这是不可能的。
模板类在实例化之前是不进行编译的,否则的话当其他程序通过dll调用该模板类的时候,编译又会报错

真相了!收工!直接删掉所有模板相关的代码,皆大欢喜,世界和平。

好叭!应该总有方法解决这种需求!我确信(认真)

万能的百度!!!

原来真有。

从上面的分析跟论坛里各个大佬的回答来看,基本上都是在说,模板类实例化的问题,因为在dll中未调用过模板类,也就是说模板未参与编译,所以在dll中是不会有关于模板类的二进制文件。所以想要在dll中导出模板类,必须将其实例化一次,或者使用__declspec(dllexport)。在模板类所在头文件DoubleLinkList.h

结尾处添加一句

template class _declspec(dllexport) FDoubleLinkList<yourclass>;

emmmmmm,这不就是变成了一个普通的C++类么?这里引用一位在CSDN发表类似文章博主的原话

是不是感觉很坑?没办法,能导出就不错了!

到此第二阶段完结!


到现在还是没解决问题,不过至少知道了很多信息。

  1. 模板类的成员函数声明与实现建议都写在.h文件中,因为一个编译单元内包含了.cpp文件以及被include 的头文件,如果将实现模板类的函数文件.cpp也包含进来,那么主函数调用就给了模板类函数一个具现化的机会。
  2. DLL中的模板类在没有被调用的情况下,不会参与编译。即使模板类被调用了,导出的模板类跟普通C++类无区别。
  3. 模板类尽量在单个DLL中使用,也就是尽量不要在UE4里跨模块引用。

写了这么多,也没见着个解决方案,这不全篇废话么?

是啊,以上都是讨论C++层面的事,但我用的是UE4啊,我可以让这个模板类不单独属于模块编译后的dll中啊,就跟STL一样,让每份引用了STL的dll都拥有一份STL源码那不就可以了。

好说,直接百度 UE4模块编译

算了!还是删代码来的实在。

短时间内,很难去看懂这些玄乎其乎的东西,但UE4里也有跟STL里差不多的容器,比如TArray我可以看看它的实现啊。

第三阶段开始扒源码

可以查到,TArray位于引擎的Core模块。看到这里,我心里想着STL做不到的东西被UE4实现了,部分源码如下

template<typename InElementType, typename InAllocator>
class TArray
{
    template <typename OtherInElementType, typename OtherAllocator>
    friend class TArray;

public:
    typedef typename InAllocator::SizeType SizeType;
    typedef InElementType ElementType;
    typedef InAllocator   Allocator;
    /*更多源码请自行翻看*/
}

好像也没啥特别,也没用什么特殊的关键字,可为什么在UE4的任何地方都能引用呢。按照UE4官方文档的说法,每多一个模块就会多一份dll,所以理论上Tarray并不属于Core模块,那就是在Build.cs文件中不管加不加Core模块,Tarray都能被调用,其他容器也同理可得。理论上是这样,行不行我还没试过(虽然自己写的模板类确实可以)。果然如某位大佬回答一样,模板是源代码级的复用

但是这怎么做到的呢?

仔细观察,发现声明模板类TArray前面并没有 CORE_API这个宏。而我的

template<typename ObjectType>
class CONTAINERCOLLECTION_API FDoubleLinkList{};

所以......就这?

OK!我删掉它了,编译确实通过了。

原因?期待有大神能开篇文章去解析UBT的工作原理。


第一次写文章,参照了很多大神的文章,有些文章看得一知半解,所以写的时候很多地方都比较含糊。不过以后返回来看自己写的文章一定会有更深刻的理解,到时候嘴角一撇,拿起键盘,嘴上骂骂咧咧地重新敲一遍。

在这里感谢每一位在网上无私分享的大佬,包括但不限于

  1. CSDN博主[性感博主在线瞎搞]
  2. CSDN博主[zhengudaoer]
  3. UE4引擎官方文档
  4. 以及知乎上任何一位热心回答问题的大佬
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值