NSDT工具推荐: Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - AI模型在线查看 - Three.js虚拟轴心开发包 - 3D模型在线减面 - STL模型在线切割 - 3D道路快速建模
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翻译整理,转载请标明出处