TSubclassOf的作用与实现 | Blurred code

TSubclassOf的作用与实现

2022/07/21

LastMod:2022/07/21

Categories: UE

TSubclassOf的作用

UE官方文档里描述了一个通过TSubclassOf在蓝图里限制下拉框选取范围的例子,不过感觉也不是讲的很清楚。

UClass描述了一个UObject类的反射信息。 通过obj->GetClass()能够获取其UClass信息。

UE中的引用分为obj referenceclass reference

一个朴素的UClass*可以指代任意UObject,所以在蓝图中选择类型的时候不好选。

	UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="Class")
	UClass* SMBody_Class;

edit-79ab670b46b24305b43bc464dfbb283c-2022-07-17-16-58-15

TSubclassOf可以限定只引用某个类型及其Child类型的类型。 比如

UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="Class")
TSubclassOf<ACharacter>  SMBody_Class2;

edit-79ab670b46b24305b43bc464dfbb283c-2022-07-17-16-59-53

如果在蓝图中选中了一个UClass或者一个TSubClassof,可以在运行期根据选中的UClass信息动态创建Actor。

	{
		auto loc = FVector(000,500,0);
		auto rot = FRotator(0);
		auto info = FActorSpawnParameters();
		info.Name = FName(TEXT("BySubClass"));
		auto actor = GetWorld()->SpawnActor<ACharacter>(SMBody_Class2,loc,rot,info);
		if(actor)
			actor->SetActorLabel("BySubClass");
	}

TSubclassOf的实现

TSubclassOf的实现位于\Engine\Source\Runtime\CoreUObject\Public\Templates,下面包含Cast.hSubClassOf两个文件。 Cast的实现以后再研究。

TSubclassOf的比较重要的函数包括

template<class TClass>
class TSubclassOf
{

public:
	typedef typename TChooseClass<TIsDerivedFrom<TClass, FField>::IsDerived, FFieldClass, UClass>::Result TClassType;
	typedef typename TChooseClass<TIsDerivedFrom<TClass, FField>::IsDerived, FField, UObject>::Result TBaseType;
private:
	TClassType* Class;
public:
	/** Default Constructor, defaults to null */
	FORCEINLINE TSubclassOf() :Class(nullptr){}
	/** Constructor that takes a UClass and does a runtime check to make sure this is a compatible class */
	FORCEINLINE TSubclassOf(TClassType* From) :Class(From){}
	/** Copy Constructor, will only compile if types are compatible */
	template <class TClassA, class = decltype(ImplicitConv<TClass*>((TClassA*)nullptr))>
	FORCEINLINE TSubclassOf(const TSubclassOf<TClassA>& From) :
		Class(*From){}

	/** Assignment operator from UClass, the type is checked on get not on set */
	FORCEINLINE TSubclassOf& operator=(TClassType* From)
	{
		Class = From;
		return *this;
	}
	/** Dereference back into a UClass, does runtime type checking */
	FORCEINLINE TClassType* operator*() const
	{
		if (!Class || !Class->IsChildOf(TClass::StaticClass()))
		{
			return nullptr;
		}
		return Class;
	}
	...

首先从成员变量Class开始,其记录了TSubclassOf内部所保存的UClass*指针。 在TSubClassof<APawn> ptr;这样的一行中,TClass作为模版参数被初始化为APawn。 随后我们分析TClassTypeTBaseType

TChooseClass<bCondition,typeA,typeB>类似于三目函数,根据第一个参数布尔变量的值返回typeA或者typeB。 TIsDerivedFrom<TClass, FField>::IsDerived是一个巧妙的模版函数,他判断第一个模版参数TClass是否继承于第二个模版参数FField,是返回true否返回false。

这里是判断传入的模版参数类型的Class信息是继承于FFieldClass,还是UClass,其Obj信息是继承于FField还是UObject。 关于FFieldUObject的分拆似乎是从4.25版本开始的,这部分还没有研究,目前我接触到的所有的对象都是继承于UObject

所以如果传入的是APawn的TClass,那么其TClassTypeTBaseType分别为UClassUObject

TSubclassOf的一段用法可以见

TSubclassOf<APawn> ptr;
ptr = UStaticMesh::StaticClass(); // StaticClass返回UClass*,被保存到ptr内部的`Class`成员变量上

//在ptr->Get()的时候执行isChildOf检查, StaticMesh不在APawn的继承链上
GWorld->SpawnActor<APawn>(ptr->Get(),.....); //ptr->Get() will be nullptr

TSubclassOf在解引用,获得其内部保存的UClass指针时会执行运行时检查。 会检查其内部的UClass成员,是否是TSubclassOf<TClass>种模版参数的TClass的子对象,不是会返回NULL。

IsChildOf是一个O(N)(似乎在发行版本里是O(1))的算法。 A->isChildOf(B)会沿着A的继承链一直往上寻找,逐个比对UClass信息,直到与B的UClass相等,或者找到UObject为止。

graph TD A[UObject] A-->B[AActor] B-->C[APawn] A-->D[UStreamableRenderAsset] D-->E[UStaticMesh]

由于APawnUStaticMesh不在一条继承链上,所以在需要一个APawn对象时候SpawnActor<APawn>,传进去一个UStaticMesh的指针是不会生效的。

这种设计对蓝图比较好,蓝图可以利用TSubclassOf<APawn>筛选出所有APawn类供选择。

TChooseClass的实现

很简单的偏特化的应用

template<bool Predicate,typename TrueClass,typename FalseClass>
class TChooseClass;

template<typename TrueClass,typename FalseClass>
class TChooseClass<true,TrueClass,FalseClass>
{
public:
	typedef TrueClass Result;
};

template<typename TrueClass,typename FalseClass>
class TChooseClass<false,TrueClass,FalseClass>
{
public:
	typedef FalseClass Result;
};

TIsDerivedFrom的实现

这个实现可以说精巧,利用了编译器的函数重载和sizeof编译期求值的特性。 首先定义了两个不同大小类型,具体是什么类型不重要,只要大小不一样即可。

然后定义一个返回DerivedType类型的函数,函数体不重要,只需要返回这个类型即可。

最后通过Test(DerivedTypePtr()),实际上相当于Test(DerivedType*),编译器会决定函数重载。 如果DerivedType*能够转换到BaseType*,那么此函数返回Yes&,否则返回No&。 同时通过sizeof比较,即可获得布尔值。

template<typename DerivedType, typename BaseType>
struct TIsDerivedFrom
{
	// Different size types so we can compare their sizes later.
	typedef char No[1];
	typedef char Yes[2];

	// Overloading Test() s.t. only calling it with something that is
	// a BaseType (or inherited from the BaseType) will return a Yes.
	static Yes& Test( BaseType* );
	static Yes& Test( const BaseType* );
	static No& Test( ... );

	// Makes a DerivedType ptr.
	static DerivedType* DerivedTypePtr(){ return nullptr ;}

	public:
	// Test the derived type pointer. If it inherits from BaseType, the Test( BaseType* ) 
	// will be chosen. If it does not, Test( ... ) will be chosen.
	static const bool Value = sizeof(Test( DerivedTypePtr() )) == sizeof(Yes);

	static const bool IsDerived = Value;
};

根据我的测试https://godbolt.org/z/v57neb1EEhttps://godbolt.org/z/e3KzeMh76TIsDerivedFrom不能处理volatile类型,应该再加一个static Yes& Test( const volatile BaseType* );重载。

同时生成DerivedType*也不需要一个函数,虽然模版参数要在实例化的时候才知道,但是用模版参数声明一个指针还是做得到的,声明DerivedType* ptr即可。