news 2026/8/7 1:40:12

Unreal对C++做了什么 · Part4幕后 · 第 15 章 · UHT 与宏的真相

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unreal对C++做了什么 · Part4幕后 · 第 15 章 · UHT 与宏的真相

第 15 章 · UHT 与宏的真相

第 4 章里我们给了一个"UHT 速写":UHT 扫描头文件、识别宏、生成反射注册代码。本章兑现那个承诺——完整追踪一个 UCLASS 从你写下宏的那一刻,到编译器看到最终代码的整条链路。

你会看到 .generated.h 和 .gen.cpp 里到底生成了什么,以及 GENERATED_BODY() 展开后的真实模样。


15.1 两阶段编译模型

标准 C++ 的编译管线是线性的:

.cpp / .h → 预处理器(展开 #include、宏) → 编译器(生成 .obj) → 链接器(生成 .exe/.dll)

Unreal 在"预处理器"之前插入了额外的一步:

头文件(含 UCLASS/UPROPERTY/UFUNCTION) │ ▼ ┌─────────┐ │ UHT │ 扫描宏、解析说明符、生成 C++ 代码 └────┬────┘ │ 输出 ▼ .generated.h + .gen.cpp │ ▼ 预处理器 → 编译器 → 链接器

UHT 是一个独立的可执行程序(或由 UBT 调用的工具),不是编译器插件。它不理解 C++ 的完整语法——它只做有限的解析:找到UCLASSUPROPERTYUFUNCTION等宏,提取类名、成员名、说明符参数,然后根据模板生成代码。

所以 Unreal 的"编译"实际上是两阶段:UHT 阶段(生成代码)和C++ 阶段(和你手写的代码一起编译)。


15.2 UHT 扫描阶段

扫描哪些文件

UBT 在编译每个模块前,会先列出该模块中所有#include “xxx.generated.h”出现的头文件。只有这些文件会被交给 UHT。所以:

  • 如果你在一个 .h 里写了 UCLASS 但忘了写#include "MyClass.generated.h",UHT 不会处理它,后续编译会报错(缺少 GENERATED_BODY 等符号)。
  • 包含 .generated.h 的必须是最后一个 #include。这是约定,方便 UHT 和生成代码的宏正确展开。

UHT 能识别什么

UHT 会查找并解析以下宏(以及它们的变体,如带参数、带返回值):

  • UCLASS(...)
  • USTRUCT(...)
  • UENUM(...)
  • UPROPERTY(...)
  • UFUNCTION(...)
  • UDELEGATE(...)/DECLARE_DYNAMIC_DELEGATE_...
  • GENERATED_BODY()/GENERATED_USTRUCT_BODY()

它不会解析你类里的普通 C++ 代码——不解析模板、不解析复杂的继承、不解析预处理器条件。它主要依赖"宏出现在类/成员上方"这种固定模式,用正则或简单语法去提取类名、成员名、说明符键值对

说明符如何被提取

例如:

UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="Combat")floatMaxHealth;

UHT 会得到:

  • 属性名:MaxHealth
  • 类型:float(通过后面的声明解析)
  • 说明符:EditAnywhere(flag)、BlueprintReadWrite(flag)、Category="Combat"(key-value)

这些信息会被编码进即将生成的 C++ 代码里——生成注册 FProperty 的调用、设置 CPF_Edit 等标志、写入元数据 Map。


15.3 代码生成阶段:.generated.h

.generated.h 的主要职责是:在你自己的类声明里插入一段"占位符",这段占位符由宏展开成 UHT 生成的声明。

典型结构(概念上)如下:

// 1) 包含必要的引擎头文件#include"UObject/NoExportTypes.h"#include"..."// 本模块需要的其他头文件// 2) 前置声明和模块 API 宏#defineMYGAME_API...// 3) 为 GENERATED_BODY() 准备的宏定义// 这里会根据类的父类、是否抽象等,定义不同的宏体#defineMYGAME_APIAMyCharacter_NoPureDeclarations(...)\/* 一堆声明:拷贝构造删除、GetNativeClass()、StaticClass()、序列化辅助等 */\...// 4) 你类里的 GENERATED_BODY() 会展开成对上面宏的调用// 例如:// #define GENERATED_BODY() \ // MYGAME_API AMyCharacter_NoPureDeclarations(AMyCharacter, ACharacter, ...)

也就是说,GENERATED_BODY() 在预处理器阶段会展开成一大段成员声明。这些声明包括(根据引擎版本和类类型略有差异):

  • 删除的拷贝构造函数/赋值运算符
  • static UClass* GetStaticClass()或等价物
  • UClass* GetNativeClass() const override
  • 与序列化、CDO 相关的内部接口
  • 有时还有反射用的小型结构体

你不需要手写这些——UHT 根据你的类名、父类名、是否 USTRUCT 等,选择对应的模板生成。


15.4 代码生成阶段:.gen.cpp

.gen.cpp 里是反射数据的注册代码,在程序启动时执行,把 UClass、FProperty、UFunction 等填进引擎的全局表。

典型内容结构

1) 静态结构体:属性/函数元数据

UHT 会为每个 UPROPERTY 生成一个小的静态结构体,描述偏移、类型、标志位、元数据,例如(简化):

structZ_Construct_UClass_AMyCharacter_Statics{staticconstFProperty*constPropPointers[];staticconstFCppClassTypeInfoStatic StaticCppClassTypeInfo;staticconstUECodeGen_Private::FFloatPropertyParams MaxHealthParam;staticconstUECodeGen_Private::FFloatPropertyParams CurrentHealthParam;// ...};

2) 属性注册表

每个 UPROPERTY 对应一个FProperty的构造参数(如 FFloatPropertyParams),里面包含:

  • 属性名(FName)
  • 偏移量(通过offsetof(AMyCharacter, MaxHealth)或类似方式得到)
  • 标志位(CPF_Edit、CPF_BlueprintVisible 等)
  • 元数据(Category 等)

3) UClass 注册函数

一个大的函数,通常叫Z_Construct_UClass_AMyCharacter(),里面会:

  • 调用UClass::StaticClass()或等价机制获取/创建 UClass;
  • 注册父类关系;
  • 逐个注册 FProperty(通过UClass::AddPropertyToClass或类似 API);
  • 为每个 UFUNCTION 注册 UFunction 和 thunk;
  • 设置类的 CDO 等。

4) 静态初始化

通过一个在程序启动时执行的静态初始化(如IMPLEMENT_CLASS宏展开后的代码),调用上面的Z_Construct_UClass_AMyCharacter(),从而在 main 之前就把反射树建好。

5) UFunction 的 thunk

每个 UFUNCTION 会生成一个小的"thunk"函数,把反射调用(ProcessEvent 传来的参数块)转成对实际 C++ 成员函数的调用,包括从参数块里按偏移取出参数、调用你的函数、把返回值写回。


15.5 GENERATED_BODY() 的展开

在 UE4 的早期版本中,有的类用的是GENERATED_UCLASS_BODY(),它会在类里插入一个构造函数声明(或定义),用于初始化 UObject 的默认属性等。后来引擎统一推荐使用GENERATED_BODY(),不再在宏里注入构造函数,构造函数完全由你手写。

GENERATED_BODY()必须放在类体的最前面(在第一个访问说明符之后、第一个成员之前),因为展开后的声明需要作为类的第一部分,以满足引擎对 UObject 布局的假设(例如虚表、反射信息指针等)。

如果你忘记写GENERATED_BODY(),会出现两类错误:

  1. UHT 报错:UHT 发现 UCLASS 但没有对应的 body 宏,会直接报错。
  2. 链接错误:即使 UHT 生成了 .gen.cpp,你的类里缺少 StaticClass() 等声明,链接时会找不到符号。

15.6 宏标记的规则与限制

UHT 的解析能力有限,因此有一系列"不能做的事":

不支持的 C++ 特性

  • 模板类template<typename T> class UMyClass : public UObject不能是 UCLASS。UHT 无法为模板实例化生成多份反射数据。
  • 多继承 UObject:一个类只能继承一个 UCLASS 标记的基类(可以是多个 UInterface)。
  • 匿名结构体/联合体中的 UPROPERTY:需要给结构体命名并用 USTRUCT if 需要反射。
  • 某些复杂类型:在 UPROPERTY 里用到的类型必须能被 UHT 识别(基本类型、USTRUCT、UObject*、TArray 等),不能是任意 C++ 类型。

常见 UHT 错误与含义

错误信息(示例)常见原因
Unrecognized type ‘XXX’XXX 未用 USTRUCT/UCLASS/UENUM 声明,或头文件未包含
Expected a type specifierUPROPERTY 等宏后面的类型无法解析(拼写错误、前置声明缺失)
Missing ‘xxx.generated.h’ include该头文件有 UCLASS 等但未 include 对应 .generated.h
Redefinition of ‘GENERATED_BODY’同一个类里写了多个 body 宏,或 .generated.h 被重复包含方式不对
The type ‘YYY’ must be a USTRUCT or UCLASS在 UPROPERTY 里用了 YYY 类型,但 YYY 不是引擎可识别的类型

遇到 UHT 报错时,先看它指向的头文件和行号,再对照上述规则检查:类型是否暴露给 UHT、include 顺序是否正确、是否用了不支持的语法。

UHT 的演进:从 C++ 到 C#

早期 UHT 是用 C++ 实现的,和引擎一起编译。UE5 中 UHT 逐步迁移到 C#,和 UBT 一样作为独立工具运行。这样做的目的是:

  • 不依赖引擎的编译结果,启动更快;
  • 与 UBT 共享 C# 代码(解析、文件 IO、诊断信息);
  • 更容易扩展和修复 UHT 的解析逻辑。

对你使用 UCLASS/UPROPERTY 的方式影响不大,只是错误信息可能更清晰、未来新特性会先在 C# 版 UHT 里出现。


15.7 完整旅程回顾

把第 4 章的"从一个 UPROPERTY 出发的追踪"和本章串起来:

  1. 你写下UPROPERTY(EditAnywhere) float MaxHealth;
  2. UHT 扫描到这一行,提取属性名、类型、说明符。
  3. UHT 生成.gen.cpp 里的一段 FFloatPropertyParams 和注册调用,把 MaxHealth 的偏移、CPF_Edit 等写入 UClass。
  4. 预处理器把你的 .h 和 .generated.h 合并,GENERATED_BODY() 展开成引擎需要的声明。
  5. 编译器编译你的 .cpp 和 .gen.cpp,得到 .obj。
  6. 链接器产出模块 dll。
  7. 运行时引擎启动时执行 .gen.cpp 里的静态初始化,AMyCharacter 的 UClass 被注册,MaxHealth 的 FProperty 挂在 UClass 上。
  8. 编辑器/蓝图/序列化/网络通过 UClass 和 FProperty 发现并操作 MaxHealth。

这就是"一个 UCLASS 从标记到编译完成"的完整链条。UHT 是这条链的起点——没有它,就没有 .generated.h 和 .gen.cpp,也就没有反射,没有 GC 对 UPROPERTY 的识别,没有蓝图和编辑器集成。


一句话总结

UHT 在 C++ 编译之前扫描含 .generated.h 的头文件,识别 UCLASS/UPROPERTY/UFUNCTION 等宏,生成 .generated.h(供 GENERATED_BODY 展开)和 .gen.cpp(反射注册代码);两阶段编译是 Unreal 反射和工具链的根基。


实验

  1. 在项目中找一个简单的 UCLASS(只有少量 UPROPERTY/UFUNCTION),打开其对应的Intermediate/Build下生成的 .generated.h 和 .gen.cpp(具体路径因版本和平台而异,可在编译后搜索文件名)。
  2. 在 .generated.h 里搜索GENERATED_BODY或你的类名,看宏展开后的声明长什么样。
  3. 在 .gen.cpp 里搜索你的属性名或Z_Construct_UClass_,看属性注册和 UClass 构造的代码。
  4. 故意在 UCLASS() 里写一个不存在的说明符(如UCLASS(NotARealSpecifier)),保存并编译,阅读 UHT 的报错信息——它会指出无法识别的说明符。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/7 1:39:24

紫光展锐UWE5622 WiFi模块移植实战:关键步骤与调试技巧

1. UWE5622 WiFi模块移植前的准备工作 在开始移植紫光展锐UWE5622 WiFi模块之前&#xff0c;我们需要做好充分的准备工作。这个阶段就像装修房子前要准备好图纸和材料一样重要。首先&#xff0c;我们需要确认目标平台的硬件配置是否满足UWE5622的要求&#xff0c;特别是SDIO接口…

作者头像 李华
网站建设 2026/8/7 1:40:04

ChatTTS部署进阶教程:Docker镜像自定义与API封装

ChatTTS部署进阶教程&#xff1a;Docker镜像自定义与API封装 "从一键部署到深度定制&#xff0c;打造专属语音合成服务" ChatTTS作为目前开源界最逼真的语音合成模型&#xff0c;其自然的中文对话表现和拟真的语音效果令人印象深刻。但官方提供的WebUI版本虽然易用&a…

作者头像 李华
网站建设 2026/7/14 15:18:16

多线程优化:DamoFD-0.5G高并发推理的性能调优实践

多线程优化&#xff1a;DamoFD-0.5G高并发推理的性能调优实践 1. 引言 在实际的人脸检测应用场景中&#xff0c;我们经常需要同时处理大量的图片请求。比如一个智能相册应用&#xff0c;用户上传几百张照片后&#xff0c;系统需要在短时间内完成所有人脸的检测和关键点定位。…

作者头像 李华