类型别名
模板类片段:
template<typename InElementType, typename InAllocatorType>
class TArray
{template <typename OtherInElementType, typename OtherAllocator>friend class TArray;public:using SizeType = typename InAllocatorType::SizeType ;using ElementType = InElementType;using AllocatorType = InAllocatorType;private:using USizeType = typename std::make_unsigned_t<SizeType>;public:using ElementAllocatorType = std::conditional_t<AllocatorType::NeedsElementType,typename AllocatorType::template ForElementType<ElementType>,typename AllocatorType::ForAnyElementType>;static_assert(std::is_signed_v<SizeType>, "TArray only supports signed index types");
}
使用TArray存放Actor时,默认的别名类型:
using Array = TArray<AActor*>;//signed int using ArraySizeType = Array::SizeType;//AActor*using ArrayElementType = Array::ElementType;// TSizedDefaultAllocator<32>using ArrayAllocatorType = Array::AllocatorType;// TSizedHeapAllocator<32>::ForElementType<AActor*>using ArrayElementAllocatorType = Array::ElementAllocatorType;// TSizedHeapAllocator<32>using ArrayAllocatorTypeTypedef = ArrayAllocatorType::Typedef;//signed int using ArrayAllocatorSizeType = ArrayAllocatorType::SizeType;
AllocatorType
TArray::AllocatorType 默认分配器是 TSizedDefaultAllocator<32>
● 作用:定义容器使用的 内存分配器类型(如 FDefaultAllocator、TInlineAllocator)。
● 设计原因:
允许用户自定义内存管理策略(堆分配、栈分配、内存池等)。
SizeType
● 作用:定义容器中 索引和大小 的类型(如 int32、int64)。
● 设计原因:
由分配器 InAllocatorType 决定,允许不同分配器根据需求选择不同大小的整数类型(例如,小内存容器用 int32,大内存用 int64)。
● 示例:
若分配器定义为 TSizedHeapAllocator<32>,则 SizeType 可能是 int32。
SizeType来自于分配器的SizeType,因为默认分配器使用 TSizedDefaultAllocator<32>,所以要去找TSizedDefaultAllocator里面的SizeType是什么.
using SizeType = typename InAllocatorType::SizeType ;//----------分配器类-------//
template <int IndexSize> class TSizedDefaultAllocator
: public TSizedHeapAllocator<IndexSize>
{ public: typedef TSizedHeapAllocator<IndexSize> Typedef; };template <int IndexSize, typename BaseMallocType = FMemory>
class TSizedHeapAllocator
{
public:using SizeType = typename TBitsToSizeType<IndexSize>::Type;
}//---------------------//
template <int IndexSize>
struct TBitsToSizeType
{// Fabricate a compile-time false result that's still dependent on the template parameterstatic_assert(IndexSize == IndexSize+1, "Unsupported allocator index size.");
};template <> struct TBitsToSizeType<8> { using Type = int8; };
template <> struct TBitsToSizeType<16> { using Type = int16; };
template <> struct TBitsToSizeType<32> { using Type = int32; };
template <> struct TBitsToSizeType<64> { using Type = int64; };//----------UE在Windows环境下的int类型--------------//
// 8-bit signed integer
typedef signed char int8;// 16-bit signed integer
typedef signed short int int16;// 32-bit signed integer
typedef signed int int32;// 64-bit signed integer
typedef signed long long int64;
TSizedDefaultAllocator 继承自 TSizedHeapAllocator, TSizedHeapAllocator中定义了SizeType,
通过偏特化,当IndexSize是32时 ,SizeType = int32.
ElementType
● 作用:表示容器存储的 元素类型(如 int、FString)。
● 设计原因:直接透传模板参数 InElementType,简化代码中对元素类型的引用。
using Array = TArray<AActor*>;
//AActor*
using ArrayElementType = Array::ElementType;
TArray存放Actor,那么ElementType就是AActor类型.
USizeType
● 作用:将 SizeType 转换为 无符号整数类型(如 uint32、uint64)。
● 设计原因:在需要无符号运算的场景(如内存块大小计算)避免负数溢出问题。
ElementAllocatorType
● 作用:根据分配器是否需要元素类型,选择具体的 内存分配器实现。
● 设计原因:
○ NeedsElementType = true:分配器需要知道元素类型(例如,为元素构造/析构或对齐优化)。此时使用 ForElementType
○ NeedsElementType = false:分配器不依赖元素类型(例如,原始内存块管理)。此时使用 ForAnyElementType,即通用分配器。
using ElementAllocatorType = std::conditional_t<AllocatorType::NeedsElementType,typename AllocatorType::template ForElementType<ElementType>,typename AllocatorType::ForAnyElementType
>;
默认的分配器定义了这个NeedsElementType类型:
template <int IndexSize, typename BaseMallocType = FMemory>
class TSizedHeapAllocator
{
public:using SizeType = typename TBitsToSizeType<IndexSize>::Type;private:using USizeType = std::make_unsigned_t<SizeType>;public:enum { NeedsElementType = true };enum { RequireRangeCheck = true };
}
因此通过std::conditional_t的匹配,选择ForElementType
TArray存放AActor时,类型为:TSizedHeapAllocator<32>::ForElementType<AActor>
// TSizedHeapAllocator<32>::ForElementType<AActor*>
using ArrayElementAllocatorType = Array::ElementAllocatorType;
using ElementAllocatorType = std::conditional_t<AllocatorType::NeedsElementType,typename AllocatorType::template ForElementType<ElementType>,typename AllocatorType::ForAnyElementType
>;//---------conditional的源码---------//
template <bool _Test, class _Ty1, class _Ty2>
struct conditional { // Choose _Ty1 if _Test is true, and _Ty2 otherwiseusing type = _Ty1;
};template <class _Ty1, class _Ty2>
struct conditional<false, _Ty1, _Ty2> {using type = _Ty2;
};template <bool _Test, class _Ty1, class _Ty2>
using conditional_t = typename conditional<_Test, _Ty1, _Ty2>::type;
当第一个bool值为true时,即 AllocatorType::NeedsElementType 为true,
那么conditional的type选择 AllocatorType::template ForElementType
如果是false,conditional的type将会选择 AllocatorType::ForAnyElementType
static_assert
● 作用:编译时检查 SizeType 是否为 有符号整数类型。
● 设计原因:
○ 安全性:有符号整数可以表示负数,便于处理 InvalidIndex(如 INDEX_NONE = -1)。
○ 兼容性:UE 代码中许多接口依赖有符号索引(如 TArray::Find 返回 int32)。
○ 错误预防:避免用户误用无符号类型导致索引计算错误(如 0u - 1 溢出为最大值)。
static_assert(std::is_signed_v<SizeType>, "TArray only supports signed index types");
组件 | 功能 | 设计目标 |
---|---|---|
SizeType | 定义索引和容量类型,由分配器决定 | 支持不同内存规模的容器 |
ElementType | 容器存储的元素类型 | 泛型编程的核心 |
AllocatorType | 内存分配策略(堆、栈、池等) | 灵活的内存管理 |
USizeType | 无符号类型,用于内存大小计算 | 避免无符号运算的溢出问题 |
ElementAllocatorType | 根据分配器需求选择类型相关或无关的实现 | 优化内存操作(构造/析构、对齐) |
static_assert | 强制 SizeType为有符号类型 | 确保索引逻辑安全 |
构造函数
TArray的成员变量
ElementAllocatorType AllocatorInstance;
SizeType ArrayNum;
SizeType ArrayMax;
SizeType = int32 = signed int
ElementAllocatorType = TSizedHeapAllocator<32>::ForElementType<AActor*>
默认构造
/*** Constructor, initializes element number counters.*/
FORCEINLINE TArray(): ArrayNum(0), ArrayMax(AllocatorInstance.GetInitialCapacity())
{}SizeType GetInitialCapacity() const
{return 0;
}// 创建一个空的 TArray(元素类型为 int32)
TArray<int32> MyArray;
// MyArray 初始状态:Num() = 0, Max() = Allocator 的初始容量(例如 4)
只定义一个TArray
从原始数组构造
/*** Constructor from a raw array of elements.** @param Ptr A pointer to an array of elements to copy.* @param Count The number of elements to copy from Ptr.* @see Append*/
FORCEINLINE TArray(const ElementType* Ptr, SizeType Count)
{if (Count < 0){// Cast to USizeType first to prevent sign extension on negative sizes, producing unusually large values.// 首先转换为 USizeType 以防止负数大小在符号扩展时产生异常大的值。UE::Core::Private::OnInvalidArrayNum((unsigned long long)(USizeType)Count);}check(Ptr != nullptr || Count == 0);CopyToEmpty(Ptr, Count, 0);
}
// 原始 C 风格数组
int32 RawArray[] = {10, 20, 30};// 从 RawArray 构造 TArray(复制前3个元素)
TArray<int32> MyArray(RawArray, 3);
// MyArray 内容:{10, 20, 30}
CopyToEmpty
template <typename OtherElementType, typename OtherSizeType>
void CopyToEmpty(const OtherElementType* OtherData, OtherSizeType OtherNum, SizeType PrevMax)
{//类型转换SizeType NewNum = (SizeType)OtherNum;//检测 溢出/丢失精度checkf((OtherSizeType)NewNum == OtherNum, TEXT("Invalid number of elements to add to this array type: %lld"), (long long)NewNum);//更新元素数量ArrayNum = NewNum;//如果 OtherNum 或 PrevMax 不为零,则需要调整当前数组的大小以适应新数据if (OtherNum || PrevMax){//调整数组的容量以适应新的元素数量。ResizeForCopy(NewNum, PrevMax);//构造元素ConstructItems<ElementType>((void*)GetData(), OtherData, OtherNum);}else{// 设为0ArrayMax = AllocatorInstance.GetInitialCapacity();}SlackTrackerNumChanged();
}
ConstructItems
/*** Constructs a range of items into memory from a set of arguments. The arguments come from an another array.** @param Dest The memory location to start copying into.* @param Source A pointer to the first argument to pass to the constructor.* @param Count The number of elements to copy.*/
template <typename DestinationElementType,typename SourceElementType,typename SizeTypeUE_REQUIRES(sizeof(DestinationElementType) > 0 && sizeof(SourceElementType) > 0 && TIsBitwiseConstructible<DestinationElementType, SourceElementType>::Value) // the sizeof here should improve the error messages we get when we try to call this function with incomplete types
>
FORCEINLINE void ConstructItems(void* Dest, const SourceElementType* Source, SizeType Count)
{if (Count){FMemory::Memcpy(Dest, Source, sizeof(SourceElementType) * Count);}
}
//
template <typename DestinationElementType,typename SourceElementType,typename SizeTypeUE_REQUIRES(sizeof(DestinationElementType) > 0 && sizeof(SourceElementType) > 0 && !TIsBitwiseConstructible<DestinationElementType, SourceElementType>::Value) // the sizeof here should improve the error messages we get when we try to call this function with incomplete types
>
FORCENOINLINE void ConstructItems(void* Dest, const SourceElementType* Source, SizeType Count)
{while (Count) {::new ((void*)Dest) DestinationElementType(*Source); // Placement new 构造++(DestinationElementType*&)Dest; // 指针步进++Source;--Count;}
}
这两个模板函数用于将源数组元素构造到目标内存中,根据类型是否支持位拷贝(bitwise copy)选择不同的构造策略。
通过模板特化和类型特性检查,在保证类型安全的前提下,为简单类型提供高效内存拷贝,为复杂类型提供安全的逐个构造逻辑,是 UE 容器高性能设计的核心机制之一。
第一个版本 即使用Memcpy, 是 位拷贝优化版本(快速路径)
TIsBitwiseConstructible 类型特性检查,判断 SourceElementType 是否可以直接按位复制到 DestinationElementType 的内存中(如基本类型、POD 类型)
FMemory::Memcpy 直接内存拷贝,效率高(O(n) 时间,无额外构造/析构调用)
使用场景:
● 基础类型:int32, float, FVector 等。
● POD 类型:无自定义构造函数/析构函数的结构体。
● 内存布局兼容的类型:保证 Source 和 Dest 的二进制兼容性。
第二个版本 即使用while循环,是逐个构造版本(安全路径)
!TIsBitwiseConstructible 类型不兼容位拷贝时启用此版本
Placement new 在目标内存地址调用构造函数(支持非 POD 类型)
指针步进 手动移动目标指针到下一个元素位置
使用场景:
● 非 POD 类型:如包含 FString、TSharedPtr 等需要构造/析构的类型。
● 有自定义构造逻辑:需要调用拷贝构造函数或转换构造函数。
从TArrayView构造
template <typename OtherElementType, typename OtherSizeType>
explicit TArray(const TArrayView<OtherElementType, OtherSizeType>& Other);//--------------//
template<typename InElementType, typename InAllocatorType>
template<typename OtherElementType, typename OtherSizeType>
FORCEINLINE TArray<InElementType, InAllocatorType>::TArray(const TArrayView<OtherElementType, OtherSizeType>& Other)
{CopyToEmpty(Other.GetData(), Other.Num(), 0);
}// 原始 C 风格数组
int32 RawArray[] = {10, 20, 30};// 创建一个 TArrayView(视图)
TArrayView<int32> View = MakeArrayView(RawArray, 3); // 从 View 构造 TArray(复制数据)
TArray<int32> MyArray(View);
// MyArray 内容:{10, 20, 30}
初始化列表
/*** Initializer list constructor*/
TArray(std::initializer_list<InElementType> InitList)
{// This is not strictly legal, as std::initializer_list's iterators are not guaranteed to be pointers, but// this appears to be the case on all of our implementations. Also, if it's not true on a new implementation,// it will fail to compile rather than behave badly.CopyToEmpty(InitList.begin(), (SizeType)InitList.size(), 0);
}// 直接通过初始化列表构造
TArray<FString> MyArray = {"Apple", "Banana", "Cherry"};
// MyArray 内容:{"Apple", "Banana", "Cherry"}
注释里面叽里呱啦说啥呢?
严格来说,这是不合法的,因为 std::initializer_list 的迭代器并不保证是指针,
但是在我们所有的实现中似乎都是这样的情况。此外,如果在新的实现中它不是指针,
那么它将会编译失败,而不是表现糟糕。
合法性问题:从技术上讲,假设 std::initializer_list 的迭代器是指针是不符合标准的,因为标准并没有保证这一点。
实际实现情况:尽管标准没有保证,但在当前的所有实现中,这些迭代器实际上确实是指针。
跨分配器
/*** Copy constructor with changed allocator. Use the common routine to perform the copy.** @param Other The source array to copy.*/
template <typename OtherElementType,typename OtherAllocatorUE_REQUIRES(UE::Core::Private::TArrayElementsAreCompatible_V<ElementType, const OtherElementType&>)
>
FORCEINLINE explicit TArray(const TArray<OtherElementType, OtherAllocator>& Other)
{CopyToEmpty(Other.GetData(), Other.Num(), 0);
}// 源数组(使用默认分配器)
TArray<int32, FDefaultAllocator> SourceArray = {1, 2, 3};// 目标数组(使用其他分配器,如栈分配器)
TArray<int32, TInlineAllocator<16>> MyArray(SourceArray);
// MyArray 内容:{1, 2, 3}(元素类型兼容即可拷贝)
拷贝构造
/*** Copy constructor. Use the common routine to perform the copy.** @param Other The source array to copy.*/
FORCEINLINE TArray(const TArray& Other)
{CopyToEmpty(Other.GetData(), Other.Num(), 0);
}TArray<FVector> SourceArray;
SourceArray.Add(FVector(1.0f, 2.0f, 3.0f));// 拷贝构造
TArray<FVector> MyArray(SourceArray);
// MyArray 内容:{FVector(1,2,3)}
带预分配内存的拷贝构造
/*** Copy constructor. Use the common routine to perform the copy.** @param Other The source array to copy.* @param ExtraSlack Tells how much extra memory should be preallocated* at the end of the array in the number of elements.*/
FORCEINLINE TArray(const TArray& Other, SizeType ExtraSlack)
{CopyToEmptyWithSlack(Other.GetData(), Other.Num(), 0, ExtraSlack);
}TArray<int32> SourceArray = {100, 200};// 拷贝构造并预留 5 个额外空间
TArray<int32> MyArray(SourceArray, 5);
// MyArray 内容:{100, 200}
// MyArray 容量:2(元素数) + 5(额外空间) = 7
ExtraSlack 数组末尾按元素的数量预先分配多少额外内存。
移动构造
FORCEINLINE TArray(TArray&& Other)
{MoveOrCopy(*this, Other, 0);
}//-------------------------//
template <typename OtherElementType,typename OtherAllocatorUE_REQUIRES(UE::Core::Private::TArrayElementsAreCompatible_V<ElementType, OtherElementType&&>)
>
FORCEINLINE explicit TArray(TArray<OtherElementType, OtherAllocator>&& Other)
{MoveOrCopy(*this, Other, 0);
}//---------------------------//
template <typename OtherElementTypeUE_REQUIRES(UE::Core::Private::TArrayElementsAreCompatible_V<ElementType, OtherElementType&&>)
>
TArray(TArray<OtherElementType, AllocatorType>&& Other, SizeType ExtraSlack)
{MoveOrCopyWithSlack(*this, Other, 0, ExtraSlack);
}
MoveOrCpoy
template <typename FromArrayType, typename ToArrayType>
static FORCEINLINE void MoveOrCopy(ToArrayType& ToArray, FromArrayType& FromArray, SizeType PrevMax)
{if constexpr (UE::Core::Private::CanMoveTArrayPointersBetweenArrayTypes<FromArrayType, ToArrayType>()){// Movestatic_assert(std::is_same_v<TArray, ToArrayType>, "MoveOrCopy is expected to be called with the current array type as the destination");using FromAllocatorType = typename FromArrayType::AllocatorType;using ToAllocatorType = typename ToArrayType::AllocatorType;if constexpr (TCanMoveBetweenAllocators<FromAllocatorType, ToAllocatorType>::Value){ToArray.AllocatorInstance.template MoveToEmptyFromOtherAllocator<FromAllocatorType>(FromArray.AllocatorInstance);}else{ToArray.AllocatorInstance.MoveToEmpty(FromArray.AllocatorInstance);}ToArray .ArrayNum = (SizeType)FromArray.ArrayNum;ToArray .ArrayMax = (SizeType)FromArray.ArrayMax;// Ensure the destination container could hold the source range (when the allocator size types shrink)if constexpr (sizeof(USizeType) < sizeof(typename FromArrayType::USizeType)){if (ToArray.ArrayNum != FromArray.ArrayNum || ToArray.ArrayMax != FromArray.ArrayMax){// Cast to USizeType first to prevent sign extension on negative sizes, producing unusually large values.UE::Core::Private::OnInvalidArrayNum((unsigned long long)(USizeType)ToArray.ArrayNum);}}FromArray.ArrayNum = 0;FromArray.ArrayMax = FromArray.AllocatorInstance.GetInitialCapacity();FromArray.SlackTrackerNumChanged();ToArray.SlackTrackerNumChanged();}else{// CopyToArray.CopyToEmpty(FromArray.GetData(), FromArray.Num(), PrevMax);}
}
MoveOrCopy 是用于在两个 TArray(或类似容器)之间高效地 移动或拷贝数据 的模板函数。
其核心逻辑是:
● 移动(Move):若源数组(FromArray)和目标数组(ToArray)的 内存分配器(Allocator)和元素类型兼容,直接转移内存所有权(零拷贝)。
● 拷贝(Copy):若条件不满足,则执行深拷贝。
片段1:
if constexpr (TCanMoveBetweenAllocators<FromAllocatorType, ToAllocatorType>::Value)
{ToArray.AllocatorInstance.template MoveToEmptyFromOtherAllocator<FromAllocatorType>(FromArray.AllocatorInstance);
}
else
{ToArray.AllocatorInstance.MoveToEmpty(FromArray.AllocatorInstance);
}//------------------------------------------------//
template <typename FromAllocatorType, typename ToAllocatorType>
struct TCanMoveBetweenAllocators
{enum { Value = false };
};template <uint8 FromIndexSize, uint8 ToIndexSize>
struct TCanMoveBetweenAllocators<TSizedHeapAllocator<FromIndexSize>, TSizedHeapAllocator<ToIndexSize>>
{// Allow conversions between different int width versions of the allocatorenum { Value = true };
};template <uint8 FromIndexSize, uint8 ToIndexSize>
struct TCanMoveBetweenAllocators<TSizedDefaultAllocator<FromIndexSize>, TSizedDefaultAllocator<ToIndexSize>>
: TCanMoveBetweenAllocators<typename TSizedDefaultAllocator<FromIndexSize>::Typedef, typename TSizedDefaultAllocator<ToIndexSize>::Typedef> {};
TCanMoveBetweenAllocators
接受两个模板参数
传入的两个都不是 TSizedHeapAllocator
或 TSizedDefaultAllocator
,Value为 false
,
传入的都是TSizedHeapAllocator
,Value为 true
传入的都是 TSizedDefaultAllocator
,则根据 TSizedDefaultAllocator
内部的兼容性判断 true
和false
.
Value=true 调用MoveToEmptyFromOtherAllocator
Value=false 调用MoveToEmpty
高僧解释:
TCanMoveBetweenAllocators
:你给我传的是什么东西我请问了,
传俩 TSizedHeapAllocator
给我,OK 我回答你true
.
传俩 TSizedDefaultAllocator
,我只能说 具体问题具体分析.
传来两个既不是 TSizedHeapAllocator
也不是 TSizedDefaultAllocator
,那我只能告诉你 false
.
if constexpr (TCanMoveBetweenAllocators<FromAllocatorType, ToAllocatorType>::Value)
{ToArray.AllocatorInstance.template MoveToEmptyFromOtherAllocator<FromAllocatorType>(FromArray.AllocatorInstance);
}
else
{ToArray.AllocatorInstance.MoveToEmpty(FromArray.AllocatorInstance);
}//-----------------------------------//template <typename OtherAllocator>
FORCEINLINE void MoveToEmptyFromOtherAllocator(typename OtherAllocator::ForAnyElementType& Other)
{// 检查当前分配器实例和传入的分配器实例不是同一个对象checkSlow((void*)this != (void*)&Other);// 如果当前分配器已经有分配的数据,则释放这些数据if (Data){BaseMallocType::Free(Data);}// 将源分配器的数据指针赋值给当前分配器Data = Other.Data;// 将源分配器的数据指针设置为 nullptr,表示它现在为空Other.Data = nullptr;
}FORCEINLINE void MoveToEmpty(ForAnyElementType& Other)
{// 调用模板函数,指定 TSizedHeapAllocator 作为模板参数this->MoveToEmptyFromOtherAllocator<TSizedHeapAllocator>(Other);
}
这两段代码的核心区别在于 分配器类型兼容性处理,具体分为两种情况:
- 当分配器类型兼容时 (TCanMoveBetweenAllocators = true)
● 模板参数:显式指定源分配器类型 FromAllocatorType。
● 目的:
当源分配器和目标分配器 类型不同但兼容(如 TSizedHeapAllocator<32> 和 TSizedHeapAllocator<64>),通过模板参数传递源分配器的类型,确保正确调用其内存管理逻辑(如释放旧内存、接管新内存)。
● 示例场景:
源数组使用 TSizedHeapAllocator<32>,目标数组使用 TSizedHeapAllocator<64>,但两者支持跨分配器移动。 - 当分配器类型不兼容时 (TCanMoveBetweenAllocators = false)
● 实际调用:
MoveToEmpty 内部调用 MoveToEmptyFromOtherAllocator,即强制将源分配器视为默认的 TSizedHeapAllocator。
● 目的:
当分配器类型 不兼容 时,假设源分配器与目标分配器共享相同的默认内存管理策略(如堆分配),直接通过默认逻辑转移内存。
● 风险:
若源分配器实际类型非 TSizedHeapAllocator,可能导致未定义行为(如内存泄漏或崩溃)。因此此路径仅在类型严格兼容时安全。
场景 1:同类型分配器移动
TArray<int32, TSizedHeapAllocator<32>> Source, Dest;
MoveOrCopy(Dest, Source, 0);
● 调用路径:
MoveToEmptyFromOtherAllocator<TSizedHeapAllocator<32>>。
● 行为:
安全转移内存,无额外开销。
场景 2:不同类型分配器移动(但兼容)
TArray<int32, TSizedHeapAllocator<32>> Source;
TArray<int32, TSizedHeapAllocator<64>> Dest;
MoveOrCopy(Dest, Source, 0);
● 调用路径:
MoveToEmptyFromOtherAllocator<TSizedHeapAllocator<32>>。
● 行为:
根据源分配器类型释放旧内存,正确接管新内存。
场景 3:不兼容分配器移动
TArray<int32, TInlineAllocator<16>> Source;
TArray<int32, TSizedHeapAllocator<32>> Dest;
MoveOrCopy(Dest, Source, 0);
● 调用路径:
MoveToEmpty → MoveToEmptyFromOtherAllocator
● 行为:
错误!TInlineAllocator(栈分配)与 TSizedHeapAllocator(堆分配)策略不同,强制转换导致未定义行为。
条件 | 调用方式 | 安全性 | 适用场景 |
---|---|---|---|
分配器兼容 (TCanMove=true) | 显式模板参数传递 (FromAllocatorType) | 高 | 不同类型但内存策略兼容的分配器 |
分配器不兼容 (TCanMove=false) | 强制默认分配器类型 (TSizedHeapAllocator) | 低 | 仅限同类型或设计确保安全的分配器 |
设计原则: | |||
● 零开销抽象:通过编译时分支选择最优路径。 | |||
● 类型安全:利用模板特性确保内存操作正确性。 | |||
● 扩展性:允许自定义分配器通过特化声明兼容性。 |
最佳实践:
● 自定义分配器:若需支持跨类型移动,需特化 TCanMoveBetweenAllocators。
● 谨慎使用默认路径:确保不兼容分配器的移动操作在设计中无害。
片段2:
ToArray.ArrayNum = (SizeType)FromArray.ArrayNum;
ToArray.ArrayMax = (SizeType)FromArray.ArrayMax;// 确保目标容器能够容纳源容器中的数据范围,特别是在分配器大小类型缩小的情况下。
// 检查目标容器的大小类型 USizeType 是否比源容器的大小类型小。
if constexpr (sizeof(USizeType) < sizeof(typename FromArrayType::USizeType))
{//检查目标容器的当前元素数量 (ArrayNum) 和最大容量 (ArrayMax) 是否与源容器相匹配。if (ToArray.ArrayNum != FromArray.ArrayNum || ToArray.ArrayMax != FromArray.ArrayMax){// Cast to USizeType first to prevent sign extension on negative sizes, producing unusually large values.UE::Core::Private::OnInvalidArrayNum((unsigned long long)(USizeType)ToArray.ArrayNum);}
}//----------------------------------------------------------------//
void UE::Core::Private::OnInvalidArrayNum(unsigned long long NewNum)
{UE_LOG(LogCore, Fatal, TEXT("Trying to resize TArray to an invalid size of %llu"), NewNum);for (;;);
}
UE::Core::Private::OnInvalidArrayNum Log使用了Fatal,引擎崩溃报错.
FromArray.ArrayNum = 0;
FromArray.ArrayMax = FromArray.AllocatorInstance.GetInitialCapacity();
FromArray.SlackTrackerNumChanged();
ToArray.SlackTrackerNumChanged();
FromArray的ArrayNum、ArrayMax设置为0,
总结
if constexpr (UE::Core::Private::CanMoveTArrayPointersBetweenArrayTypes<FromArrayType, ToArrayType>())
{// Movestatic_assert(std::is_same_v<TArray, ToArrayType>, "MoveOrCopy is expected to be called with the current array type as the destination");using FromAllocatorType = typename FromArrayType::AllocatorType;using ToAllocatorType = typename ToArrayType::AllocatorType;//判断传入的两个分配器能否移动if constexpr (TCanMoveBetweenAllocators<FromAllocatorType, ToAllocatorType>::Value){//移动分配器ToArray.AllocatorInstance.template MoveToEmptyFromOtherAllocator<FromAllocatorType>(FromArray.AllocatorInstance);}else{ToArray.AllocatorInstance.MoveToEmpty(FromArray.AllocatorInstance);}//拿来吧你ToArray .ArrayNum = (SizeType)FromArray.ArrayNum;ToArray .ArrayMax = (SizeType)FromArray.ArrayMax;// 确保目标容器能够容纳源容器中的数据范围,特别是在分配器大小类型缩小的情况下。// 检查目标容器的大小类型 USizeType 是否比源容器的大小类型小。if constexpr (sizeof(USizeType) < sizeof(typename FromArrayType::USizeType)){//检查目标容器的当前元素数量 (ArrayNum) 和最大容量 (ArrayMax) 是否与源容器相匹配。if (ToArray.ArrayNum != FromArray.ArrayNum || ToArray.ArrayMax != FromArray.ArrayMax){// Cast to USizeType first to prevent sign extension on negative sizes, producing unusually large values.// 触发崩溃UE::Core::Private::OnInvalidArrayNum((unsigned long long)(USizeType)ToArray.ArrayNum);}}//FromArray的ArrayNum、ArrayMax设为0FromArray.ArrayNum = 0;FromArray.ArrayMax = FromArray.AllocatorInstance.GetInitialCapacity();FromArray.SlackTrackerNumChanged();ToArray.SlackTrackerNumChanged();
}
析构
/** Destructor. */
~TArray()
{DestructItems(GetData(), ArrayNum);// note ArrayNum, ArrayMax and data pointer are not invalidated// they are left unchanged and use-after-destruct will see them the same as before destruct
}//--------------------------------//
template <typename ElementType,typename SizeTypeUE_REQUIRES(sizeof(ElementType) > 0 && std::is_trivially_destructible_v<ElementType>) // the sizeof here should improve the error messages we get when we try to call this function with incomplete types
>
FORCEINLINE void DestructItems(ElementType* Element, SizeType Count)
{
}template <typename ElementType,typename SizeTypeUE_REQUIRES(sizeof(ElementType) > 0 && !std::is_trivially_destructible_v<ElementType>) // the sizeof here should improve the error messages we get when we try to call this function with incomplete types
>
FORCENOINLINE void DestructItems(ElementType* Element, SizeType Count)
{while (Count){// We need a typedef here because VC won't compile the destructor call below if ElementType itself has a member called ElementTypetypedef ElementType DestructItemsElementTypeTypedef;Element->DestructItemsElementTypeTypedef::~DestructItemsElementTypeTypedef();++Element;--Count;}
}
is_trivially_destructible_v
检查给定类型的析构函数是否是平凡的(trivial)。
● 平凡析构函数:类型没有用户定义的析构函数,所有非静态成员和基类都有平凡析构函数。
○ 示例:基本数据类型、简单的POD(Plain Old Data)结构体、某些标准库类型(如 std::array 当元素类型为平凡类型时)。
● 非平凡析构函数:类型有用户定义的析构函数,或者其成员或基类有非平凡析构函数。
○ 示例:用户定义析构函数的类、包含标准库容器(如 std::vector)的类、从具有非平凡析构函数的基类派生的类。
#include <iostream>
#include <type_traits>
#include <string>
#include <array>// 基本数据类型
std::cout << "int: " << std::is_trivially_destructible_v<int> << std::endl;
// 输出: true// 简单结构体
struct Trivial {int x;double y;
};
std::cout << "Trivial: " << std::is_trivially_destructible_v<Trivial> << std::endl;
// 输出: true// 用户定义析构函数
struct NonTrivial {~NonTrivial() noexcept {}int x;double y;
};
std::cout << "NonTrivial: " << std::is_trivially_destructible_v<NonTrivial> << std::endl;
// 输出: false// 成员类型有非平凡析构函数
struct NonTrivialMember {std::string str;
};
std::cout << "NonTrivialMember: " << std::is_trivially_destructible_v<NonTrivialMember> << std::endl;
// 输出: false// 基类有非平凡析构函数
struct Base {~Base() noexcept {}
};
struct Derived : public Base {int x;
};
std::cout << "Derived: " << std::is_trivially_destructible_v<Derived> << std::endl;
// 输出: false// 标准库容器
std::vector<int> vec;
std::cout << "std::vector<int>: " << std::is_trivially_destructible_v<decltype(vec)> << std::endl;
// 输出: false// 标准库类型(当元素类型为平凡类型时)
std::array<int, 5> arr;
std::cout << "std::array<int, 5>: " << std::is_trivially_destructible_v<decltype(arr)> << std::endl;
// 输出: true
POD
根据C++标准,POD类型必须满足以下条件:
- 平凡的默认构造函数(Trivial Default Constructor):
○ 类型没有用户定义的默认构造函数,或者它的默认构造函数是平凡的(即编译器生成的默认构造函数)。 - 平凡的析构函数(Trivial Destructor):
○ 类型没有用户定义的析构函数,或者它的析构函数是平凡的(即编译器生成的析构函数)。 - 平凡的拷贝/移动操作(Trivial Copy/Move Operations):
○ 类型没有用户定义的拷贝构造函数、拷贝赋值运算符、移动构造函数或移动赋值运算符,或者这些操作是平凡的(即编译器生成的操作)。 - 标准布局(Standard Layout):
○ 类型的所有非静态数据成员都具有相同的访问控制(public, protected, private),并且基类和非静态数据成员不能有虚函数或虚基类。
○ 所有非静态数据成员都必须具有标准布局类型(standard-layout type)。
○ 类型没有虚函数或虚基类。
● std::is_trivial:检查类型是否有平凡的构造函数、析构函数和拷贝/移动操作。
● std::is_standard_layout:检查类型是否具有标准布局。
平凡类型:
#include <iostream>
#include <type_traits>struct Trivial {int x;double y;
};int main() {std::cout << std::boolalpha;// 检查是否为平凡类型std::cout << "Trivial is trivial: " << std::is_trivial_v<Trivial> << std::endl; // 输出: true// 检查是否为标准布局类型std::cout << "Trivial is standard layout: " << std::is_standard_layout_v<Trivial> << std::endl; // 输出: truereturn 0;
}
非平凡类型:
#include <iostream>
#include <type_traits>
#include <string>struct NonTrivial {~NonTrivial() noexcept {}int x;double y;
};struct NonStandardLayout {virtual void foo() {}int x;double y;
};struct NonTrivialMember {std::string str;int x;double y;
};int main() {std::cout << std::boolalpha;// 检查是否为平凡类型std::cout << "NonTrivial is trivial: " << std::is_trivial_v<NonTrivial> << std::endl; // 输出: falsestd::cout << "NonStandardLayout is trivial: " << std::is_trivial_v<NonStandardLayout> << std::endl; // 输出: falsestd::cout << "NonTrivialMember is trivial: " << std::is_trivial_v<NonTrivialMember> << std::endl; // 输出: false// 检查是否为标准布局类型std::cout << "NonStandardLayout is standard layout: " << std::is_standard_layout_v<NonStandardLayout> << std::endl; // 输出: falsereturn 0;
}
普通函数
Pop
Pop 函数是 TArray 类中的一个成员函数,用于从数组中移除并返回最后一个元素。
这个函数还提供了可选的收缩(shrink)功能,允许在移除元素后调整数组的容量以节省内存。
// * *
// *从数组中弹出元素。
// *
// * @param allowshrink是否允许在删除元素期间收缩数组。
// * @返回弹出的元素。
// *
ElementType Pop(EAllowShrinking AllowShrinking = EAllowShrinking::Yes)
{//确保数组至少有一个元素RangeCheck(0);//获取数组中最后一个元素(索引为 ArrayNum - 1)。//使用 MoveTempIfPossible 将其转换为右值引用,从而启用移动语义。//这可以避免不必要的拷贝操作,提高性能。ElementType Result = MoveTempIfPossible(GetData()[ArrayNum - 1]);//从数组中移除最后一个元素。RemoveAtImpl(ArrayNum - 1);if (AllowShrinking == EAllowShrinking::Yes){//释放未使用的内存。ResizeShrink();}return Result;
}template <typename T>
UE_INTRINSIC_CAST FORCEINLINE constexpr std::remove_reference_t<T>&& MoveTempIfPossible(T&& Obj) noexcept
{//定义一个类型别名 CastType,它是移除了引用属性后的 T 类型。using CastType = std::remove_reference_t<T>;//将传入的对象 Obj 转换为右值引用,并返回。//这样做的目的是启用移动语义。return (CastType&&)Obj;
}
MoveTempIfPossible 右值引用:
● 参数 T&& Obj:使用了通用引用(universal reference),可以接受左值引用或右值引用。
● 返回类型 std::remove_reference_t
将一个引用转换为右值引用(rvalue reference), 这是 UE 中 std::move 的等价实现。
与 MoveTemp 不同,它不会进行静态断言,因此在模板或宏中使用时更加灵活,不会中断编译。
因为在模板或宏中使用时,参数的具体类型可能不明确,但你仍然希望在可以的情况下利用移动语义,而不中断编译。
左值右值:
class MyClass
{
public:MyClass() = default;MyClass(MyClass&& other) noexcept {// 移动构造函数的实现}
};// 使用 MoveTempIfPossible
template <typename T>
void ProcessObject(T&& Obj)
{MyClass MovedObj = MoveTempIfPossible(Obj);
}int main()
{MyClass Obj;ProcessObject(Obj); // 左值引用ProcessObject(MyClass()); // 右值引用return 0;
}
● Obj 是一个左值引用。MoveTempIfPossible 会将其转换为右值引用,从而触发移动构造函数。
● MyClass() 是一个临时对象(右值)。MoveTempIfPossible 也会将其转换为右值引用,从而触发移动构造函数。
Add
在数组末尾添加一个新项,可能会重新分配整个数组以容纳它。
返回一个索引。
FORCEINLINE SizeType Add(const ElementType& Item)
{CheckAddress(&Item);return Emplace(Item);
}FORCEINLINE SizeType Add(ElementType&& Item)
{CheckAddress(&Item);return Emplace(MoveTempIfPossible(Item));
}// 检查指定的地址是否不是容器内元素的一部分。
// 用于在实现中检查引用参数是否会因为可能的重新分配而失效。
FORCEINLINE void CheckAddress(const ElementType* Addr) const
{checkf(Addr < GetData() || Addr >= (GetData() + ArrayMax), TEXT("Attempting to use a container element (%p) which already comes from the container being modified (%p, ArrayMax: %lld, ArrayNum: %lld, SizeofElement: %d)!"), Addr, GetData(), (long long)ArrayMax, (long long)ArrayNum, sizeof(ElementType));
}
Emplace
在数组的末尾构造一个新元素,可能会重新分配整个数组来容纳它。
返回一个索引。
/*** Constructs a new item at the end of the array, possibly reallocating the whole array to fit.** @param Args The arguments to forward to the constructor of the new item.* @return Index to the new item*/
template <typename... ArgsType>
FORCEINLINE SizeType Emplace(ArgsType&&... Args)
{//增加数组的大小,并返回新元素将要插入的位置索引。const SizeType Index = AddUninitialized();//GetData() 返回指向数组数据的指针,GetData() + Index 计算出新元素应放置的位置。//使用 ::new 进行原位构造(placement new),//直接在指定内存位置构造新对象,避免额外的拷贝或移动操作。//Forward<ArgsType>(Args)... 将参数完美转发给构造函数。::new((void*)(GetData() + Index)) ElementType(Forward<ArgsType>(Args)...);return Index;
}FORCEINLINE SizeType AddUninitialized()
{//确保数组内部状态的一致性,防止非法状态。//checkSlow((ArrayNum >= 0) & (ArrayMax >= ArrayNum)); CheckInvariants();// ArrayNum = ArrayNum + 1const USizeType OldNum = (USizeType)ArrayNum;const USizeType NewNum = OldNum + (USizeType)1;ArrayNum = (SizeType)NewNum;if (NewNum > (USizeType)ArrayMax){//如果 NewNum 超过了 ArrayMax,则调用 ResizeGrow 增加数组的容量。ResizeGrow((SizeType)OldNum);}else{//调用 SlackTrackerNumChanged 更新松弛(slack)信息。SlackTrackerNumChanged();}//返回 OldNum,即新元素将要插入的位置索引。return OldNum;
}
FPrivateToken
让TArray可以调用私有构造函数
class FMyType
{
private:struct FPrivateToken { explicit FPrivateToken() = default; };
public:// This has an equivalent access level to a private constructor,// as only friends of FMyType will have access to FPrivateToken,// but Emplace can legally call it since it's public.explicit FMyType(FPrivateToken, int32 Int, float Real, const TCHAR* String);
};TArray<FMyType> Arr;
Arr.Emplace(FMyType::FPrivateToken{}, 5, 3.14f, TEXT("Banana"));
Remove
SizeType Remove(const ElementType& Item)
{// 检查指定的地址是否不是容器内元素的一部分。CheckAddress(&Item);// Element is non-const to preserve compatibility with existing code with a non-const operator==() member function// 元素是非 const 的,以保持与现有代码中具有非 const operator==() 成员函数的兼容性return RemoveAll([&Item](ElementType& Element) { return Element == Item; });
}
template <class PREDICATE_CLASS>
SizeType RemoveAll(const PREDICATE_CLASS& Predicate)
{//如果为空,则直接返回 0,因为没有元素可以移除。//OriginalNum 记录数组的原始大小const SizeType OriginalNum = ArrayNum;if (!OriginalNum){return 0; // nothing to do, loop assumes one item so need to deal with this edge case here}//指向数组数据的指针。ElementType* Data = GetData();//WriteIndex ReadIndex分别用于写入和读取操作的索引。SizeType WriteIndex = 0;//不断增加,直到遇到与当前运行类型不同的元素。SizeType ReadIndex = 0;//根据第一个元素是否满足谓词,初始化 bNotMatch 变量。//如果bNotMatch = false,则说明匹配成功,在do-while中的if-else会执行false分支//else分支调用DestructItems,销毁元素//如果bNotMatch = true,匹配失败,if-else执行true分支,移动元素重新排序bool bNotMatch = !::Invoke(Predicate, Data[ReadIndex]); // use a ! to guarantee it can't be anything other than zero or onedo{//记录当前运行的起始位置。SizeType RunStartIndex = ReadIndex++;while (ReadIndex < OriginalNum && bNotMatch == !::Invoke(Predicate, Data[ReadIndex])){ReadIndex++;}//计算当前运行的长度。SizeType RunLength = ReadIndex - RunStartIndex;checkSlow(RunLength > 0);if (bNotMatch){// this was a non-matching run, we need to move itif (WriteIndex != RunStartIndex){//将这些元素移动到 WriteIndex 指向的位置RelocateConstructItems<ElementType>((void*)(Data + WriteIndex), Data + RunStartIndex, RunLength);}//更新 WriteIndex,使其指向下一个需要写入数据的位置。WriteIndex += RunLength;}else{// this was a matching run, delete it//销毁这些元素。DestructItems(Data + RunStartIndex, RunLength);}bNotMatch = !bNotMatch;} while (ReadIndex < OriginalNum);ArrayNum = WriteIndex;SlackTrackerNumChanged();//返回被移除的元素数量,即原始数组大小减去当前数组大小。return OriginalNum - ArrayNum;
}
● 模板参数 PREDICATE_CLASS:谓词类实例的类型。
● 参数 Predicate:一个谓词对象,用于判断某个元素是否需要被移除。
● 返回值 SizeType:返回被移除的元素数量。
- 获取原始数组大小:
○ 如果数组为空,直接返回 0。 - 初始化变量:
○ 获取数组数据指针 Data。
○ 初始化读取索引 ReadIndex 和写入索引 WriteIndex。
○ 根据第一个元素的状态初始化 bNotMatch 变量。 - 遍历数组:
○ 使用 do...while 循环遍历数组,每次处理一段连续的匹配或非匹配元素段(运行)。
○ 内层循环:找到一段连续的匹配或非匹配元素。
○ 处理非匹配运行:将这些元素移动到 WriteIndex 指向的位置,并更新 WriteIndex。
○ 处理匹配运行:销毁这些元素。
○ 切换 bNotMatch 的值,准备处理下一个运行。 - 更新数组大小:
○ 更新数组的有效元素数量 ArrayNum 为 WriteIndex。 - 返回结果:
○ 返回被移除的元素数量。
RemoveAt
void RemoveAt(SizeType Index, EAllowShrinking AllowShrinking = EAllowShrinking::Yes)
{RangeCheck(Index);RemoveAtImpl(Index);if (AllowShrinking == EAllowShrinking::Yes){// 根据需要 缩小数组大小ResizeShrink();}
}void RemoveAtImpl(SizeType Index)
{// 获取要移除元素的位置ElementType* Dest = GetData() + Index;// 销毁该位置的元素DestructItem(Dest);// Skip relocation in the common case that there is nothing to move.// 在没有东西可移动的情况下,跳过重定位。SizeType NumToMove = (ArrayNum - Index) - 1;if (NumToMove){RelocateConstructItems<ElementType>((void*)Dest, Dest + 1, NumToMove);//将一组元素从一个位置移动到另一个位置//传入参数是两个指针,所以TCanBitwiseRelocate_V通过,则调用的函数是://FMemory::Memmove(Dest, Source, sizeof(SourceElementType) * Count);}// 减少数组的有效元素数量--ArrayNum;// 更新松弛信息(如果有的话)SlackTrackerNumChanged();
}
namespace UE::Core::Private::MemoryOps
{template <typename DestinationElementType, typename SourceElementType>constexpr inline bool TCanBitwiseRelocate_V =std::is_same_v<DestinationElementType, SourceElementType> || (TIsBitwiseConstructible<DestinationElementType, SourceElementType>::Value &&std::is_trivially_destructible_v<SourceElementType>);
}
条件:
- 如果 DestinationElementType 和 SourceElementType 是相同的类型。
- DestinationElementType 可以从 SourceElementType 逐位构造,并且 SourceElementType 是平凡可析构的(trivially destructible)。
template <
typename DestinationElementType,
typename SourceElementType,
typename SizeType
UE_REQUIRES(sizeof(DestinationElementType) > 0 && sizeof(SourceElementType) > 0 && UE::Core::Private::MemoryOps::TCanBitwiseRelocate_V<DestinationElementType, SourceElementType>) // the sizeof here should improve the error messages we get when we try to call this function with incomplete types
>
FORCEINLINE void RelocateConstructItems(void* Dest, SourceElementType* Source, SizeType Count)
{static_assert(!std::is_const_v<SourceElementType>, "RelocateConstructItems: Source cannot be const");/* All existing UE containers seem to assume trivial relocatability (i.e. memcpy'able) of their members,* so we're going to assume that this is safe here. However, it's not generally possible to assume this* in general as objects which contain pointers/references to themselves are not safe to be trivially* relocated.** However, it is not yet possible to automatically infer this at compile time, so we can't enable* different (i.e. safer) implementations anyway. */FMemory::Memmove(Dest, Source, sizeof(SourceElementType) * Count);
}
template <
typename DestinationElementType,
typename SourceElementType,
typename SizeType
UE_REQUIRES(sizeof(DestinationElementType) > 0 && sizeof(SourceElementType) > 0 && !UE::Core::Private::MemoryOps::TCanBitwiseRelocate_V<DestinationElementType, SourceElementType>) // the sizeof here should improve the error messages we get when we try to call this function with incomplete types
>
FORCENOINLINE void RelocateConstructItems(void* Dest, SourceElementType* Source, SizeType Count)
{static_assert(!std::is_const_v<SourceElementType>, "RelocateConstructItems: Source cannot be const");//逐个元素地调用构造函数和析构函数进行移动。while (Count){// We need a typedef here because VC won't compile the destructor call below if SourceElementType itself has a member called SourceElementTypetypedef SourceElementType RelocateConstructItemsElementTypeTypedef;::new ((void*)Dest) DestinationElementType((SourceElementType&&)*Source);++(DestinationElementType*&)Dest;(Source++)->RelocateConstructItemsElementTypeTypedef::~RelocateConstructItemsElementTypeTypedef();--Count;}
}
DestructItem
template <typename ElementType>
FORCEINLINE void DestructItem(ElementType* Element)
{//如果 ElementType 的大小为 0,说明这是一个不完整的类型。//虽然这种情况不应该发生,但这段代码可以帮助生成更有意义的编译错误信息。if constexpr (sizeof(ElementType) == 0){// Should never get here, but this construct should improve the error messages we get when we try to call this function with incomplete types}//检测不是平凡可析构的else if constexpr (!std::is_trivially_destructible_v<ElementType>){// We need a typedef here because VC won't compile the destructor call below if ElementType itself has a member called ElementType//避免编译器在某些情况下(如 ElementType 自身有一个成员也叫 ElementType)//无法正确解析析构函数调用的问题。typedef ElementType DestructItemsElementTypeTypedef;//显式调用对象的析构函数,通过 typedef 确保析构函数调用的语法正确。Element->DestructItemsElementTypeTypedef::~DestructItemsElementTypeTypedef();}
}
该函数针对类型 T 进行了优化,不会动态分派析构函数调用(如果 T 的析构函数是虚函数)。
Top
返回顶部元素,即最后一个。
FORCEINLINE ElementType& Top() UE_LIFETIMEBOUND
{return Last();
}FORCEINLINE const ElementType& Top() const UE_LIFETIMEBOUND
{return Last();
}
Last
返回数组中从末尾开始计数的第 n 个元素.
IndexFromTheEnd(可选)索引从数组的末尾(默认= 0)
FORCEINLINE ElementType& Last(SizeType IndexFromTheEnd = 0) UE_LIFETIMEBOUND
{RangeCheck(ArrayNum - IndexFromTheEnd - 1);return GetData()[ArrayNum - IndexFromTheEnd - 1];
}//获取倒数第二个元素
int& secondLastElement = MyArray.Last(1);
Sort
注意:
● 如果你的数组包含原始指针,它们将在排序过程中自动解引用。
● 因此,你的数组将根据被指向的值进行排序,而不是指针本身的值。
● 如果这不是你想要的行为,请直接使用 Algo::Sort(MyArray)。
● 自动解引用行为不会发生在智能指针上。
void Sort()
{Algo::Sort(*this, TDereferenceWrapper<ElementType, TLess<>>(TLess<>()));
}
Algo::Sort传入了*this 和 一个谓词TDereferenceWrapper
TDereferenceWrapper又传入了 ElementType
和 TLess<>
template<typename T, class PREDICATE_CLASS>
struct TDereferenceWrapper
{const PREDICATE_CLASS& Predicate;TDereferenceWrapper(const PREDICATE_CLASS& InPredicate): Predicate(InPredicate) {}/** Pass through for non-pointer types */FORCEINLINE bool operator()(T& A, T& B) { return Predicate(A, B); } FORCEINLINE bool operator()(const T& A, const T& B) const { return Predicate(A, B); }
};template<typename T, class PREDICATE_CLASS>
struct TDereferenceWrapper<T*, PREDICATE_CLASS>
{const PREDICATE_CLASS& Predicate;TDereferenceWrapper(const PREDICATE_CLASS& InPredicate): Predicate(InPredicate) {}/** Dereference pointers */FORCEINLINE bool operator()(T* A, T* B) const {return Predicate(*A, *B); }
};
TLess<>
只是个比大小的仿函数,重载了()运算符 进行比大小
template <>
struct TLess<void>
{
template <typename T, typename U>
FORCEINLINE bool operator()(T&& A, U&& B) const
{return Forward<T>(A) < Forward<U>(B);
}
};
TDereferenceWrapper<ElementType, TLess<>> 下面简称为TD
在TD里面,此时 T = ElementType,PREDICATE_CLASS = TLess<>
并且把TLess<>存到一个Predicate变量里面。
TD 也重载了()运算符,在里面执行Predicate的()运算符,
此时Predicate是TLess<>类型,所以
TD(A,B)->TLess(A,B)->比大小
那么A和B是从哪来的?
void Sort()
{Algo::Sort(*this, TDereferenceWrapper<ElementType, TLess<>>(TLess<>()));
}//Algo::Sort
template <typename RangeType, typename PredicateType>
FORCEINLINE void Sort(RangeType&& Range, PredicateType Pred)
{IntroSort(Forward<RangeType>(Range), MoveTemp(Pred));
}template <typename RangeType, typename PredicateType>
FORCEINLINE void IntroSort(RangeType&& Range, PredicateType Predicate)
{AlgoImpl::IntroSortInternal(GetData(Range), GetNum(Range), FIdentityFunctor(), MoveTemp(Predicate));
}
TDereferenceWrapper的特化
抛开A、B不谈,这个Sort是怎么回事,为什么要如此复杂的层层包装,
一个括号运算符重载 套了 另一个括号运算符的重载,怎么会这样?
TDereferenceWrapper 的注释是:在排序函数中解引用指针类型的帮助类
Algo::Sort(ArrayInt, TLess<>());
● 这里直接使用了默认的 TLess<> 比较器。
● TLess<> 使用标准的 < 运算符进行比较。
● 适用于非指针类型(如 int, float, 自定义类等)
● 或智能指针(如 std::shared_ptr, std::unique_ptr),这些类型的比较不需要额外的解引用操作。
如果对 TArray<int> 进行TLess排序,排序将基于指针本身的内存地址,而不是指针所指向的整数值。
这就和期望不符合,要的是对int排序,实际上却对int排序了,排序对象不是值 而是指针。
Algo::Sort(ArrayInt, TDereferenceWrapper<ElementType, TLess<>>(TLess<>()));
● 这里使用了 TDereferenceWrapper 来包裹 TLess<>。
● TDereferenceWrapper 会根据元素类型的不同行为有所不同:
○ 如果元素是指针类型(如 int*),则会自动解引用指针,使得排序基于指针所指向的实际值。
○ 如果元素是非指针类型,则直接使用 < 运算符进行比较。
既然它能够根据 指针类型 或 非指针类型 随机应变,那么它是如何实现的?
它会根据TArray存放的元素类型ElementType 进行偏特化,
如果ElementType是int,那么就选取下方代码中的 第一个模板类.
如果ElementType是int*,那么就选取下方代码中的 第二个模板类.
第二个模板类是一个偏特化,当传入的T是一个T*样式的指针时,第二个模板类就会被选择.
template<typename T, class PREDICATE_CLASS>
struct TDereferenceWrapper
{const PREDICATE_CLASS& Predicate;TDereferenceWrapper(const PREDICATE_CLASS& InPredicate): Predicate(InPredicate) {}/** Pass through for non-pointer types */FORCEINLINE bool operator()(T& A, T& B) { return Predicate(A, B); } FORCEINLINE bool operator()(const T& A, const T& B) const { return Predicate(A, B); }
};template<typename T, class PREDICATE_CLASS>
struct TDereferenceWrapper<T*, PREDICATE_CLASS>
{const PREDICATE_CLASS& Predicate;TDereferenceWrapper(const PREDICATE_CLASS& InPredicate): Predicate(InPredicate) {}/** Dereference pointers */FORCEINLINE bool operator()(T* A, T* B) const {return Predicate(*A, *B); }
};
第二个模板类的括号运算符重载,对A、B进行了解引用。
到此就真相大白了,原来是这么设计的,
在编译时就根据TArray存放的数据类型 选择合适的模板类进行排序。
根据 指针类型/非指针类型,TDereferenceWrapper从下面的两条路径里选择:
TD(A,B)
->TLess(A,B)
->比大小
TD(*A,*B)
->TLess(A,B)
->比大小
Find
两个版本,
1.传入元素和索引,判断 索引处的元素 = 传入的元素
2.传入元素,遍历查找
FORCEINLINE bool Find(const ElementType& Item, SizeType& Index) const
{Index = this->Find(Item);return Index != INDEX_NONE;
}SizeType Find(const ElementType& Item) const
{// 返回指向第一个数组条目的指针const ElementType* RESTRICT Start = GetData();// 遍历数组,查找指定元素for (const ElementType* RESTRICT Data = Start, *RESTRICT DataEnd = Data + ArrayNum; Data != DataEnd; ++Data){if (*Data == Item){return static_cast<SizeType>(Data - Start);}}return INDEX_NONE;
}
在 Engine\Source\Runtime\Core\Public\Misc\CoreMiscDefines.h
中 有定义
enum {INDEX_NONE= -1};
所以查找失败时 返回-1.
RESTRICT
Engine\Source\Runtime\Core\Public\HAL\Platform.h
#define RESTRICT __restrict
类型限定符,用于指针声明,表示该指针是唯一指向其目标对象的指针。
当能确保某个指针不会与其他指针指向同一块内存时,
可以使用 __restrict
来帮助编译器生成更高效的代码。这在循环和函数参数传递中特别有用,
因为这些地方通常涉及大量的内存访问操作。
void foo(int *__restrict p, int *__restrict q)
{*p = 5; // 编译器知道 p 和 q 指向不同的对象*q = 10;
}
__restrict
告诉编译器指针 p
和 q
不会指向同一块内存区域。
因此,编译器可以假设对 *p
和 *q
的修改不会相互影响,从而进行更多的优化,例如重排序指令或减少不必要的内存屏障。
void processArrays(int *__restrict a, int *__restrict b, int *__restrict c, size_t n)
{for (size_t i = 0; i < n; ++i) {a[i] = b[i] + c[i];}
}
__restrict
关键字告诉编译器:
● a、b 和 c 指向的内存区域互不重叠。
● 因此,编译器可以假设对 a[i]、b[i] 和 c[i] 的访问不会互相干扰。
总结:
● __restrict 关键字:用于指示编译器优化内存访问,告诉编译器指针不会指向相同的内存区域。
● 作用:允许编译器进行更多的优化,如指令重排和向量化,从而提高代码性能。
● 注意事项:确保指针确实不会指向相同的内存区域,否则可能导致未定义行为。
FindByPredicate
class TGetValue
{
public:bool operator()(int x){return x%2 ==0;}
};TArray<int> ArrayInt;
ArrayInt = {2,3,5,7,50};
auto FindValue = *ArrayInt.FindByPredicate([](int x){return x==2;});
auto FindValue2 = *ArrayInt.FindByPredicate(TGetValue());
自定义匹配方法,可以传入仿函数、lambda函数 等等..
总之就是遍历TArray中的元素,执行传入的谓词,如果谓词的结果为true 则if(true) 返回Data.
template <typename Predicate>
ElementType* FindByPredicate(Predicate Pred)
{for (ElementType* RESTRICT Data = GetData(), *RESTRICT DataEnd = Data + ArrayNum; Data != DataEnd; ++Data){if (::Invoke(Pred, *Data)){return Data;}}return nullptr;
}
Invoke
用于统一调用不同类型的可调用对象(如普通函数指针、lambda 表达式、成员函数指针等)。
//通用可调用对象
template <typename FuncType, typename... ArgTypes>
FORCEINLINE auto Invoke(FuncType&& Func, ArgTypes&&... Args)
-> decltype(Forward<FuncType>(Func)(Forward<ArgTypes>(Args)...))
{return Forward<FuncType>(Func)(Forward<ArgTypes>(Args)...);
}//成员变量访问
template <typename ReturnType, typename ObjType, typename TargetType>
FORCEINLINE auto Invoke(ReturnType ObjType::*pdm, TargetType&& Target)
-> decltype(UE::Core::Private::DereferenceIfNecessary<ObjType>(Forward<TargetType>(Target), &Target).*pdm)
{return UE::Core::Private::DereferenceIfNecessary<ObjType>(Forward<TargetType>(Target), &Target).*pdm;
}//成员函数调用
template <
typename PtrMemFunType,
typename TargetType,
typename... ArgTypes,
typename ObjType = TMemberFunctionPtrOuter_T<PtrMemFunType>
>
FORCEINLINE auto Invoke(PtrMemFunType PtrMemFun, TargetType&& Target, ArgTypes&&... Args)
-> decltype((UE::Core::Private::DereferenceIfNecessary<ObjType>(Forward<TargetType>(Target), &Target).*PtrMemFun)(Forward<ArgTypes>(Args)...))
{return (UE::Core::Private::DereferenceIfNecessary<ObjType>(Forward<TargetType>(Target), &Target).*PtrMemFun)(Forward<ArgTypes>(Args)...);
}
三种形态,分别是 通用可调用对象、成员变量访问、成员函数调用
在前面的例子中,FindByPredicate传入了一个lambda表达式 ,又测试了重载了括号运算符的仿函数类.
这些情况都是通过第一个版本 通用可调用对象 进行调用的.
Forward
template <typename T>
UE_INTRINSIC_CAST FORCEINLINE constexpr T&& Forward(std::remove_reference_t<T>& Obj) noexcept
{return (T&&)Obj;
}template <typename T>
UE_INTRINSIC_CAST FORCEINLINE constexpr T&& Forward(std::remove_reference_t<T>&& Obj) noexcept
{return (T&&)Obj;
}
完美转发
用于在函数调用过程中保留参数的值类别(value category),即保持参数是左值还是右值的特性。
这使得函数可以将参数“完美”地转发给其他函数,而不会引入不必要的拷贝或转换。
● 左值引用:当传入左值时,Forward 返回左值引用。
● 右值引用:当传入右值时,Forward 返回右值引用。
万能引用 T&&
根据传入的参数类型推导为左值引用或右值引用。
template <typename T>
void foo(T&& param) {// ...
}
#include <iostream>void process(int x)
{std::cout << "Processing: " << x << std::endl;
}template <typename Func, typename... Args>
void wrapper(Func&& func, Args&&... args)
{// 使用 std::forward 进行完美转发std::forward<Func>(func)(std::forward<Args>(args)...);
}int main()
{int value = 42;// 左值//std::forward<int&>(value) 将 value 作为左值引用转发给 process。wrapper(process, value);// 右值//std::forward<int&&>(100) 将 100 作为右值引用转发给 process。wrapper(process, 100);return 0;
}
#include <memory>class MyClass
{
public:template <typename... Args>static std::unique_ptr<MyClass> create(Args&&... args) {return std::make_unique<MyClass>(std::forward<Args>(args)...);}private:MyClass(int x, double y) : x_(x), y_(y) {}int x_;double y_;
};int main()
{auto obj1 = MyClass::create(10, 3.14); // 右值int a = 20;auto obj2 = MyClass::create(a, 2.71); // 左值return 0;
}
● create 函数是一个工厂函数,接受任意数量的参数并使用 std::make_unique 创建 MyClass 对象。
● 使用 std::forward
Reserve
保留内存,使数组至少可以包含Number元素。
Number-分配后数组应该能够包含的元素数量。
FORCEINLINE void Reserve(SizeType Number)
{checkSlow(Number >= 0);if (Number < 0){// Cast to USizeType first to prevent sign extension on negative sizes, producing unusually large values.UE::Core::Private::OnInvalidArrayNum((unsigned long long)(USizeType)Number);}else if (Number > ArrayMax){ResizeTo(Number);}
}
Init
设置数组的大小,用给定的元素填充它。
Element-用来填充数组的元素。
Number-分配后数组应该能够包含的元素数量。
void Init(const ElementType& Element, SizeType Number)
{Empty(Number);for (SizeType Index = 0; Index < Number; ++Index){Add(Element);}
}
NumBytes
SIZE_T
,这是一个无符号整数类型,通常用于表示内存大小。
/** @returns Number of bytes used, excluding slack */
FORCEINLINE SIZE_T NumBytes() const
{return static_cast<SIZE_T>(ArrayNum) * sizeof(ElementType);//将 ArrayNum 转换为 SIZE_T 类型,//以确保与 sizeof(ElementType) 的乘积结果是 SIZE_T 类型,避免潜在的溢出问题。
}
计算数组当前使用的字节数,不包括未使用的额外空间(即所谓的“松弛”或“slack”)。
这个函数对于了解数组的实际内存占用非常有用。
TArray<int> MyArray;
MyArray.Add(10);
MyArray.Add(20);
MyArray.Add(30);// 计算当前使用的字节数
SIZE_T bytesUsed = MyArray.NumBytes();//NumBytes的计算过程
bytesUsed = static_cast<SIZE_T>(3) * sizeof(int); // 假设 sizeof(int) == 4
bytesUsed = 3 * 4;
bytesUsed = 12; // 当前使用的字节数是 12 字节
重载[]
访问数组中指定索引处的元素。
/*** Array bracket operator. Returns reference to element at given index.** @returns Reference to indexed element.*/
FORCEINLINE ElementType& operator[](SizeType Index) UE_LIFETIMEBOUND
{RangeCheck(Index);return GetData()[Index];
}FORCEINLINE void RangeCheck(SizeType Index) const
{CheckInvariants();// Template property, branch will be optimized outif constexpr (AllocatorType::RequireRangeCheck){checkf((Index >= 0) & (Index < ArrayNum),TEXT("Array index out of bounds: %lld into an array of size %lld"),(long long)Index, (long long)ArrayNum); // & for one branch}
}
GetData
返回指向数组数据的指针,即实际存储数组元素的内存块的首地址。
通过 GetData()[Index] 访问指定索引处的元素。
UE_LIFETIMEBOUND 宏
用于 静态代码分析 的注解宏,其核心作用是标记函数返回的指针或引用 依赖于某个对象的生命周期,帮助开发者避免悬垂指针(Dangling Pointer)错误。
● 编译警告/错误:
若返回的指针/引用的生命周期超出其依赖对象,静态分析工具(如 UE 的静态分析器或 Clang/GCC 的警告)会发出警告。
● 文档提示:
代码阅读者可以直观看出返回值需要谨慎处理生命周期。
● 该属性主要用于帮助静态分析工具检测潜在的生命周期问题。虽然它不会直接影响编译行为,但可以帮助开发者发现潜在的错误。
● 然而,过度使用可能会使代码变得冗长,因此需要合理使用。
宏定义在:
Engine/Source/Runtime/Core/Public/MSVC/MSVCPlatform.h
Engine/Source/Runtime/Core/Public/Clang/ClangPlatform.h
#ifdef __has_cpp_attribute#if __has_cpp_attribute(msvc::lifetimebound)#define UE_LIFETIMEBOUND [[msvc::lifetimebound]]#endif
#endif//---------------//
#define UE_LIFETIMEBOUND [[clang::lifetimebound]]
class FContainer {
public:int32* GetData() UE_LIFETIMEBOUND {return Data; // 返回指针,生命周期依赖 FContainer 实例}private:int32* Data;
};void Test() {int32* Ptr;{FContainer Container;Ptr = Container.GetData(); // 警告:Ptr 的生命周期超出 Container}*Ptr = 42; // 危险!悬垂指针
}
函数接受一个引用类型的参数,并希望确保该引用在其整个生命周期内都是有效的
void ProcessObject([[msvc::lifetimebound]] const MyClass& obj)
{// 处理对象
}
[[msvc::lifetimebound]] 标记了 obj 参数,表示该引用在其生命周期内应保持有效。
如果编译器或静态分析工具检测到 obj 的生命周期可能超出其有效范围,会发出相应的警告或错误。
对于返回值也可以使用 [[msvc::lifetimebound]] 来标记生命周期边界
[[msvc::lifetimebound]] MyClass& GetObject()
{static MyClass instance;return instance;
}
GetObject 返回一个静态对象的引用,并且使用 [[msvc::lifetimebound]] 标记返回值,确保返回的引用在其整个生命周期内都是有效的。
#include <iostream>class MyClass
{
public:MyClass(int v) : value(v) {}void PrintValue() const { std::cout << "Value: " << value << std::endl; }private:int value;
};// 使用 [[msvc::lifetimebound]] 标记函数参数
void ProcessObject([[msvc::lifetimebound]] const MyClass& obj)
{obj.PrintValue();
}int main()
{MyClass obj(42);ProcessObject(obj); // 正确使用,obj 在 ProcessObject 调用期间有效// 下面的代码会导致悬空引用,静态分析工具可能会发出警告// MyClass* ptr = new MyClass(100);// ProcessObject(*ptr);// delete ptr; // obj 已经被删除,ProcessObject 中的引用无效return 0;
}