虚幻C++ Cast解析

Unreal Engine C++ Cast(SomeObject) 允许动态类型安全地转换对象。 但是 Cast 和 dynamic_cast<T*> 有什么区别呢? 让我们弄清楚!

Unreal Engine C++ 为反射系统提供了内置支持,该系统提供了执行类型安全向上和向下转换的方式,而无需 dynamic_cast<T*>。 让我们看看函数 Cast:

template <typename To, typename From>
FORCEINLINE To* Cast(From* Src)
{
	return TCastImpl<From, To>::DoCast(Src);
}

Cast 函数简单地使用一些称为 TCastImpl 的模板结构将 From 类的指针转换为 To 类的指针。 事实上,TCastImpl 是所有魔法发生的地方。

template <typename From, typename To, ECastType CastType = TGetCastType<From, To>::Value>
struct TCastImpl
{
	// This is the cast flags implementation
	FORCEINLINE static To* DoCast( UObject* Src )
	{
		return Src && Src->GetClass()->HasAnyCastFlag(TCastFlags<To>::Value) ? (To*)Src : nullptr;
	}

	FORCEINLINE static To* DoCastCheckedWithoutTypeCheck( UObject* Src )
	{
		return (To*)Src;
	}
};

给定 From 类 DoCast 的有效指针,使用 C 风格转换将其转换为 To 类的指针,否则返回 nullptr(注意使用 C 风格转换时不需要考虑 dynamic_cast)。 这样 const 和非常量指针都可以正确处理,因为 C 风格的转换首先尝试 const_cast,然后才尝试 static_cast。 到目前为止,一切都很好。

关于 DoCast 实现有几个问题。 首先是 HasAnyCastFlag() 函数的效率如何? 事实证明,这个函数只是检查位掩码。 另请注意,使用 FORCEINLINE(实际上是 MSVC 支持的 __forceinline 关键字)可能会消除函数调用成本。

FORCEINLINE bool HasAnyCastFlag(EClassCastFlags FlagToCheck) const
{
    return (ClassCastFlags&FlagToCheck) != 0;
}

此外,切记 Cast 函数不应过于频繁地调用,这一点很重要。 理想情况下,游戏代码根本不应该调用 Cast 函数!

第二个问题与 C 风格的转换有关。 在某些情况下会使用reinterpret_cast吗? 答案并不是因为那是虚幻反射系统发挥作用的地方。 它所做的只是在其 CDO(类默认对象)中存储有关该类的附加信息。 更具体地说,它是相应的 UStruct 对象或 ClassCastFlags 或两者。 在运行时使用此反射信息可以确定两个类是否属于同一层次结构,如果不属于则返回 nullptr。

现在让我们回到结构 TCastImpl。 事实证明,上面的 TCastImpl::DoCast 函数不会在 UE4 C++ 模块的默认配置中被调用。 为什么? 这完全归功于模板结构 TGetCastType 的工作方式(如下)。

template <typename From, typename To, bool bFromInterface = TIsIInterface<From>::Value, bool bToInterface = TIsIInterface<To>::Value, EClassCastFlags CastClass = TCastFlags<To>::Value>
struct TGetCastType
{
#if USTRUCT_FAST_ISCHILDOF_IMPL == USTRUCT_ISCHILDOF_STRUCTARRAY
	static const ECastType Value = ECastType::UObjectToUObject;
#else
	static const ECastType Value = ECastType::FromCastFlags;
#endif
};

template <typename From, typename To                           > struct TGetCastType<From, To, false, false, CASTCLASS_None> { static const ECastType Value = ECastType::UObjectToUObject;     };
template <typename From, typename To                           > struct TGetCastType<From, To, false, true , CASTCLASS_None> { static const ECastType Value = ECastType::UObjectToInterface;   };
template <typename From, typename To, EClassCastFlags CastClass> struct TGetCastType<From, To, true,  false, CastClass     > { static const ECastType Value = ECastType::InterfaceToUObject;   };
template <typename From, typename To, EClassCastFlags CastClass> struct TGetCastType<From, To, true,  true , CastClass     > { static const ECastType Value = ECastType::InterfaceToInterface; };

检查 TGetCastType 的第三个和第四个模板参数的接口类型。 这样,将选择上面四个 TGetCastType 特化之一。 之后,将使用 ECastType 值选择 TCastImpl 结构的特化来执行转换本身(UObject 到 UObject,UObject 到 Interface 等等)。

让我们看一下处理 UObject 到 UObject 转换的 TCastImpl 特化之一(其他 TCastImpl 特化非常相似):

template <typename From, typename To>
struct TCastImpl<From, To, ECastType::UObjectToUObject>
{
	FORCEINLINE static To* DoCast( UObject* Src )
	{
		return Src && Src->IsA<To>() ? (To*)Src : nullptr;
	}

	FORCEINLINE static To* DoCastCheckedWithoutTypeCheck( UObject* Src )
	{
		return (To*)Src;
	}
};

DoCast() 调用 IsA() 函数来确定传递的对象是否为指定类型。 如果是,则应用 C 风格转换。

在 UE4 C++ 模块的默认配置中调用 IsA() 函数的性能成本是多少? 事实证明,IsA() 函数的性能重要部分是对 IsChildOf() 函数的调用,该函数具有两个完全不同的实现。

	/** Returns true if this struct either is SomeBase, or is a child of SomeBase. This will not crash on null structs */
#if USTRUCT_FAST_ISCHILDOF_COMPARE_WITH_OUTERWALK || USTRUCT_FAST_ISCHILDOF_IMPL == USTRUCT_ISCHILDOF_OUTERWALK
	bool IsChildOf( const UStruct* SomeBase ) const;
#else
	bool IsChildOf(const UStruct* SomeBase) const
	{
		return (SomeBase ? IsChildOfUsingStructArray(*SomeBase) : false);
	}
#endif

当 UE_EDITOR = 1 预处理器指令 USTRUCT_FAST_ISCHILDOF_IMPL = USTRUCT_ISCHILDOF_OUTERWALK 时,这意味着将使用以下 IsChildOf 函数的实现:

#if USTRUCT_FAST_ISCHILDOF_COMPARE_WITH_OUTERWALK || USTRUCT_FAST_ISCHILDOF_IMPL == USTRUCT_ISCHILDOF_OUTERWALK
bool UStruct::IsChildOf( const UStruct* SomeBase ) const
{
	if (SomeBase == nullptr)
	{
		return false;
	}

	bool bOldResult = false;
	for ( const UStruct* TempStruct=this; TempStruct; TempStruct=TempStruct->GetSuperStruct() )
	{
		if ( TempStruct == SomeBase )
		{
			bOldResult = true;
			break;
		}
	}

#if USTRUCT_FAST_ISCHILDOF_IMPL == USTRUCT_ISCHILDOF_STRUCTARRAY
	const bool bNewResult = IsChildOfUsingStructArray(*SomeBase);
#endif

#if USTRUCT_FAST_ISCHILDOF_COMPARE_WITH_OUTERWALK
	ensureMsgf(bOldResult == bNewResult, TEXT("New cast code failed"));
#endif

	return bOldResult;
}
#endif

这里最重要的部分是内部 for 循环,它试图在给定类和传递的类之间找到一对相等的反射结构。 如果找到这样的一对,那么一个类是另一个类的孩子。 此类 IsChildOf 函数的运行时成本等于继承树的深度 O(Depth(InheritanceTree))。

当 UE_EDITOR = 1 预处理器指令 USTRUCT_FAST_ISCHILDOF_IMPL = USTRUCT_ISCHILDOF_STRUCTARRAY 时,这意味着将使用以下 IsChildOf 函数的实现:

bool IsChildOf(const UStruct* SomeBase) const
{
    return (SomeBase ? IsChildOfUsingStructArray(*SomeBase) : false);
}

#if USTRUCT_FAST_ISCHILDOF_IMPL == USTRUCT_ISCHILDOF_STRUCTARRAY
class FStructBaseChain
{
protected:
	COREUOBJECT_API FStructBaseChain();
	COREUOBJECT_API ~FStructBaseChain();

	// Non-copyable
	FStructBaseChain(const FStructBaseChain&) = delete;
	FStructBaseChain& operator=(const FStructBaseChain&) = delete;

	COREUOBJECT_API void ReinitializeBaseChainArray();

    // this is O(1) implementation of IsChildOf
	FORCEINLINE bool IsChildOfUsingStructArray(const FStructBaseChain& Parent) const
	{
		int32 NumParentStructBasesInChainMinusOne = Parent.NumStructBasesInChainMinusOne;
		return NumParentStructBasesInChainMinusOne <= NumStructBasesInChainMinusOne && StructBaseChainArray[NumParentStructBasesInChainMinusOne] == &Parent;
	}

private:
	FStructBaseChain** StructBaseChainArray;
	int32 NumStructBasesInChainMinusOne;

	friend class UStruct;
};
#endif

函数 IsChildOfUsingStructArray 使用 StructBaseChainArray 数组作为类的反射数据或结构的存储,以加速检查算法。

Cast 如何同时支持向上转换和向下转换? 原因是不同类的指针可以指向同一个对象,但这不会改变它们的反射数据或结构(实际上是 StructBaseChainArray 的内容)所以 IsA 总是会找到一对相等的反射结构,因为两个类都属于同一个 类层次结构。

由此得出 Cast 运行时成本为:

- Linear,   O(Depth(InheritanceTree)), in the editor environment     (UE_EDITOR = 1).
- Constant, O(1),                      in the non-editor environment (UE_EDITOR = 0).

为了略微降低 Cast 运行时成本,有函数 ExactCast:

template< class T >
FORCEINLINE T* ExactCast( UObject* Src )
{
	return Src && (Src->GetClass() == T::StaticClass()) ? (T*)Src : nullptr;
}

GetClass() 和 StaticClass() 调用都是 O(1),因此当事先知道传递的对象的类型时,ExactCast 是一个不错的选择。 甚至还有一个更高效的 CastChecked 函数,它在非编辑器环境中基本上是 C 风格的转换,但它并不是那么安全。

现在,如果可以让 static_cast 做完全相同的事情,那么使用 Cast 有什么意义呢? 答案是类型安全。 如果类类型不匹配或更改继承树 Cast 将返回 nullptr。 相反,static_cast 可能会执行强制转换并返回一些无效指针。 以下是此类行为的示例:

// APawn is the parent of ACharacter
APawn* NewPawn = NewObject<APawn>(GWorld);

ACharacter* StaticCastCharacter = static_cast<ACharacter*>(NewPawn);
ACharacter* UnrealCastCharacter = Cast<ACharacter>(NewPawn);

即使创建的对象是 APawn 类型,上面的 static_cast<ACharacter*> 也会返回一个指向 ACharacter 的指针。 但是,Cast 将返回 nullptr,这要好得多。

要关闭 C++ 强制转换主题,人们可能仍会在 Unreal C++ 中使用 dynamic_cast,但在可能的情况下(当从指针到指针或从右值到右值时)使用 Cast 函数被严重“覆盖”。

综上所述,Unreal C++ 中关于 Cast 的关键事项如下:

- Cast<T> has to be used for *UObjects* due to type safety; it will return *nullptr* in case of a failure in comparison with *static_cast*.
- Cast<T> runtime cost is *O(1) or constant* in non-editor environment and *O(Depth(InheritanceTree))* in editor environment.
- Cast<T> does not use *dynamic_cast*.

原文链接:HOW UNREAL ENGINE C++ CAST<T> FUNCTION WORKS?

BimAnt翻译整理,转载请标明出处