UEC++ 本质是 C++ 的一个超集,它支持和使用 C++ 的全部特性,但是它在标准特性之上自己构建了一套语法。很多开发中的编译问题只有知道了两者的边界,才能够快速和准确地定位问题出现在哪个阶段。对于使用 UE 之前就学习过 C++ 的来说这不是什么问题,但是对于先接触 UE 然后慢慢学 C++ 的同学来说,这是个挺大的问题。
标准 C++是基于 ISO/IEC 14882 的语言规范(C++98/03/11/14/17 等标准),UEC++ 则是我们开发当中使用的 Epic 在标准 C++ 之上扩展的用法,这里不讨论 GC、反射之类的基于 C++ 之上自己构建的对象体系,也不涉及 UE 中的各种库,关注的着重点在于核心语法层面。
近期博客的更新文章里,写了一些 UE 反射机制的内容,可以作为本篇文章的进阶内容结合来看:
在 UE4.23+ 以后,UE 所支持的 C++ 标准是可以被控制的,在 *.target.cs
和*.build.cs
中均可以设置 CppStandard
的值,目前有三个标准可选:Cpp14
、Cpp17
、Latast
,它控制了传递给编译器的 /std:c++*
值。
开始具体的分析之前,首先要知道标准 C++ 和 UE C++ 都是怎么执行编译的,因为只有先区分了最根本的使用区别,才能够进一步分析为什么会有这些区别。
编译标准 C++ 代码
首先,C++ 的代码只依赖于编译器,如下面代码:
1 | // hw.cpp |
想要编译上面的代码,需要使用一个编译器(GCC/MSVC)等,VS 使用的是 MSVC,以 GCC 为例:
1 | $ g++ hw.cpp -o hw.exe |
通过这一行命令就会编译出 hw.exe,但是编译器是一套工具链,虽然只执行了一条命令,但是它其实是调用了一串的工具进行预处理、语法分析、编译、链接等等一系列操作,不过它们不是本篇文章的重点,有兴趣的可以看一下我之前的这篇文章:C/C++ 编译和链接模型分析 。
举上面的例子需要关注的有两点:
- 标准 C++ 语法可以直接用编译器编译;
- 编译器对代码需要执行预处理、语法分析、编译、链接等操作;
编译 UEC++ 代码
首先,UEC++ 代码,并不像标准 C++ 那样把代码单独存在一个文件就可以编译的,UE 自己搭建了一套编译体系,所有基于 UE 引擎的代码必须要通过 UE 的这套编译体系才可以编译。
我之前写过 UE 构建系统的一些文章,想要 UE 项目的构建系统可以看一下:
UEC++ 的代码必须要依赖于一个UE 项目,其基本项目结构为:
1 | Example\GWorld\Source>tree /a /f |
其中各个文件的职责为:
*.Target.cs
是用来控制的是生成的可执行程序的外部编译环境,就是所谓的 Target。比如,生成的是什么Type
(Game/Client/Server/Editor/Program),开不开启 RTTI(bForceEnableRTTI
),CRT 使用什么方式链接(bUseStaticCRT
) 等等。*.Build.cs
控制的是 Module 编译过程,由它来控制所属 Module 的对其他 Module 的依赖、文件包含、链接、宏定义等等相关的操作,*.Build.cs
告诉 UE 的构建系统,它是一个 Module,并且编译的时候要做哪些事情。- 其余的是代码源文件(一般情况下头文件放在
Public/
,实现放在Private/
)
UE 构建系统的重点是 UBT 和 UHT,他们各自的作用我之前的文章中有提到:
- UBT 的作用是收集和构建编译环境,调用 UHT 生成代码,然后 调用真正的编译器进行编译
- UHT 的作用是把项目中所有代码里的 UHT 标记翻译为真正的 C++ 代码(如
UCLASS/GENERATED_BODY
等等),它属于 UBT 工作流程中的一环
上面写的需要关注的有三点:
- UEC++ 的代码必须通过 UE 自己的构建体系
- UEC++ 的代码必须要通过 UHT 进行翻译成 真正的 C++ 代码
- UE 的项目里真正进入编译阶段的,全部都是标准 C++ 的代码
所以,UEC++ 和标准 C++ 的区别在于,UEC++ 自己定义了一些语法,需要通过专门的解释器进行翻译,然后再通过 C++ 编译器进行编译(进入标准 C++ 的编译流程)。
关于流程上的区别就说这么多,下面的内容来写 UE 具体有哪些特殊的语法。
UEC++ 的特殊语法
UEC++ 的特殊语法主要是用于指导 UHT 来产生辅助代码的方式。
来聊 UEC++ 的特殊语法之前,需要先明确一点:UCLASS
/UFUNCTION
/UPROPERTY
等都不是 真正有意义的 C++ 宏 ,他们是UHT 的标记,在经过 UHT 生成代码之后他们就是空宏了,没有意义。
UE 的代码是把UHT 标记 和真正的宏 都以 宏的形式 来表现,从结果来说,它们都是生成了一些代码,但是它们的处理流程不同。
UHT 标记是先通过 UHT 进行扫描并生成代码,再通过编译器进行预处理等等,这里存在一个先后的过程,其限制就为:对 UHT 对代码的处理在前,编译器对宏的预处理在后,所以在 UE 中没办法用宏来包裹 UHT 标记。
UE 的 UHT 标记包括但不限于:
可以从 Runtime\CoreUObject\Public\ObjectMacros.h
看更多的 UHT 标记,有一个简单的分辨方法:如果这个宏是一个空宏,那么它就是一个 UHT 标记:
1 | // Runtime\CoreUObject\Public\ObjectMacros.h |
下面简单列举一些:
*.generated.h
这个文件是 UE 特有的,它是 UHT 生成的代码(大多都是宏)UCLASS
/UFUNCTION
/UPROPERTY
/UENUM
等 标记 在 C++ 中是没有的,它们的作用是指导 UHT 生成什么样的辅助代码;GENERATED_BODY
等系列宏标准 C++ 是没有的,它的作用在于把 UHT 生成的代码包含到当前类中来;BlueprintNativeEvent
函数的实现需要加_Implementation
,这个规则是没有的,上面提到了 C++ 中连UFUNCTION
都没有;C++ 中没有 Slate
UE 项目中的宏都会生成在
Intermediate\Build\Win64\UE4Editor\Development\MODULE_NAME
下,如MODULE_NAME_API
是导出符号,在标准 C++ 项目中需要你自己定义导出。标准 C++ 的接口可以通过抽象类的来实现,并不需要一个特定基类,而且并没有 UE 中不可以提供数据成员的限制(仅从语法的角度,当然从设计思路上接口要无状态)
标准 C++ 没有 UE 中的 DELEGATE
Cast<>
和NewObject<>
是 UE 特有的,C++ 使用四种标准 cast 和new
C++ 也没有 UE 的 Thunk 函数
…
了解 UEC++ 和标准 C++ 的区别的关键点在于能够了解两者在构建流程上的区别,这一点能够区分开之后,再多的语法区别都是在这个结构内。至于 UEC++ 的反射,这是另一个可以写很大篇幅的内容了,先挖个坑。
如何学习 C++
从最开始接触 C 语言到现在快有十年了,也看了不少 C++ 的书,但是我觉得学习 C++ 最重要的是要去了解一下那些特性为什么这么设计,受哪些历史特性的限制,了解特性之间的关联,再去看看这些特性的编译器的实现,很多犄角旮旯的东西产生的原因就很明显了。
推荐一些书(建议按照顺序阅读,带 * 建议必读):
- The C Programing Language
- *C++ Primer / The C++ Programming Language
- *inside the c++ object model
- *Effective C++
- Modern Effective C++
- C++ coding standard: 101 rules, guidelines and best practices
- *The design and evolution of c++
前两本是基础语法,如果没有太多时间,可以读 C++ Primer 或者 TC++PL 中的其中一本即可,我还写过一篇文章对比这两本书 读 TC++PL、C++Primer 和 ISO C++,有兴趣的可以读一下,学习 C++ 后面的路还很长。
我建议学完基础语法之后就开始大量地写代码,因为只看懂了理论,不上手多写是没有意义的。
如何学习 UEC++
在 C++ 的基础语法学的差不多了之后,直接就开始在 UE 中写项目吧,作为 开放源代码 的引擎(但 UE 不是开源软件,许可证区别),没有什么是藏着掖着的,可以在边写项目的同时边尝试看 UE 引擎里的代码,去尝试分析 UE 的构建流程和代码生成。
我的一个小技巧就是,可以多去翻一下 Intermediate
中通过 UHT 生产的代码,不少的错误或者疑问的问题都能在里面找到答案。
最重要的还是:多看!多写!多思考!