受新冠病毒(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中的各种库,关注的着重点在于核心语法层面。
近期博客的更新文章里,写了一些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生产的代码,不少的错误或者疑问的问题都能在里面找到答案。
最重要的还是:多看!多写!多思考!