叠甲:仅为记录个人学习过程;内容很多是从别处看来的,在参考中列出;缺乏实践经验,有些地方纸上谈兵;难免有错误,敬请指出。

系列目录:【我看UE源码】(0)系列开篇 & 目录

UE4 dynamic_cast一则

GEngine是一个UEngine类型的指针,它是所有引擎类的抽象基类,用于管理一些对编辑器或游戏必要的系统:

/**
* Global engine pointer. Can be 0 so don't use without checking.
*/
ENGINE_API UEngine*	GEngine = NULL;

它有一个派生类UGameEngine,用来管理支持游戏的核心系统,包含一些指针比如GameViewportWindow等:

/**
 * Engine that manages core systems that enable a game.
 */
UCLASS(config=Engine, transient)
class ENGINE_API UGameEngine
	: public UEngine
{
    // ...
public:      
	/** The game viewport window */
	TWeakPtr<class SWindow> GameViewportWindow;
	/** The primary scene viewport */
	TSharedPtr<class FSceneViewport> SceneViewport;
	/** The game viewport widget */
	TSharedPtr<class SViewport> GameViewportWidget;
};

在使用的时候,新建一个子类指针、把父类指针GEngine转成子类UGameEngine类型的指针,这种情况下为什么能访问到子类单独定义的数据呢?

UGameEngine* GameEngine = Cast<UGameEngine>(GEngine);
OutSceneViewport = GameEngine->SceneViewport;
OutInputWindow = GameEngine->GameViewportWindow;

这其实就是dynamic_cast,把基类指针或引用转换成派生类指针或引用,目的是使用派生类的非虚函数、访问派生类的成员变量。

C++ 11有四种类型转换,在此结合使用场景简单总结一下。

①const_cast

修改属性和编译器优化

const_cast用于修改const或volatile属性。一般是用来去掉const属性:

const int a = 1;

// int* pa = &a;	// 错误,const int*类型的值不能用于初始化int*类型的实体
int* pa = const_cast<int*>(&a);	// 正确,去掉了const常量a的const属性,可以赋值给pa

// int& ra = a;		// 错误
int& ra = const_cast<int&>(a);	// 正确

通过以下代码,尝试给指向“解除const属性的值”的指针和引用赋值:

const int a = 1;

int* pa = const_cast<int*>(&a);
*pa = 10;
std::cout << "a:" << &a << ' ' << a << std::endl;
std::cout << "pa:" << pa << ' ' << *pa << std::endl;

std::cout << std::endl;

int& ra = const_cast<int&>(a);
ra = 5;
std::cout << "a:" << &a << ' ' << a << std::endl;
std::cout << "pa:" << pa << ' ' << *pa << std::endl;
std::cout << "ra:" << &ra << ' ' << ra << std::endl;

首先,const_cast说是“去掉const属性”,作用的对象不是a,而是指向a的指针或引用。不会修改a本身的const属性,而是把指向常量的指针和引用去掉了const属性、提供了修改a的一个新接口

然后,输出结果有些奇怪:

  • 第一,指针、引用转换const属性前后,指向的内存是一样的。把常量指针/引用转换成非常量的指针/引用,仍然指向原来的对象,理解为只是看待该内存的方式变了
  • 第二,通过指针和引用修改原值,发现原值依然没变(好像是保留了const属性),但去掉const属性的指针和引用之间就会相互影响

既然地址相同,值也应该是相同的,可能是编译器优化的原因。这里的优化可能是通过符号表,详情见下面专门讲const内存的小节。

加上volatile,告诉编译器a的值随时可能发生变化(虽然它是const),不让编译器优化,每次都从内存中取值,就发现相同的地址的值都一样了:

volatile const int a = 1;
int* pa = const_cast<int*>(&a);

*pa = 10;
std::cout << "a:" << &a << ' ' << a << std::endl;
std::cout << "pa:" << pa << ' ' << *pa << std::endl;

std::cout << std::endl;


int& ra = const_cast<int&>(a);
ra = 5;
std::cout << "a:" << &a << ' ' << a << std::endl;
std::cout << "pa:" << pa << ' ' << *pa << std::endl;
std::cout << "ra:" << &ra << ' ' << ra << std::endl;
const和volatile

上面的例子,既然已经给变量加上了const属性,还能给它加上volatile属性吗?有意义吗?

首先,它们各自的含义并不是绝对的:

  • const含义是“请作为常量使用”,对于局部const变量,可以理解为加了一个注释,表示一般不希望在程序中修改它的值。并非“这肯定是一个常量”,也并不存在常量区
  • volatile的含义是“编译器不要自以为是地优化,这个值随时可能改变”。并非“你可以修改这个值”

然后,它们的作用和起作用的阶段有所不同:

  • const只在编译期有用,在运行期没用。在编译时,const保证其修饰的变量在范围内没有被直接从值上修改;在运行期,变量的值被改变就不受const限制了
  • volatile在编译器和运行期都有用。在编译期,告诉编译器不要优化这个变量;在运行期,每次用到这个变量,都从内存中取该变量的值

因此,const和volatile同时修饰一个变量:

  • 可以,volatile并不是non-const的含义,它们之间不冲突
  • 有意义,表示变量在程序编译期不能被修改、不能被优化,运行期可能修改,每次用到时都去内存读数据,避免出错

编译器一般不为const变量分配内存,而是将它保存在符号表中,这使得它成为一个编译期间的值,没有了存储和读内存的操作。

volatile的作用是告诉编译器,i是随时可能发生变化的,每次使用它的时候必须从内存中取出i的值。

——《C语言深度剖析》

打印volatile变量的地址

上图中,为啥打印volatile变量的地址,结果是1?可以看看这个帖子(最高赞好像还答错了)。简单来说,ostream重载了const void*<<运算符,对于一般的指针T*会隐式转换成const void*来输出。但volatile变量的指针不会隐式转换成指针,而被认作了bool变量输出1。

那么为了用<<输出volatile变量的地址,就需要显式转换一下。使用static_cast是不行的,会报错:static_cast无法丢掉常量或其他类型限定符。

因此又回过头来,使用本小节讲述的对象:const_cast,去掉变量的volatile属性,转换成普通指针T*、再被隐式转换成const void*来输出地址。

volatile int b = 2;
std::cout << const_cast<int*>(&b) << std::endl;

std::cout << std::endl;

volatile const int a = 1;
int* pa = const_cast<int*>(&a);
*pa = 10;
std::cout << "a:" << const_cast<int*>(&a) << ' ' << a << std::endl;
std::cout << "pa:" << pa << ' ' << *pa << std::endl;

std::cout << std::endl;

int& ra = const_cast<int&>(a);
ra = 5;
std::cout << "a:" << const_cast<int*>(&a) << ' ' << a << std::endl;
std::cout << "pa:" << pa << ' ' << *pa << std::endl;
std::cout << "ra:" << &ra << ' ' << ra << std::endl;

或者,<<运算符用不了可以用printf

volatile const int a = 1;

std::cout << &a << std::endl;		// 输出1
printf("%p\n", &a);		// 输出地址
std::cout << const_cast<int*>(&b) << std::endl;		// 输出地址

顺带一提,普通类型转换成const类型具体是什么规则我也不是很懂。在UE4里,我新建了一个TMap<const SWidget*, int32>,那就必须给它加入const类型的SWidget*,否则就会有模板报错。当然也必须就是这种原生指针,其他封装后的智能指针也不行。

TMap<const SWidget*, int32> WidgetIndexMap;

// ...
void function(const SWidget* Widget, ...)
{
    int32 Index = XXXLists.Add(NewList);
	WidgetIndexMap.Emplace(Widget, Index);
}

// ...
const SWidget* WidgetPtr = ...;
if(WidgetIndexMap.Contains(WidgetPtr))
{
	int32 RnederBatchIdnex = WidgetIndexMap[WidgetPtr];
}
补充:const内存和其他

对于上述通过指针、引用(也就是地址)来修改const变量的值,只对局部变量有效。

const int ga = 1;

int main()
{
    int* pga = const_cast<int*>(&ga);
	*pga = 10;		// 运行时报错
	std::cout << "a:" << const_cast<int*>(&a) << ' ' << a << std::endl;
    
	int& rga = const_cast<int&>(ga);
	rga = 20;		// 运行时报错
	std::cout << "a:" << const_cast<int*>(&a) << ' ' << a << std::endl;
}

如果进行上面代码中的操作,试图修改全局const变量的值,编译器不报错,但运行时在指针、引用赋值的地方报错:“引发了异常:写入访问权限冲突”。

这是由C++ const的内存分配方式导致的。关于内存、const的更多信息、const引用传参,见【我看UE源码】(2)源码阅读支线之如何理解C++的const关键字?

const_cast的使用

被const_cast转换的类型必须是指针、引用或指向对象类型成员的指针。也就是,不能把基本类型变量和对象转换成非常量

const Student stu1;
Student stu2 = const_cast<Student>(stu1);	// 错误

const int x = 10;
int y = const_cast<int>(i);	// 错误

使用const_cast去掉const属性,并不是真的想改变原类型的const属性,只是又提供了一个接口(指针或引用)。因为前文(补充的文章)提到,对全局const,使用地址也无法修改其值;对于局部const,可以看作是一个注释,可以用接口修改其值。

②reinterpret_cast

灵活但不安全的转换

从单词意思上看,它的作用是把相同内存中的数据“重新解释”,概念上像union。比如进行不同类型的指针之间、不同类型的引用之间、指针和能容纳指针的整数类型之间的转换。转换时执行逐个bit复制的操作。

这种转换类型提供了很强的灵活性,但安全性需要程序员来保证。

下面的案例复述自这篇文章

int num = 0x00636261;
int* pnum = &num;
char* pstr = reinterpret_cast<char*>(pnum);	// 把int*重新看成char*
std::cout << "pnum: " << pnum << ", " << std::hex << *pnum << std::endl;
std::cout << "pstr: " << static_cast<void*>(pstr) << ", " << pstr << std::endl;	// 对于char*指针,直接输出会输出字符串,要转成void*输出它指向的地址

在上面的程序片段中,首先定义int变量num,用十六进制值0x00636261初始化;然后定义int* pnum指向num;然后使用reinterpret_cast,把int*类型的指针pnum转换成了char*类型的指针pstr。

对比输出两个指针,发现它们的地址是相同的,也就是reinterpret_cast不改变操作对象的值,而是改变了看待同一块内存区域的方式。

一个指向字符串的指针是如何地与一个指向整数的指针或一个指向其他自定义类型对象的指针有所不同呢?从内存需求的观点来说,没有什么不同!它们三个都需要足够的内存(并且是相同大小的内存)来放置一个机器地址。指向不同类型之各指针间的差异,既不在其指针表示法不同,也不在其内容(代表一个地址)不同,而是在其所寻址出来的对象类型不同。也就是说,指针类型会教导编译器如何解释某个特定地址中的内存内容及其大小。

——《深度探索C++对象模型》

如图:

使用reinterpret_cast把pnum从int*转变成char*类型的指针,并初始化pstr后,pstr也指向num的内存区域,并且按照指针的类型(char*)对应的规则来解读这个内存区域。一个char占用一个Byte,对pstr解引用,得到的将是一个字符,也就是a(0x61是a的ASCII码)。

为什么输出了三个字符?这是由于在输出char*指针时,ostream会把它当做输出一个字符串来处理,直至遇到'\0'才表示字符串结束。如果将num的值改为0x63006261,输出的字符串就变为”ab”。

再改为0x64636261后,作者输出字符串除了abcd后面还有6个字符,但我试着就只输出abcd了。

补充:endian

上面的例子里,为什么输出16进制数字是0x00636261,输出字符串却成了abc(61 62 63)?

这是因为在reinterpret_cast中,存在字节序(endian)问题。查看这个问题下milo的回答

整数类型static_cast,指针++/–都不涉及casting和字节序问题。把指针类型reinterpret_cast,或者使用union转换了类型,就有字节序问题。

int a = 0x12345678;
char* c = reinterpret_cast<char*>(&a);
printf("%x %x %x %x\n", c[0], c[1], c[2], c[3]);

union
{
	int a;
	char c[4];
}u;
u.a = 0x12345678;
printf("%x %x %x %x\n", u.c[0], u.c[1], u.c[2], u.c[3]);

从这个答案和我的输出可以确定我的电脑是little-endian。接下来说一下什么是endian:

  • 对于多字节的数据,计算机一定从低地址向高地址存储
  • big-endian的计算机,把数据的高字节(比如数的高位)存到低地址、低字节存到高地址,也就是按人类读数的顺序来存。对于int32 的 0x12345678,从低地址到高地址的四个字节分别存 0x12、0x34、0x56、0x78
  • little-endian的计算机,数据的低字节存入低地址、高字节存入高地址。同一个例子,字节存放顺序从低地址到高地址为:0x78、0x56、0x34、0x12

人们阅读内存的顺序总是大端序的。而且小端序的每个字节内部还是保留了大端序的形式,而不是全部反过来,这让小端序在人类的脑子里更容易混淆。

那么为什么还被使用呢?因为至少在1970s,当CPU只有几千个逻辑门的时候,小端序更加有效率。因此,很多内部处理都是小端序的,并且也渗透到一些外部格式中。

另一方面,大端序对人类更容易理解,因此对于格式规范,如网络协议、文件格式,大部分都是大端序的。

reinterpret_cast的使用

这个“重新解释”可以实现两个作用:

  • 指针和整数之间的转换
  • 不同类型的指针/成员指针/引用之间的转换

对于第一个作用,可以用来在指针中存储额外信息。例如在特定平台上,如果指针指向的类型是4字节对齐的,那么指针转成的整数的最低两bit位一定都是0(因为都指向0x00,0x04,0x08,0x0c这样的地址),就可以用这两位存储其他数据。

第二个作用,可以用来通过成员访问到完整的结构体对象,或者从完整的结构体对象访问间接成员。

除此之外,还可以利用以上功能,把类型不符合的数据硬塞到容器中。虽然需要使用者额外注意才能保证类型安全,但有时候能降低数据结构的复杂程度。

要是只从使用的角度上来说,C++的这些转换是为了代替C语言用括号执行的强制转换。如果换成const_cast或者static_cast合法,就具有相似的转换行为;如果不合法,就可以要考虑使用reinterpret_cast

相当于告诉编译器:我用其他的转换你会报错,那么我就用interpret_cast,对于类型我心里有数,你不要报错。

总之,reinterpret_cast较危险,使用较少。

③static_cast

题外话:

虽然const_cast是用来去除const指针或引用的const属性的,但static_cast却不是用来去掉变量static属性的。因为static实际上决定的是一个变量的作用域和生命周期,改变static属性会造成范围性的影响,而const只是限定一个变量自己。

但无论是哪一个属性,都是在变量一出生(完成编译的时候)就决定了的变量的特性,所以实际上都是不容许改变的。这点在前文已经看到了,const_cast也就是给const常量的指针或引用去除const属性,给原常量提供一个新的接口,并没有真正修改对象本身的const属性(还是不能直接给它赋值)。

回到static_cast,一般用它来代替C语言的类型转换。它跟reinterpret_cast一样,都不能去除指针或引用的const限定,跟const_cast的分工有所区别。其他一些区分:

  • static_cast不仅可以用在指针和引用上,还可以用在基础数据类型和对象上
  • reinterpret_cast可以用于“完全没有关系”的类型之间,static_cast需要转换的双方“有一点关系”
  • static_cast会进行编译期类型检查,但不会进行运行时类型检查
使用场景

static_cast的作用:

  • 基本数据类型之间的转换
  • 类层级结构中,基类和派生类之间的指针或引用的转换
    • 进行上行转换(把派生类的指针或引用转换成基类)是安全的
    • 进行下行转换(基类指针或引用转换成派生类),由于没有运行时类型检查,是不安全的,因为派生类中可能定义了基类中没有的成员变量
  • 把空指针转换成目标类型的空指针
  • 把任何类型的表达式转换成void类型

static_cast 不能用于在不同类型的指针之间互相转换,也不能用于整型和指针之间的互相转换,当然也不能用于不同类型的引用之间的转换。因为这些属于风险比较高的转换。

在指针和引用方面,似乎也只有继承关系是可以被static_cast接受的,其他情况的指针和引用转换都会被static_cast直接扔出编译错误,而这层关系上的转换又几乎都可以被dynamic_cast所代替。这样看起来static_cast的作用就比较小了。

static_cast真正用处并不在指针和引用上,而在基础类型和对象的转换上。而基于基础类型和对象的转换都是其他三个转换运算符所办不到的。

以下是一些例子。

基本数据类型:

char a = 'a';
int b = static_cast<int>(a);	// 正确

double* c = new double;
void* d = static_cast<void*>(c);	// 正确

int e = 10;
const int f = static_cast<const int>(e);	// 正确,给变量加上const属性

const int g = 20;
int* h = static_cast<int*>(&g);		// 编译错误,static_cast不能去掉常量对象的引用或指针的const属性

类层级结构:

class Parents
{
public:
    virtual ~Parents(){}
    // ...
};

class Children : public Parents
{
    // ...
};

int main()
{
    Child* daughter = new Children();
    Parents* mother = static_cast<Parents*> (daughter);	// right, cast with polymorphism
    
    Parents* father = new Parents();
    Children* son = static_cast<Children*> (father);	// no error, but not safe	
}

从基类到子类的转换,使用static_cast是不安全的,在后面的dynamic_cast里讲。

指针和引用:

class A
{
public:
	operator int() { return 1; }
	operator char* () { return nullptr; }
};

int main()
{
	A a;
	int n;
	char* p = const_cast<char*>("abcd");
	
	n = static_cast<int>(3.14159);	// n变为3
    
	n = static_cast<int>(a);	// 调用a.operator int(),返回1
	p = static_cast<char*>(a);	// 调用a.operator char*(),返回空指针
    
    n = static_cast<int>(p);	// 编译错误,static_cast不能进行整形和指针之间的转换
	p = static_cast<char*>(n);	// 编译错误,static_cast不能进行整形和指针之间的转换
    
	return 0;
}

④dynamic_cast

安全的下行转换

多态基类指含有虚函数的基类。用reinterpret_cast可以将多态基类指针强制转换为派生类的指针,但这种转换不检查安全性,也就是不检查转换后的指针是否确实指向一个派生类的对象。

dynamic_cast专门用来将多态基类的指针或引用转换成派生类的指针或引用,并且进行运行时类型检查(RTTI,Run-Time Type Identification)。对于不安全的指针转换返回nullptr。如果是引用,由于不存在空引用,直接抛出异常,名为std::bad_cast。

即,dynamic_cast“只能用于安全的类型转换”,而不是“保证变换的安全”,因此不能用来将非多态基类的指针或引用转换成派生类的指针或引用,因为这个转换不安全。只能用reinterpret_cast来完成。

dynamic_cast使用的前提是基类指针本身就指向一个派生类的对象。这一部分在下一节再提,先看一下如何判断转换成功和失败:

class Base
{
public:
	virtual ~Base() {};
};

class Derived :public Base {};

int main()
{
	Base b;
	Derived d;
	Derived* pd;
	pd = reinterpret_cast<Derived*>(&b);
	if (pd == nullptr) std::cout << "unsafe reinterpret_cast from base to derived" << std::endl;

	pd = dynamic_cast<Derived*>(&b);
	if (pd == nullptr) std::cout << "unsafe dynamic_cast from base to derived" << std::endl;

	pd = dynamic_cast<Derived*>(&d);
	if (pd == nullptr) std::cout << "unsafe dynamic_cast from derived to derived" << std::endl;

	Base* pb = dynamic_cast<Base*>(&d);
	if (pb == nullptr) std::cout << "unsafe dynamic_cast from derived to base" << std::endl;

	return 0;
}

输出:unsafe dynamic_cast from base to derived。在转换错误的例子中,是从一个多态基类(含有虚函数的基类)转换到子类,但b本身不是一个子类d的对象,因此也不能转换。

除了检查指针到指针的转换返回nullptr,可以用以下方式捕获引用转换的异常:

try{
    const Derived& d = dynamic_cast<Derived&>(b);
}catch(bad_cast){
    // ...
}

如果把指针转换到void*,RTTI决定表达式的实际类型。结果是指向被转换对象的指针。

void* pb = dynamic_cast<void*>(b);	// pb指向b类型的对象
void* pd = dynamic_cast<void*>(d);	// pd指向d类型的对象
dynamic_cast的使用
基类指针本来就指向派生类对象

接着上一小节,dynamic_cast使用的前提之一是基类指针本身就指向一个派生类的对象。dynamic_cast的目的是把基类指针转换程派生类指针,调用派生类中的非虚函数、使用派生类的成员变量。因为基类指针本来就指向派生类对象,可以直接调用派生类的虚函数(里氏替换),无需转换。

以下是一个完整的例子:

#include<iostream>

class Base
{
public:
	Base(int _x) :x(_x) {}
	int getx() { return x; }
	virtual ~Base() {}
private:
	int x;
};

class DerivedA : public Base
{
public:
	DerivedA(int _x, int _y) :Base(_x), y(_y) {}
	int gety() { return y; }
private:
	int y;
};

class DerivedB : public Base
{
public:
	DerivedB(int _x, int _z) :Base(_x), z(_z) {}
	int getz() { return z; }
private:
	int z;
};

enum TYPE
{
	BASE = 0, 
	DERIVEDA, 
	DERIVEDB
};

// 根据设定的type,创建相应的对象,统一返回基类指针
Base* CreateObject(int x, int y, TYPE type)
{
	if (type == BASE)
	{
		return new Base(x);
	}
	else if (type == DERIVEDA)
	{
		return new DerivedA(x, y);
	}
	else if (type == DERIVEDB)
	{
		int z = y;
		return new DerivedB(x, z);
	}
	return nullptr;
}

int main()
{
	Base* p = CreateObject(10, 20, DERIVEDA);	// 基类指针p,指向一个派生A类对象
	std::cout << "访问基类成员变量x:" << p->getx() << std::endl;

	// 希望使用派生A类的成员变量b,需要进行dynamic_cast
	DerivedA* pA = dynamic_cast<DerivedA*>(p);	// p本来就指向派生A类对象,因此可以安全转换
	if (pA)
	{
		std::cout << "访问派生A类成员变量y:" << pA->gety() << std::endl;
	}
	else
	{
		std::cout << "转换到派生A类失败" << std::endl;
	}

	DerivedB* pB = dynamic_cast<DerivedB*>(p);	// p本来不是指向派生B类,因此转换失败
	if (pB)
	{
		std::cout << "访问派生B类成员变量y:" << pB->getz() << std::endl;
	}
	else
	{
		std::cout << "转换到派生B类失败" << std::endl;
	}

	return 0;
}

创建一个派生A类的对象,用一个基类指针p指向它。dynamic_cast只能把p转换成它指向的派生A类的指针,不能转换成另一个派生B类对象的指针。

比较容易理解,因为不能从无到有、凭空访问子类特有的数据。

多态基类

使用dynamic_cast的另一个前提:基类是多态基类,即至少有一个虚函数。一般来说,日常使用的基类的析构函数都是虚函数,这样用基类指针调用时,可以从子类开始析构,并一直析构到基类。也就是这个前提一般是满足的。

使用总结

运行时多态也可以通过在基类里添加虚函数来实现,但在一些应用场景里,一些方法是派生类特有的,对基类无意义,如果添加为基类的虚函数就进行了污染,不符合设计思想。

而在合适的时候,通常应该优先使用虚函数。也有几个情况下dynamic_cast是更好的选择:

  • 不能改变基类来添加虚函数时(比如基类是标准库中的类)
  • 想访问派生类特有的成员函数时(目的是使用非虚函数)
  • 添加虚函数到基类中没有意义时(不合适的时候,比如基类里没有合适的返回值)
回到开头的UE4 Cast

讲完了四种类型转换,回过头来,看一下文章一开始提到的UE4中的一个转换场景:

首先,GEngine是一个UEngine类型的指针,UEngine类是所有引擎类的抽象基类,用于管理一些对编辑器或游戏必要的系统:

/**
* Global engine pointer. Can be 0 so don't use without checking.
*/
ENGINE_API UEngine*	GEngine = NULL;

然后,它有一个派生类UGameEngine,用来管理支持游戏的核心系统,包含一些指针比如GameViewportWindow等:

/**
 * Engine that manages core systems that enable a game.
 */
UCLASS(config=Engine, transient)
class ENGINE_API UGameEngine
	: public UEngine
{
    // ...
public:      
	/** The game viewport window */
	TWeakPtr<class SWindow> GameViewportWindow;
	/** The primary scene viewport */
	TSharedPtr<class FSceneViewport> SceneViewport;
	/** The game viewport widget */
	TSharedPtr<class SViewport> GameViewportWidget;
};

在使用的时候,是新建一个子类指针、把父类指针GEngine转成子类UGameEngine类型的指针,然后去访问子类单独定义的成员变量。这就是使用dynamic_cast的目的。

UGameEngine* GameEngine = Cast<UGameEngine>(GEngine);
OutSceneViewport = GameEngine->SceneViewport;
OutInputWindow = GameEngine->GameViewportWindow;

这个转换满足了dynamic_cast的两个前提,含有虚函数不必多说。另一个前提:基类指针本来就指向一个派生类的对象。我们查看GEngine的创建过程:

int32 FEngineLoop::Init()
{
	// Figure out which UEngine variant to use.
	UClass* EngineClass = nullptr;
	if( !GIsEditor )
	{
		SCOPED_BOOT_TIMING("Create GEngine");
		// We're the game.
		FString GameEngineClassName;
		GConfig->GetString(TEXT("/Script/Engine.Engine"), TEXT("GameEngine"), GameEngineClassName, GEngineIni);
		EngineClass = StaticLoadClass( UGameEngine::StaticClass(), nullptr, *GameEngineClassName);
		if (EngineClass == nullptr)
		{
			UE_LOG(LogInit, Fatal, TEXT("Failed to load UnrealEd Engine class '%s'."), *GameEngineClassName);
		}
		GEngine = NewObject<UEngine>(GetTransientPackage(), EngineClass);
	}
}

可以看到,GEngine在创建时,传入了一个EngineClassEngineClass使用UGameEngine::StaticClass()创建。可以理解为,最终创建的基类GEngine指针指向了派生类UGameEngine的对象。

也就是,可以用dynamic_cast进行安全的下行转换,使用派生类的成员变量。

这样,我们从UE4的一处Cast,讲解了C++的四种类型转换,并在最后发现,UE4的这一处类型转换实际上是安全的下行转换。

参考