UEC++ 与标准 C++ 的区别与联系

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 的值,目前有三个标准可选:Cpp14Cpp17Latast,它控制了传递给编译器的 /std:c++* 值。

开始具体的分析之前,首先要知道标准 C++ 和 UE C++ 都是怎么执行编译的,因为只有先区分了最根本的使用区别,才能够进一步分析为什么会有这些区别。

编译标准 C++ 代码

首先,C++ 的代码只依赖于编译器,如下面代码:

1
2
3
4
5
6
7
8
// hw.cpp
#include <iostream>

#define HW_MSG "HelloWorld"
int main()
{
std::cout<<HW_MSG<<std::endl;
}

想要编译上面的代码,需要使用一个编译器(GCC/MSVC)等,VS 使用的是 MSVC,以 GCC 为例:

1
$ g++ hw.cpp -o hw.exe

通过这一行命令就会编译出 hw.exe,但是编译器是一套工具链,虽然只执行了一条命令,但是它其实是调用了一串的工具进行预处理、语法分析、编译、链接等等一系列操作,不过它们不是本篇文章的重点,有兴趣的可以看一下我之前的这篇文章:C/C++ 编译和链接模型分析
举上面的例子需要关注的有两点:

  1. 标准 C++ 语法可以直接用编译器编译;
  2. 编译器对代码需要执行预处理、语法分析、编译、链接等操作;

编译 UEC++ 代码

首先,UEC++ 代码,并不像标准 C++ 那样把代码单独存在一个文件就可以编译的,UE 自己搭建了一套编译体系,所有基于 UE 引擎的代码必须要通过 UE 的这套编译体系才可以编译。
我之前写过 UE 构建系统的一些文章,想要 UE 项目的构建系统可以看一下:

UEC++ 的代码必须要依赖于一个UE 项目,其基本项目结构为:

1
2
3
4
5
6
7
8
9
Example\GWorld\Source>tree /a /f
| GWorld.Target.cs
|
\---GWorld
GWorld.Build.cs
GWorld.cpp
GWorld.h
Public/
Private/

其中各个文件的职责为:

  • *.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 工作流程中的一环

上面写的需要关注的有三点:

  1. UEC++ 的代码必须通过 UE 自己的构建体系
  2. UEC++ 的代码必须要通过 UHT 进行翻译成 真正的 C++ 代码
  3. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Runtime\CoreUObject\Public\ObjectMacros.h
// ...
// These macros wrap metadata parsed by the Unreal Header Tool, and are otherwise
// ignored when code containing them is compiled by the C++ compiler
#define UPROPERTY(...)
#define UFUNCTION(...)
#define USTRUCT(...)
#define UMETA(...)
#define UPARAM(...)
#define UENUM(...)
#define UDELEGATE(...)

// This pair of macros is used to help implement GENERATED_BODY() and GENERATED_USTRUCT_BODY()
#define BODY_MACRO_COMBINE_INNER(A,B,C,D) A##B##C##D
#define BODY_MACRO_COMBINE(A,B,C,D) BODY_MACRO_COMBINE_INNER(A,B,C,D)

// Include a redundant semicolon at the end of the generated code block, so that intellisense parsers can start parsing
// a new declaration if the line number/generated code is out of date.
#define GENERATED_BODY_LEGACY(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_GENERATED_BODY_LEGACY);
#define GENERATED_BODY(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_GENERATED_BODY);

#define GENERATED_USTRUCT_BODY(...) GENERATED_BODY()
#define GENERATED_UCLASS_BODY(...) GENERATED_BODY_LEGACY()
#define GENERATED_UINTERFACE_BODY(...) GENERATED_BODY_LEGACY()
#define GENERATED_IINTERFACE_BODY(...) GENERATED_BODY_LEGACY()

#if UE_BUILD_DOCS || defined(__INTELLISENSE__)
#define UCLASS(...)
#else
#define UCLASS(...) BODY_MACRO_COMBINE(CURRENT_FILE_ID,_,__LINE__,_PROLOG)
#endif

#define UINTERFACE(...) UCLASS()
// ...

下面简单列举一些:

  1. *.generated.h这个文件是 UE 特有的,它是 UHT 生成的代码(大多都是宏)

  2. UCLASS/UFUNCTION/UPROPERTY/UENUM 标记 在 C++ 中是没有的,它们的作用是指导 UHT 生成什么样的辅助代码;

  3. GENERATED_BODY等系列宏标准 C++ 是没有的,它的作用在于把 UHT 生成的代码包含到当前类中来;

  4. BlueprintNativeEvent函数的实现需要加 _Implementation,这个规则是没有的,上面提到了 C++ 中连UFUNCTION 都没有;

  5. C++ 中没有 Slate

  6. UE 项目中的宏都会生成在 Intermediate\Build\Win64\UE4Editor\Development\MODULE_NAME 下,如 MODULE_NAME_API 是导出符号,在标准 C++ 项目中需要你自己定义导出。

  7. 标准 C++ 的接口可以通过抽象类的来实现,并不需要一个特定基类,而且并没有 UE 中不可以提供数据成员的限制(仅从语法的角度,当然从设计思路上接口要无状态)

  8. 标准 C++ 没有 UE 中的 DELEGATE

  9. Cast<>NewObject<> 是 UE 特有的,C++ 使用四种标准 cast 和new

  10. C++ 也没有 UE 的 Thunk 函数

了解 UEC++ 和标准 C++ 的区别的关键点在于能够了解两者在构建流程上的区别,这一点能够区分开之后,再多的语法区别都是在这个结构内。至于 UEC++ 的反射,这是另一个可以写很大篇幅的内容了,先挖个坑。

如何学习 C++

从最开始接触 C 语言到现在快有十年了,也看了不少 C++ 的书,但是我觉得学习 C++ 最重要的是要去了解一下那些特性为什么这么设计,受哪些历史特性的限制,了解特性之间的关联,再去看看这些特性的编译器的实现,很多犄角旮旯的东西产生的原因就很明显了。
推荐一些书(建议按照顺序阅读,带 * 建议必读):

  1. The C Programing Language
  2. *C++ Primer / The C++ Programming Language
  3. *inside the c++ object model
  4. *Effective C++
  5. Modern Effective C++
  6. C++ coding standard: 101 rules, guidelines and best practices
  7. *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 生产的代码,不少的错误或者疑问的问题都能在里面找到答案。

最重要的还是:多看!多写!多思考!