UE5.7.1 源码版 UFUNCTION 参数传递踩坑:为什么 BlueprintImplementableEvent(FString) 会编译失败?

前言

最近在使用 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 的代码规范,也可以避免参数签名带来的编译问题。