2021-01-14 UE4大项目中的C++头文件管理

最近项目编译的时候遇到了Circular Dependencies的问题。

说到Circular Dependencies,简而言之就是头文件A include了头文件B,头文件B又引用了头文件A。

虽说我们平时都会稍微注意着点,不让这么明显的情况发生,但是如果文件多了,引用层次多了,还是可能会发生这样的问题。

有两种思路来尝试解决这个问题。

前置声明

我们知道,只要你没有用到struct或者class的具体内容,只是把它们的指针传来传去,那是可以用前置声明的。比如这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// File: HeaderA.h
USTRUCT()
struct StructA
{
GENERATED_BODY()
int MemberA;
}
// File: HeaderB.h
struct StructA;

UCLASS()
class ClassA
{
GENERATED_BODY()
StructA MemberA; // error: 需要知道struct的具体内容来决定需要分配的内存大小
StructA* MemberA; // 能编译通过,但是需要手动管理内存

UPROPERTY()
StructA* MemberA; // error: uproperty不支持struct指针
}

可以看到我上面列举了三种前置声明的写法。

  1. 第一种肯定不行,因为编译器需要知道Struct的大小,但是前置声明并没有具体的struct信息,所以不能提供大小信息,编译不会通过。
  2. 能够编译通过,但是这样子就是一个非UE4管理的指针,需要自己负责它的内存管理,就是需要自己new和delete
  3. 不能编译通过,因为uproperty不支持struct指针

所以如果采用前置声明方案,就只能够使用struct指针 + 手动管理内存了。没有了UE4的内存管理,写起代码来很是麻烦。

管理头文件

既然要尽量避免struct的前置声明,那么就必须把struct所在的头文件包含进来。

回过头来想一想,之前为什么会产生循环依赖呢?这八成是因为我们经常把struct和class写在同一个头文件中。struct我们经常只是使用「内联类型」(比如int,float,bool)来作为成员变量的类型;而对于class,有很大概率其成员变量的类型是另一个自定义类。

1
2
3
4
5
6
7
8
9
10
struct MyStruct
{
int32 AAA;
}

class MyClassA
{
// 使用另一个自定义类型来作为成员的类型
MyAnotherClass* Member;
}

如果在头文件里面包含了另一个定义了自定义类的头文件,那么就很有可能最后会组成一个很复杂的依赖链。

所以,如果实在有必要引入struct所在的头文件,那么我们要尽量避免这个头文件里面定义了另一个类。

具体地来说,写代码的时候可以有以下规则:

  1. struct放在专门的头文件中,并且这个头文件里面不可以有class的定义,避免在头文件中include该文件的时候会include到类的定义,从而又依赖了其他的复杂类型
  2. 同个功能模块的struct可以放在同一个头文件中定义,但是不同模块的strcut最好放在不同的头文件。这是为了避免有太多地方引用头文件(设想一下整个项目只有一个Header.h头文件,里面定义了所有的struct,被各个cpp文件包含),导致一改动头文件里面的一点内容,就会导致大量重新编译的问题。

That’s All.

最后祝各位:身体健康。

Buy Me A Coffee / 捐一杯咖啡的钱
分享这篇文章~
0%
//