前言
最近在使用 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
bool
UHT 不会修改参数类型。
生成代码仍然是:
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)
{
}
否则一定出现:
LNK2019
BlueprintImplementableEvent
实现由 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 的代码规范,也可以避免参数签名带来的编译问题。