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

新冠病毒(COVID-19)影响,整个春节都禁足在家,目前还没有复工,只能看看书整理一下笔记,希望疫情尽快过去,希望朋友们都身体健康。
本篇文章的主要内容是分析一下在使用UE开发时使用的C++和标准C++在语法上有哪些区别,UEC++本质是C++的一个超集,它支持和使用C++的全部特性,但是它在标准特性之上自己构建了一套语法。很多开发中的编译问题只有知道了两者的边界,才能够快速和准确地定位问题出现在哪个阶段。对于使用UE之前就学习过C++的来说这不是什么问题,但是对于先接触UE然后慢慢学C++的同学来说,这是个挺大的问题。

标准C++是基于ISO/IEC 14882的语言规范(C++98/03/11/14/17等标准),UEC++则是我们开发当中使用的Epic在标准C++之上扩展的用法,这里不讨论GC、反射之类的基于C++之上自己构建的对象体系,也不涉及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函数

  11. ...

了解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生产的代码,不少的错误或者疑问的问题都能在里面找到答案。

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

全文完,若有不足之处请评论指正。

扫描二维码,分享此文章

本文标题:UEC++与标准C++的区别与联系
文章作者:查利鹏
发布时间:2020年02月11日 11时22分
本文字数:本文一共有2.5k字
原始链接:https://imzlp.me/posts/20425/
专栏链接:https://zhuanlan.zhihu.com/p/106326754
许可协议: CC BY-NC-SA 4.0
捐赠BTC:1CbUgUDkMdy6YRmjPJyq1hzfcpf2n36avm
转载请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!