前言
最近在使用UE5.7.1 源码版开发 UMG Widget 时,遇到了一个比较奇怪的问题。
同样是UFUNCTION(BlueprintImplementableEvent),下面两个函数,一个可以正常编译,一个却直接报错。
UFUNCTION(BlueprintImplementableEvent) void SwitchBackgroundStation(int32 StationIndex);正常编译。
而下面这个:
UFUNCTION(BlueprintImplementableEvent) void SetTips(FString Tip);却报出了下面的错误:
ItemTip.gen.cpp(62): error C2511: void UItemTip::SetTips(const FString&) 'UItemTip' 中没有找到重载的成员函数刚开始一直以为是:
Intermediate 缓存没有清理
UHT 没有重新生成
Live Coding 导致旧代码残留
BlueprintImplementableEvent 使用错误
结果全部排查后都不是。
最终通过查看 UHT 生成代码以及多组实验,终于定位到了真正原因。
一、问题复现
Widget 定义如下:
UCLASS() class STEPEDITOR_API UItemTip : public UUserWidget { GENERATED_BODY() public: UFUNCTION(BlueprintImplementableEvent) void SetTips(FString Tip); };编译报错:
ItemTip.gen.cpp(62): error C2511 void UItemTip::SetTips(const FString& Tip) 'UItemTip' 中没有找到重载成员函数注意这里有一个细节:
自己声明的是
void SetTips(FString Tip);而 UHT 生成的是
void UItemTip::SetTips(const FString& Tip)参数类型已经发生了变化。
二、查看 UHT 生成代码
打开:
Intermediate/Build/.../ItemTip.gen.cpp可以看到:
void UItemTip::SetTips(const FString& Tip) { ItemTip_eventSetTips_Parms Parms; Parms.Tip = Tip; UFunction* Func = FindFunctionChecked(NAME_UItemTip_SetTips); ProcessEvent(Func, &Parms); }这里已经明确可以看到:
FString被 UHT 自动转换成了:
const FString&这也是编译失败的直接原因。
三、进一步验证
为了确认是不是只有 FString 有问题,我又增加了几个测试函数。
UFUNCTION(BlueprintImplementableEvent) void TestString(FString Str); UFUNCTION(BlueprintImplementableEvent) void TestText(FText Text); UFUNCTION(BlueprintImplementableEvent) void TestName(FName Name); UFUNCTION(BlueprintImplementableEvent) void TestArray(TArray<int32> Array);结果如下:
| 参数类型 | 编译结果 |
|---|---|
| int32 | ✅ 正常 |
| FString | ❌ C2511 |
| FText | ❌ C2511 |
| FName | ❌(同样需要 const 引用) |
| TArray | ❌ C2511 |
可以发现:
所有大型对象类型都会出现相同的问题。
四、为什么 int32 没问题?
继续测试:
UFUNCTION(BlueprintImplementableEvent) void SwitchBackgroundStation(int32 StationIndex);完全正常。
原因很简单。
对于基础类型:
int32 float boolUHT 不会修改参数类型。
生成代码仍然是:
void SwitchBackgroundStation(int32 StationIndex)所以不会发生签名不一致。
五、BlueprintCallable 呢?
随后又测试了:
UFUNCTION(BlueprintCallable) void TestString(FString Str);编译报错:
LNK2019 无法解析的外部符号 UItemTip::TestString(FString)这个错误和前面的不是同一个问题。
原因非常简单:
BlueprintCallable只是把函数暴露给 Blueprint。
它仍然是一个普通 C++ 函数。
因此必须提供 cpp 实现:
void UItemTip::TestString(FString Str) { }否则一定会出现 LNK2019。
所以:
BlueprintCallable 的 LNK2019 属于正常行为;
BlueprintImplementableEvent 的 C2511 才是本文讨论的问题。
六、解决方案
把所有大型对象参数统一改成const 引用。
例如:
UFUNCTION(BlueprintImplementableEvent) void SetTips(const FString& Tip); UFUNCTION(BlueprintImplementableEvent) void TestText(const FText& Text); UFUNCTION(BlueprintImplementableEvent) void TestName(const FName& Name); UFUNCTION(BlueprintImplementableEvent) void TestArray(const TArray<int32>& Array);修改以后即可正常编译。
七、原因分析
从实验结果来看,可以得到下面几个结论。
1、UHT 会自动优化大型对象参数
对于:
FString
FText
FName
TArray
TMap
大部分 UStruct
UHT 在生成代码时,会采用:
const Type&而不是值传递。
例如:
自己写:
void Foo(FString Str);UHT 实际生成:
void Foo(const FString& Str);这样可以避免 Blueprint 调用时产生一次对象拷贝。
2、基础类型不会修改
例如:
int32 float bool依旧保持值传递。
因此不会出现签名问题。
3、BlueprintCallable 与 BlueprintImplementableEvent 的区别
BlueprintCallable
属于普通 C++ 函数。
必须自己实现。
UFUNCTION(BlueprintCallable) void Foo(int32 Value);必须有:
void UMyClass::Foo(int32 Value) { }否则一定出现:
LNK2019BlueprintImplementableEvent
实现由 Blueprint 完成。
不需要 cpp。
但是参数类型必须与 UHT 生成的一致。
否则会出现:
C2511八、推荐写法
建议以后所有 UFUNCTION 都遵循 Epic 的代码风格。
基础类型
int32 float bool FVector FRotator直接值传递即可。
例如:
void Foo(int32 Value);大型对象
统一使用 const 引用:
const FString& const FText& const FName& const TArray<T>& const TMap<K,V>& const FMyStruct&例如:
UFUNCTION(BlueprintCallable) void SetName(const FString& Name); UFUNCTION(BlueprintImplementableEvent) void OnDataLoaded(const TArray<int32>& Data); UFUNCTION(BlueprintNativeEvent) void OnTextChanged(const FText& Text);这样既符合 Epic 官方源码风格,也能避免 UHT 自动生成参数时出现签名不一致的问题。
九、最终建议
对于UE5.7.1 源码版开发,建议统一遵循下面的规范:
| 参数类型 | 推荐写法 |
|---|---|
| int32 | int32 |
| float | float |
| bool | bool |
| FString | const FString& |
| FText | const FText& |
| FName | const FName& |
| TArray | const TArray<T>& |
| TMap | const TMap<K,V>& |
| UStruct | const FMyStruct& |
按照这种方式编写UFUNCTION,既符合 Unreal Engine 的代码规范,也可以避免参数签名带来的编译问题。