UE TArray
类型别名
模板类片段:
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 parameter
static_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 otherwise
using 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
{
// 设为0
ArrayMax = 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 SizeType
UE_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 SizeType
UE_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 OtherAllocator
UE_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 OtherAllocator
UE_REQUIRES(UE::Core::Private::TArrayElementsAreCompatible_V<ElementType, OtherElementType&&>)
>
FORCEINLINE explicit TArray(TArray<OtherElementType, OtherAllocator>&& Other)
{
MoveOrCopy(*this, Other, 0);
}
//---------------------------//
template <
typename OtherElementType
UE_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>())
{
// Move
static_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
{
// Copy
ToArray.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 allocator
enum { 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>())
{
// Move
static_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设为0
FromArray.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 SizeType
UE_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 SizeType
UE_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 ElementType
typedef 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;
// 输出: true
return 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;
// 输出: false
std::cout << "NonStandardLayout is trivial: " << std::is_trivial_v<NonStandardLayout> << std::endl;
// 输出: false
std::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;
// 输出: false
return 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 + 1
const 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 one
do
{
//记录当前运行的起始位置。
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 it
if (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 SourceElementType
typedef 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 out
if 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;
}

浙公网安备 33010602011771号