wip(standalone): paused by the owner -- hashed maps + inline allocator (no speedup yet), streaming in progress
Hashed TMap/TSet and real TInlineAllocator storage did not move level 0 (72.1 -> 74.1 s, within noise): the slowness is elsewhere. StreamingWorld.h started (first build hit an int64/uint64 mismatch), library plan drafted (SHARED_CPP_LIBRARY_PLAN.md), before-normal captures saved. Uncompiled/unmeasured parts: streaming. Resume by forking session 01a0fd2e-3c3b-7050-b4e9-628fdcc4d553. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
05f62cff36
commit
3f300b912a
8 files changed
+997
-100
No files matched your search
+296
-19
@@ -19,7 +19,9 @@
|
||||
#include <initializer_list>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <memory_resource>
|
||||
#include <mutex>
|
||||
#include <new>
|
||||
#include <optional>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
@@ -88,13 +90,192 @@ template<class Signature> using TFunctionRef = std::function<Signature>;
|
||||
template<class T> constexpr T&& Forward(std::remove_reference_t<T>& Value){return std::forward<T>(Value);}
|
||||
template<class T> constexpr T&& Forward(std::remove_reference_t<T>&& Value){return std::forward<T>(Value);}
|
||||
|
||||
struct FCompatAllocationHeader
|
||||
{
|
||||
std::pmr::memory_resource* Pool=nullptr;
|
||||
void* Base=nullptr;
|
||||
std::size_t Bytes=0;
|
||||
std::size_t Alignment=0;
|
||||
};
|
||||
|
||||
inline std::pmr::memory_resource* FCompatThreadPool()
|
||||
{
|
||||
// A synchronized pool can return a worker's allocations from the result-drain thread. Keep
|
||||
// one process-lifetime pool per allocating thread, matching Unreal's per-thread small bins.
|
||||
thread_local auto* Pool=new std::pmr::synchronized_pool_resource();
|
||||
return Pool;
|
||||
}
|
||||
|
||||
template<class T>
|
||||
class FCompatAllocator
|
||||
{
|
||||
public:
|
||||
using value_type=T;
|
||||
using is_always_equal=std::true_type;
|
||||
using propagate_on_container_move_assignment=std::true_type;
|
||||
FCompatAllocator() noexcept=default;
|
||||
template<class U> FCompatAllocator(const FCompatAllocator<U>&) noexcept {}
|
||||
|
||||
[[nodiscard]] T* allocate(std::size_t Count)
|
||||
{
|
||||
if(Count>std::numeric_limits<std::size_t>::max()/sizeof(T))throw std::bad_array_new_length();
|
||||
const std::size_t Bytes=Count*sizeof(T);
|
||||
const std::size_t Alignment=std::max(alignof(T),alignof(FCompatAllocationHeader));
|
||||
const std::size_t Total=Bytes+sizeof(FCompatAllocationHeader)+Alignment;
|
||||
std::pmr::memory_resource* Pool=Bytes<=65536?FCompatThreadPool():nullptr;
|
||||
void* Base=Pool?Pool->allocate(Total,Alignment): ::operator new(Total,std::align_val_t(Alignment));
|
||||
void* Candidate=static_cast<std::byte*>(Base)+sizeof(FCompatAllocationHeader);
|
||||
std::size_t Space=Total-sizeof(FCompatAllocationHeader);
|
||||
void* Data=std::align(alignof(T),Bytes,Candidate,Space);
|
||||
if(!Data)
|
||||
{
|
||||
if(Pool)Pool->deallocate(Base,Total,Alignment);
|
||||
else ::operator delete(Base,std::align_val_t(Alignment));
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
auto* Header=reinterpret_cast<FCompatAllocationHeader*>(static_cast<std::byte*>(Data)-sizeof(FCompatAllocationHeader));
|
||||
::new (Header) FCompatAllocationHeader{Pool,Base,Total,Alignment};
|
||||
return static_cast<T*>(Data);
|
||||
}
|
||||
|
||||
void deallocate(T* Data,std::size_t) noexcept
|
||||
{
|
||||
if(!Data)return;
|
||||
auto* Header=reinterpret_cast<FCompatAllocationHeader*>(reinterpret_cast<std::byte*>(Data)-sizeof(FCompatAllocationHeader));
|
||||
const FCompatAllocationHeader Allocation=*Header;
|
||||
Header->~FCompatAllocationHeader();
|
||||
if(Allocation.Pool)Allocation.Pool->deallocate(Allocation.Base,Allocation.Bytes,Allocation.Alignment);
|
||||
else ::operator delete(Allocation.Base,std::align_val_t(Allocation.Alignment));
|
||||
}
|
||||
|
||||
template<class U> bool operator==(const FCompatAllocator<U>&)const noexcept{return true;}
|
||||
template<class U> bool operator!=(const FCompatAllocator<U>&)const noexcept{return false;}
|
||||
};
|
||||
|
||||
template<class Allocator> struct FCompatInlineCapacity { static constexpr std::size_t Value=0; };
|
||||
template<int32 N> struct FCompatInlineCapacity<TInlineAllocator<N>>
|
||||
{ static constexpr std::size_t Value=static_cast<std::size_t>(N); };
|
||||
|
||||
template<class T,std::size_t InlineCount,class Allocator=FCompatAllocator<T>>
|
||||
class FCompatSmallVector
|
||||
{
|
||||
public:
|
||||
using value_type=T;
|
||||
using iterator=T*;
|
||||
using const_iterator=const T*;
|
||||
using reference=T&;
|
||||
using const_reference=const T&;
|
||||
|
||||
FCompatSmallVector()=default;
|
||||
FCompatSmallVector(const FCompatSmallVector& Other){assign(Other.begin(),Other.end());}
|
||||
FCompatSmallVector(FCompatSmallVector&& Other) noexcept
|
||||
{
|
||||
if(Other.bHeap){Heap=std::move(Other.Heap);bHeap=true;Other.bHeap=false;Other.InlineSize=0;}
|
||||
else for(T& Value:Other)emplace_back(std::move(Value));
|
||||
Other.clear();
|
||||
}
|
||||
FCompatSmallVector& operator=(const FCompatSmallVector& Other)
|
||||
{if(this!=&Other)assign(Other.begin(),Other.end());return *this;}
|
||||
FCompatSmallVector& operator=(FCompatSmallVector&& Other) noexcept
|
||||
{
|
||||
if(this==&Other)return *this;
|
||||
clear();
|
||||
if(Other.bHeap){Heap=std::move(Other.Heap);bHeap=true;Other.bHeap=false;Other.InlineSize=0;}
|
||||
else for(T& Value:Other)emplace_back(std::move(Value));
|
||||
Other.clear();return *this;
|
||||
}
|
||||
~FCompatSmallVector(){clear();}
|
||||
|
||||
T* data(){return bHeap?Heap.data():InlineData();}
|
||||
const T* data()const{return bHeap?Heap.data():InlineData();}
|
||||
std::size_t size()const{return bHeap?Heap.size():InlineSize;}
|
||||
bool empty()const{return size()==0;}
|
||||
std::size_t capacity()const{return bHeap?Heap.capacity():InlineCount;}
|
||||
std::size_t heap_capacity()const{return bHeap?Heap.capacity():0;}
|
||||
iterator begin(){return data();} iterator end(){return data()+size();}
|
||||
const_iterator begin()const{return data();} const_iterator end()const{return data()+size();}
|
||||
reference operator[](std::size_t I){return data()[I];}
|
||||
const_reference operator[](std::size_t I)const{return data()[I];}
|
||||
reference back(){return data()[size()-1];} const_reference back()const{return data()[size()-1];}
|
||||
|
||||
void reserve(std::size_t Count)
|
||||
{
|
||||
if(Count<=capacity())return;
|
||||
if(bHeap){Heap.reserve(Count);return;}
|
||||
std::vector<T,Allocator> NewHeap;NewHeap.reserve(Count);
|
||||
for(std::size_t I=0;I<InlineSize;++I)NewHeap.emplace_back(std::move(InlineData()[I]));
|
||||
DestroyInline();Heap=std::move(NewHeap);InlineSize=0;bHeap=true;
|
||||
}
|
||||
|
||||
void clear()noexcept
|
||||
{if(bHeap)Heap.clear();else DestroyInline();}
|
||||
void resize(std::size_t Count)
|
||||
{
|
||||
const std::size_t Old=size();
|
||||
if(Count<Old)
|
||||
{
|
||||
if(bHeap)Heap.resize(Count);
|
||||
else{while(InlineSize>Count)InlineData()[--InlineSize].~T();}
|
||||
return;
|
||||
}
|
||||
reserve(Count);
|
||||
while(size()<Count)emplace_back();
|
||||
}
|
||||
void assign(std::size_t Count,const T& Value)
|
||||
{clear();reserve(Count);for(std::size_t I=0;I<Count;++I)emplace_back(Value);}
|
||||
template<class It> void assign(It First,It Last)
|
||||
{clear();for(;First!=Last;++First)emplace_back(*First);}
|
||||
void push_back(const T& Value){emplace_back(Value);}
|
||||
void push_back(T&& Value){emplace_back(std::move(Value));}
|
||||
template<class... Args> reference emplace_back(Args&&... ArgsIn)
|
||||
{
|
||||
if(bHeap){Heap.emplace_back(std::forward<Args>(ArgsIn)...);return Heap.back();}
|
||||
if(InlineSize<InlineCount)
|
||||
{
|
||||
T* Result=::new (static_cast<void*>(InlineData()+InlineSize)) T(std::forward<Args>(ArgsIn)...);
|
||||
++InlineSize;return *Result;
|
||||
}
|
||||
reserve(std::max<std::size_t>(InlineCount?InlineCount*2:1,InlineSize+1));
|
||||
Heap.emplace_back(std::forward<Args>(ArgsIn)...);return Heap.back();
|
||||
}
|
||||
void pop_back(){if(bHeap)Heap.pop_back();else InlineData()[--InlineSize].~T();}
|
||||
template<class It> iterator insert(iterator Position,It First,It Last)
|
||||
{
|
||||
const std::size_t Offset=static_cast<std::size_t>(Position-begin());
|
||||
assert(Offset==size());
|
||||
for(;First!=Last;++First)emplace_back(*First);
|
||||
return begin()+Offset;
|
||||
}
|
||||
iterator erase(iterator First,iterator Last)
|
||||
{
|
||||
const std::size_t Start=static_cast<std::size_t>(First-begin());
|
||||
const std::size_t Finish=static_cast<std::size_t>(Last-begin());
|
||||
const std::size_t Old=size();
|
||||
if(Finish<=Start)return begin()+Start;
|
||||
const std::size_t Removed=Finish-Start;
|
||||
for(std::size_t I=Finish;I<Old;++I)data()[I-Removed]=std::move(data()[I]);
|
||||
if(bHeap)Heap.resize(Old-Removed);
|
||||
else while(InlineSize>Old-Removed)InlineData()[--InlineSize].~T();
|
||||
return begin()+Start;
|
||||
}
|
||||
|
||||
private:
|
||||
T* InlineData(){return reinterpret_cast<T*>(InlineBytes);}
|
||||
const T* InlineData()const{return reinterpret_cast<const T*>(InlineBytes);}
|
||||
void DestroyInline()noexcept{while(InlineSize>0)InlineData()[--InlineSize].~T();}
|
||||
alignas(T) std::byte InlineBytes[(InlineCount?InlineCount:1)*sizeof(T)]{};
|
||||
std::vector<T,Allocator> Heap;
|
||||
std::size_t InlineSize=0;
|
||||
bool bHeap=false;
|
||||
};
|
||||
|
||||
template<class T, class Allocator = FDefaultAllocator>
|
||||
class TArray
|
||||
{
|
||||
public:
|
||||
using value_type = T;
|
||||
using FStored = std::conditional_t<std::is_same_v<T,bool>,uint8,T>;
|
||||
using FStorage = std::vector<FStored>;
|
||||
using FStorage = FCompatSmallVector<FStored,FCompatInlineCapacity<Allocator>::Value,FCompatAllocator<FStored>>;
|
||||
using iterator = typename FStorage::iterator;
|
||||
using const_iterator = typename FStorage::const_iterator;
|
||||
using reference = typename FStorage::reference;
|
||||
@@ -133,7 +314,7 @@ public:
|
||||
const T* Find(const T& Value) const { const int32 I=IndexOfByKey(Value); return I==INDEX_NONE?nullptr:&Values[I]; }
|
||||
T* GetData() { return reinterpret_cast<T*>(Values.data()); }
|
||||
const T* GetData() const { return reinterpret_cast<const T*>(Values.data()); }
|
||||
std::size_t GetAllocatedSize() const { return Values.capacity() * sizeof(T); }
|
||||
std::size_t GetAllocatedSize() const { return Values.heap_capacity() * sizeof(T); }
|
||||
T& Last() { return Values.back(); }
|
||||
const T& Last() const { return Values.back(); }
|
||||
T Pop(bool = false) { T Out=std::move(Values.back()); Values.pop_back(); return Out; }
|
||||
@@ -151,42 +332,133 @@ private:
|
||||
|
||||
template<class K, class V> struct TPair { K Key{}; V Value{}; TPair()=default; TPair(const K& K0,const V& V0):Key(K0),Value(V0){}; };
|
||||
|
||||
template<class K>
|
||||
struct FCompatKeyHash
|
||||
{
|
||||
std::size_t operator()(const K& Key) const noexcept
|
||||
{
|
||||
if constexpr (std::is_integral_v<K> || std::is_enum_v<K>)
|
||||
return std::hash<K>{}(Key);
|
||||
else if constexpr (requires { GetTypeHash(Key); })
|
||||
return static_cast<std::size_t>(GetTypeHash(Key));
|
||||
else
|
||||
static_assert(sizeof(K) == 0, "Standalone TMap/TSet key type needs GetTypeHash");
|
||||
}
|
||||
};
|
||||
|
||||
template<class K, class V, class Allocator = FDefaultAllocator>
|
||||
class TMap
|
||||
{
|
||||
public:
|
||||
int32 Num() const { return static_cast<int32>(Pairs.size()); }
|
||||
bool IsEmpty() const { return Pairs.empty(); }
|
||||
void Reserve(int32 N) { Pairs.reserve(static_cast<std::size_t>(std::max(0,N))); }
|
||||
void Reset() { Pairs.clear(); }
|
||||
void Reserve(int32 N)
|
||||
{
|
||||
const size_t Count=static_cast<size_t>(std::max(0,N));
|
||||
Pairs.reserve(Count);
|
||||
const size_t Needed=std::max<size_t>(8,std::bit_ceil(std::max<size_t>(1,(Count*10+6)/7)));
|
||||
if(Buckets.size()<Needed)RebuildIndex(Needed);
|
||||
}
|
||||
void Reset()
|
||||
{
|
||||
Pairs.clear();Tombstones=0;
|
||||
if(++Generation==0){for(FBucket& B:Buckets)B.Generation=0;Generation=1;}
|
||||
}
|
||||
void Empty(int32 = 0) { Reset(); }
|
||||
bool Contains(const K& Key) const { return FindPair(Key) != Pairs.end(); }
|
||||
V* Find(const K& Key) { auto I=FindPair(Key); return I==Pairs.end()?nullptr:&I->Value; }
|
||||
const V* Find(const K& Key) const { auto I=FindPair(Key); return I==Pairs.end()?nullptr:&I->Value; }
|
||||
V& FindOrAdd(const K& Key) { if (V* P=Find(Key)) return *P; Pairs.emplace_back(Key,V{}); return Pairs.back().Value; }
|
||||
V& Add(const K& Key,const V& Value) { if (V* P=Find(Key)) { *P=Value; return *P; } Pairs.emplace_back(Key,Value); return Pairs.back().Value; }
|
||||
V& Add(const K& Key,V&& Value) { if (V* P=Find(Key)) { *P=std::move(Value); return *P; } Pairs.emplace_back(Key,std::move(Value)); return Pairs.back().Value; }
|
||||
int32 Remove(const K& Key) { auto I=FindPair(Key); if(I==Pairs.end())return 0; Pairs.erase(I);return 1; }
|
||||
bool Contains(const K& Key) const { return FindIndex(Key)!=INDEX_NONE; }
|
||||
V* Find(const K& Key) { const int32 I=FindIndex(Key);return I==INDEX_NONE?nullptr:&Pairs[static_cast<size_t>(I)].Value; }
|
||||
const V* Find(const K& Key) const { const int32 I=FindIndex(Key);return I==INDEX_NONE?nullptr:&Pairs[static_cast<size_t>(I)].Value; }
|
||||
V& FindOrAdd(const K& Key) { if (V* P=Find(Key)) return *P; return Add(Key,V{}); }
|
||||
V& Add(const K& Key,const V& Value) { if (V* P=Find(Key)) { *P=Value; return *P; } PrepareInsert();const int32 I=static_cast<int32>(Pairs.size());Pairs.emplace_back(Key,Value);InsertIndex(Key,I);return Pairs.back().Value; }
|
||||
V& Add(const K& Key,V&& Value) { if (V* P=Find(Key)) { *P=std::move(Value); return *P; } PrepareInsert();const int32 I=static_cast<int32>(Pairs.size());Pairs.emplace_back(Key,std::move(Value));InsertIndex(Key,I);return Pairs.back().Value; }
|
||||
int32 Remove(const K& Key)
|
||||
{
|
||||
const int32 I=FindIndex(Key);if(I==INDEX_NONE)return 0;
|
||||
MarkRemoved(Key);const int32 Last=static_cast<int32>(Pairs.size()-1);
|
||||
if(I!=Last){Pairs[static_cast<size_t>(I)]=std::move(Pairs.back());UpdateIndex(Pairs[static_cast<size_t>(I)].Key,I);}
|
||||
Pairs.pop_back();
|
||||
if(Tombstones>Pairs.size()&&Tombstones>Buckets.size()/8)RebuildIndex(Buckets.size());
|
||||
return 1;
|
||||
}
|
||||
V& operator[](const K& Key) { return FindOrAdd(Key); }
|
||||
const V& operator[](const K& Key) const { const V* P=Find(Key); assert(P); return *P; }
|
||||
auto begin(){return Pairs.begin();} auto end(){return Pairs.end();}
|
||||
auto begin()const{return Pairs.begin();} auto end()const{return Pairs.end();}
|
||||
private:
|
||||
using It=typename std::vector<TPair<K,V>>::iterator;
|
||||
using CIt=typename std::vector<TPair<K,V>>::const_iterator;
|
||||
It FindPair(const K& Key){return std::find_if(Pairs.begin(),Pairs.end(),[&](auto& P){return P.Key==Key;});}
|
||||
CIt FindPair(const K& Key)const{return std::find_if(Pairs.begin(),Pairs.end(),[&](auto& P){return P.Key==Key;});}
|
||||
std::vector<TPair<K,V>> Pairs;
|
||||
struct FBucket { uint32 Generation=0;int32 Index=INDEX_NONE; };
|
||||
void PrepareInsert()
|
||||
{
|
||||
const size_t Count=Pairs.size()+1;
|
||||
const size_t Needed=std::max<size_t>(8,std::bit_ceil(std::max<size_t>(1,(Count*10+6)/7)));
|
||||
if(Buckets.size()<Needed||Tombstones>Buckets.size()/8)RebuildIndex(std::max(Needed,Buckets.size()));
|
||||
}
|
||||
int32 FindIndex(const K& Key)const
|
||||
{
|
||||
if(Buckets.empty())return INDEX_NONE;
|
||||
size_t Slot=FCompatKeyHash<K>{}(Key)&(Buckets.size()-1);
|
||||
for(size_t Probe=0;Probe<Buckets.size();++Probe)
|
||||
{
|
||||
const FBucket& B=Buckets[Slot];
|
||||
if(B.Generation!=Generation)return INDEX_NONE;
|
||||
if(B.Index>=0&&Pairs[static_cast<size_t>(B.Index)].Key==Key)return B.Index;
|
||||
Slot=(Slot+1)&(Buckets.size()-1);
|
||||
}
|
||||
return INDEX_NONE;
|
||||
}
|
||||
void InsertIndex(const K& Key,int32 PairIndex)
|
||||
{
|
||||
size_t Slot=FCompatKeyHash<K>{}(Key)&(Buckets.size()-1),FirstTombstone=Buckets.size();
|
||||
for(size_t Probe=0;Probe<Buckets.size();++Probe)
|
||||
{
|
||||
FBucket& B=Buckets[Slot];
|
||||
if(B.Generation!=Generation){if(FirstTombstone!=Buckets.size())Slot=FirstTombstone;Buckets[Slot]={Generation,PairIndex};return;}
|
||||
if(B.Index==INDEX_NONE&&FirstTombstone==Buckets.size())FirstTombstone=Slot;
|
||||
Slot=(Slot+1)&(Buckets.size()-1);
|
||||
}
|
||||
assert(FirstTombstone!=Buckets.size());Buckets[FirstTombstone]={Generation,PairIndex};
|
||||
}
|
||||
void MarkRemoved(const K& Key)
|
||||
{
|
||||
size_t Slot=FCompatKeyHash<K>{}(Key)&(Buckets.size()-1);
|
||||
for(size_t Probe=0;Probe<Buckets.size();++Probe)
|
||||
{
|
||||
FBucket& B=Buckets[Slot];if(B.Generation!=Generation)return;
|
||||
if(B.Index>=0&&Pairs[static_cast<size_t>(B.Index)].Key==Key){B.Index=INDEX_NONE;++Tombstones;return;}
|
||||
Slot=(Slot+1)&(Buckets.size()-1);
|
||||
}
|
||||
}
|
||||
void UpdateIndex(const K& Key,int32 PairIndex)
|
||||
{
|
||||
size_t Slot=FCompatKeyHash<K>{}(Key)&(Buckets.size()-1);
|
||||
for(size_t Probe=0;Probe<Buckets.size();++Probe)
|
||||
{
|
||||
FBucket& B=Buckets[Slot];if(B.Generation!=Generation)return;
|
||||
if(B.Index>=0&&Pairs[static_cast<size_t>(B.Index)].Key==Key){B.Index=PairIndex;return;}
|
||||
Slot=(Slot+1)&(Buckets.size()-1);
|
||||
}
|
||||
}
|
||||
void RebuildIndex(size_t Capacity)
|
||||
{
|
||||
Capacity=std::max<size_t>(8,std::bit_ceil(Capacity));Buckets.assign(Capacity,FBucket{});Generation=1;Tombstones=0;
|
||||
for(int32 I=0;I<static_cast<int32>(Pairs.size());++I)InsertIndex(Pairs[static_cast<size_t>(I)].Key,I);
|
||||
}
|
||||
std::vector<TPair<K,V>,FCompatAllocator<TPair<K,V>>> Pairs;
|
||||
std::vector<FBucket,FCompatAllocator<FBucket>> Buckets;
|
||||
uint32 Generation=1;
|
||||
size_t Tombstones=0;
|
||||
};
|
||||
|
||||
template<class T, class Allocator = FDefaultAllocator>
|
||||
class TSet
|
||||
{
|
||||
public:
|
||||
int32 Num()const{return Values.Num();} void Reset(){Values.Reset();} void Empty(){Reset();}
|
||||
bool Contains(const T& V)const{return Values.Contains(V);} void Add(const T& V){Values.AddUnique(V);} void Add(T&& V){if(!Contains(V))Values.Add(std::move(V));}
|
||||
int32 Num()const{return Values.Num();} void Reset(){Values.Reset();Indices.Reset();} void Empty(){Reset();}
|
||||
void Reserve(int32 N){Values.Reserve(N);Indices.Reserve(N);}
|
||||
bool Contains(const T& V)const{return Indices.Contains(V);}
|
||||
void Add(const T& V){if(!Indices.Contains(V)){const int32 I=Values.Add(V);Indices.Add(V,I);}}
|
||||
void Add(T&& V){if(!Indices.Contains(V)){const int32 I=Values.Add(std::move(V));Indices.Add(Values[I],I);}}
|
||||
auto begin(){return Values.begin();}auto end(){return Values.end();}auto begin()const{return Values.begin();}auto end()const{return Values.end();}
|
||||
private:TArray<T,Allocator> Values;
|
||||
private:TArray<T,Allocator> Values;TMap<T,int32,Allocator> Indices;
|
||||
};
|
||||
|
||||
template<class T> class TUniquePtr
|
||||
@@ -225,6 +497,11 @@ public:
|
||||
static FString Join(const TArray<FString>& Parts, std::string_view Delimiter){FString Result;for(int32 I=0;I<Parts.Num();++I){if(I)Result+=Delimiter;Result+=Parts[I];}return Result;}
|
||||
static FString Printf(const char* Format,...){char Buf[4096];va_list A;va_start(A,Format);int N=std::vsnprintf(Buf,sizeof(Buf),Format,A);va_end(A);return N<0?FString{}:FString(Buf,static_cast<std::size_t>(std::min<int>(N,sizeof(Buf)-1)));}
|
||||
};
|
||||
inline uint32 GetTypeHash(const FString& Value)
|
||||
{
|
||||
const std::string_view View(Value.data(),Value.size());
|
||||
return static_cast<uint32>(std::hash<std::string_view>{}(View));
|
||||
}
|
||||
inline const char* operator*(const FString& S){return S.c_str();}
|
||||
using FName = FString;
|
||||
constexpr const char* NAME_None = "";
|
||||
|
||||
Reference in new issue
Block a user