Files
2025-12-10 14:38:26 -08:00

683 lines
25 KiB
C++

// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under *
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************
#ifndef STRING_BASE_H_083217454562342526
#define STRING_BASE_H_083217454562342526
#include <atomic>
#include <utility> //std::exchange
#include "string_tools.h"
//Zbase - a policy based string class optimizing performance and flexibility
namespace zen
{
/* Allocator Policy:
-----------------
void* allocate(size_t size) //throw std::bad_alloc
void deallocate(void* ptr)
size_t calcCapacity(size_t length) */
class AllocatorOptimalSpeed //exponential growth + min size
{
protected:
//::operator new/delete show same performance characterisics like malloc()/free()!
static void* allocate(size_t size) { return ::operator new (size); } //throw std::bad_alloc
static void deallocate(void* ptr) { ::operator delete (ptr); }
static size_t calcCapacity(size_t length) { return std::max<size_t>(16, std::max(length + length / 2, length)); }
//- size_t might overflow! => better catch here than return a too small size covering up the real error: a way too large length!
//- any growth rate should not exceed golden ratio: 1.618033989
};
class AllocatorOptimalMemory //no wasted memory, but more reallocations required when manipulating string
{
protected:
static void* allocate(size_t size) { return ::operator new (size); } //throw std::bad_alloc
static void deallocate(void* ptr) { ::operator delete (ptr); }
static size_t calcCapacity(size_t length) { return length; }
};
/* Storage Policy:
---------------
template <typename Char, //Character Type
class AP> //Allocator Policy
Char* create(size_t size)
Char* create(size_t size, size_t minCapacity)
Char* clone(Char* ptr)
void destroy(Char* ptr) //must handle "destroy(nullptr)"!
bool canWrite(const Char* ptr, size_t minCapacity) //needs to be checked before writing to "ptr"
size_t length(const Char* ptr)
void setLength(Char* ptr, size_t newLength) */
template <class Char, //Character Type
class AP> //Allocator Policy
class StorageDeepCopy : public AP
{
protected:
~StorageDeepCopy() {}
Char* create(size_t size) { return create(size, size); }
Char* create(size_t size, size_t minCapacity)
{
assert(size <= minCapacity);
const size_t newCapacity = AP::calcCapacity(minCapacity);
assert(newCapacity >= minCapacity);
Descriptor* const newDescr = static_cast<Descriptor*>(this->allocate(sizeof(Descriptor) + (newCapacity + 1) * sizeof(Char))); //throw std::bad_alloc
new (newDescr) Descriptor(size, newCapacity);
return reinterpret_cast<Char*>(newDescr + 1); //alignment note: "newDescr + 1" is Descriptor-aligned, which is larger than alignment for Char-array! => no problem!
}
Char* clone(Char* ptr)
{
const size_t len = length(ptr);
Char* newData = create(len); //throw std::bad_alloc
std::copy(ptr, ptr + len + 1, newData);
return newData;
}
void destroy(Char* ptr)
{
if (!ptr) return; //support "destroy(nullptr)"
Descriptor* const d = descr(ptr);
d->~Descriptor();
this->deallocate(d);
}
//this needs to be checked before writing to "ptr"
static bool canWrite(const Char* ptr, size_t minCapacity) { return minCapacity <= descr(ptr)->capacity; }
static size_t size(const Char* ptr) { return descr(ptr)->length; }
static void setLength(Char* ptr, size_t newLength)
{
assert(canWrite(ptr, newLength));
descr(ptr)->length = newLength;
}
private:
struct Descriptor
{
Descriptor(size_t len, size_t cap) :
length (static_cast<uint32_t>(len)),
capacity(static_cast<uint32_t>(cap)) {}
uint32_t length;
const uint32_t capacity; //allocated size without null-termination
};
static Descriptor* descr( Char* ptr) { return reinterpret_cast< Descriptor*>(ptr) - 1; }
static const Descriptor* descr(const Char* ptr) { return reinterpret_cast<const Descriptor*>(ptr) - 1; }
};
template <class Char, //Character Type
class AP> //Allocator Policy
class StorageRefCountThreadSafe : public AP
{
protected:
~StorageRefCountThreadSafe() {}
Char* create(size_t size) { return create(size, size); }
Char* create(size_t size, size_t minCapacity)
{
assert(size <= minCapacity);
if (minCapacity == 0) //perf: avoid memory allocation for empty string
{
++globalEmptyString.descr.refCount;
return &globalEmptyString.nullTerm;
}
const size_t newCapacity = AP::calcCapacity(minCapacity);
assert(newCapacity >= minCapacity);
Descriptor* const newDescr = static_cast<Descriptor*>(this->allocate(sizeof(Descriptor) + (newCapacity + 1) * sizeof(Char))); //throw std::bad_alloc
new (newDescr) Descriptor(size, newCapacity);
return reinterpret_cast<Char*>(newDescr + 1);
}
static Char* clone(Char* ptr)
{
++descr(ptr)->refCount;
return ptr;
}
void destroy(Char* ptr)
{
assert(ptr != reinterpret_cast<Char*>(0x1)); //detect double-deletion
if (!ptr) //support "destroy(nullptr)"
{
return;
}
Descriptor* const d = descr(ptr);
if (--(d->refCount) == 0) //operator--() is overloaded to decrement and evaluate in a single atomic operation!
{
d->~Descriptor();
this->deallocate(d);
}
}
static bool canWrite(const Char* ptr, size_t minCapacity) //needs to be checked before writing to "ptr"
{
const Descriptor* const d = descr(ptr);
assert(d->refCount > 0);
return d->refCount == 1 && minCapacity <= d->capacity;
}
static size_t size(const Char* ptr) { return descr(ptr)->length; }
static void setLength(Char* ptr, size_t newLength)
{
assert(canWrite(ptr, newLength));
descr(ptr)->length = static_cast<uint32_t>(newLength);
}
private:
struct Descriptor
{
constexpr Descriptor(size_t len, size_t cap) :
length (static_cast<uint32_t>(len)),
capacity(static_cast<uint32_t>(cap))
{
static_assert(decltype(refCount)::is_always_lock_free);
}
std::atomic<uint32_t> refCount{1}; //std:atomic is uninitialized by default!
uint32_t length;
const uint32_t capacity; //allocated size without null-termination
};
static Descriptor* descr( Char* ptr) { return reinterpret_cast< Descriptor*>(ptr) - 1; }
static const Descriptor* descr(const Char* ptr) { return reinterpret_cast<const Descriptor*>(ptr) - 1; }
struct GlobalEmptyString
{
Descriptor descr{0 /*length*/, 0 /*capacity*/};
Char nullTerm = 0;
};
static_assert(offsetof(GlobalEmptyString, nullTerm) - offsetof(GlobalEmptyString, descr) == sizeof(Descriptor), "no gap!");
static_assert(std::is_trivially_destructible_v<GlobalEmptyString>, "this memory needs to live forever");
inline static constinit GlobalEmptyString globalEmptyString; //constinit: dodge static initialization order fiasco!
};
template <class Char>
using DefaultStoragePolicy = StorageRefCountThreadSafe<Char, AllocatorOptimalSpeed>;
//################################################################################################################################################################
//perf note: interestingly StorageDeepCopy and StorageRefCountThreadSafe show same performance in FFS comparison
template <class Char, //Character Type
template <class> class SP = DefaultStoragePolicy> //Storage Policy
class Zbase : public SP<Char>
{
public:
Zbase();
Zbase(const Char* str) : Zbase(str, str + strLength(str)) {} //implicit conversion from a C-string!
Zbase(const Char* str, size_t len) : Zbase(str, str + len) {}
explicit Zbase(const std::basic_string_view<Char> view) : Zbase(view.begin(), view.end()) {}
Zbase(size_t count, Char fillChar);
template <class RandomAccessIterator>
Zbase(RandomAccessIterator first, RandomAccessIterator last);
Zbase(const Zbase& str);
Zbase(Zbase&& tmp) noexcept;
//explicit Zbase(Char ch); //dangerous if implicit: Char buffer[]; return buffer[0]; ups... forgot &, but not a compiler error! //-> non-standard extension!!!
~Zbase();
//operator const Char* () const; //NO implicit conversion to a C-string!! Many problems... one of them: if we forget to provide operator overloads, it'll just work with a Char*...
operator std::basic_string_view<Char>() const& noexcept { return {data(), size()}; }
//operator std::basic_string_view<Char>() const&& = delete; //=> probably a bug!
//STL accessors
using iterator = Char*;
using const_iterator = const Char*;
using reference = Char&;
using const_reference = const Char&;
using value_type = Char;
iterator begin();
iterator end ();
const_iterator begin () const { return rawStr_; }
const_iterator end () const { return rawStr_ + size(); }
const_iterator cbegin() const { return begin(); }
const_iterator cend () const { return end (); }
//std::string functions
size_t length() const { return size(); }
size_t size () const;
const Char* c_str() const { return rawStr_; } //C-string format with 0-termination
const Char* data() const { return &*begin(); }
/**/ Char* data() { return &*begin(); }
const Char& operator[](size_t pos) const;
/**/ Char& operator[](size_t pos);
bool empty() const { return size() == 0; }
void clear();
#if 0 //avoid redundant std::string API bloat!
size_t find (const Zbase& str, size_t pos = 0) const; //
size_t find (const Char* str, size_t pos = 0) const; //
size_t find (Char ch, size_t pos = 0) const; //returns "npos" if not found
size_t rfind(Char ch, size_t pos = npos) const; //
size_t rfind(const Char* str, size_t pos = npos) const; //
#endif
//Zbase& replace(size_t pos1, size_t n1, const Zbase& str);
void reserve(size_t minCapacity);
Zbase& assign(const Char* str, size_t len) { return assign(str, str + len); }
Zbase& append(const Char* str, size_t len) { return append(str, str + len); }
template <class RandomAccessIterator> Zbase& assign(RandomAccessIterator first, RandomAccessIterator last);
template <class RandomAccessIterator> Zbase& append(RandomAccessIterator first, RandomAccessIterator last);
void resize(size_t newSize, Char fillChar = 0);
void swap(Zbase& str) { std::swap(rawStr_, str.rawStr_); }
void push_back(Char val) { operator+=(val); } //STL access
void pop_back();
Zbase& operator=(Zbase&& tmp) noexcept;
Zbase& operator=(const Zbase& str);
Zbase& operator=(const Char* str) { return assign(str, strLength(str)); }
Zbase& operator=(Char ch) { return assign(&ch, 1); }
Zbase& operator+=(const Zbase& str) { return append(str.c_str(), str.size()); }
Zbase& operator+=(const Char* str) { return append(str, strLength(str)); }
Zbase& operator+=(Char ch) { return append(&ch, 1); }
Zbase& operator+=(const std::basic_string_view<Char> str) { return append(str.begin(), str.end()); }
static const size_t npos = static_cast<size_t>(-1);
inline friend Zbase operator+( const Char* lhs, const Zbase& rhs) { return Zbase(lhs, strLength(lhs), rhs.c_str(), rhs.size()); }
inline friend Zbase operator+( Char lhs, const Zbase& rhs) { return Zbase(&lhs, 1, rhs.c_str(), rhs.size()); }
inline friend Zbase operator+(const std::basic_string_view<Char> lhs, const Zbase& rhs) { return Zbase(lhs.data(), lhs.size(), rhs.c_str(), rhs.size()); }
private:
Zbase (int) = delete; //
Zbase(size_t count, int) = delete; //
Zbase& operator= (int) = delete; //detect usage errors by creating an intentional ambiguity with "Char"
Zbase& operator+= (int) = delete; //
void push_back (int) = delete; //
Zbase (std::nullptr_t) = delete;
Zbase(size_t count, std::nullptr_t) = delete;
Zbase& operator= (std::nullptr_t) = delete;
Zbase& operator+= (std::nullptr_t) = delete;
void push_back (std::nullptr_t) = delete;
//not part of std::string API => private:
Zbase(const Char* str1, size_t len1, const Char* str2, size_t len2);
//alternative: Zbase() + reserve() + 2 x append()
Char* rawStr_;
};
template <class Char, template <class> class SP> bool operator==(const Zbase<Char, SP>& lhs, const Zbase<Char, SP>& rhs);
template <class Char, template <class> class SP> bool operator==(const Zbase<Char, SP>& lhs, const Char* rhs);
template <class Char, template <class> class SP> inline bool operator==(const Char* lhs, const Zbase<Char, SP>& rhs) { return operator==(rhs, lhs); }
//follow convention + compare by unsigned char; alternative: std::lexicographical_compare_three_way + reinterpret_cast<const std::make_unsigned_t<Char>*>()
template <class Char, template <class> class SP> std::strong_ordering operator<=>(const Zbase<Char, SP>& lhs, const Zbase<Char, SP>& rhs) { return compareString(lhs, rhs); }
template <class Char, template <class> class SP> std::strong_ordering operator<=>(const Zbase<Char, SP>& lhs, const Char* rhs) { return compareString(lhs, rhs); }
template <class Char, template <class> class SP> std::strong_ordering operator<=>(const Char* lhs, const Zbase<Char, SP>& rhs) { return compareString(lhs, rhs); }
template <class Char, template <class> class SP> inline Zbase<Char, SP> operator+(const Zbase<Char, SP>& lhs, const Zbase<Char, SP>& rhs) { return Zbase<Char, SP>(lhs) += rhs; }
template <class Char, template <class> class SP> inline Zbase<Char, SP> operator+(const Zbase<Char, SP>& lhs, const Char* rhs) { return Zbase<Char, SP>(lhs) += rhs; }
template <class Char, template <class> class SP> inline Zbase<Char, SP> operator+(const Zbase<Char, SP>& lhs, Char rhs) { return Zbase<Char, SP>(lhs) += rhs; }
template <class Char, template <class> class SP> inline Zbase<Char, SP> operator+(const Zbase<Char, SP>& lhs, const std::basic_string_view<Char> rhs) { return Zbase<Char, SP>(lhs) += rhs; }
//don't use unified first argument but save one move-construction in the r-value case instead!
template <class Char, template <class> class SP> inline Zbase<Char, SP> operator+(Zbase<Char, SP>&& lhs, const Zbase<Char, SP>& rhs) { return std::move(lhs += rhs); } //the move *is* needed!!!
template <class Char, template <class> class SP> inline Zbase<Char, SP> operator+(Zbase<Char, SP>&& lhs, const Char* rhs) { return std::move(lhs += rhs); } //lhs, is an l-value parameter...
template <class Char, template <class> class SP> inline Zbase<Char, SP> operator+(Zbase<Char, SP>&& lhs, Char rhs) { return std::move(lhs += rhs); } //and not a local variable => no copy elision
template <class Char, template <class> class SP> inline Zbase<Char, SP> operator+(Zbase<Char, SP>&& lhs, const std::basic_string_view<Char> rhs) { return std::move(lhs += rhs); }
template <class Char, template <class> class SP> inline Zbase<Char, SP> operator+(const Zbase<Char, SP>&, int) = delete; //detect usage errors
template <class Char, template <class> class SP> inline Zbase<Char, SP> operator+(int, const Zbase<Char, SP>&) = delete; //
//################################# implementation ########################################
template <class Char, template <class> class SP> inline
Zbase<Char, SP>::Zbase()
{
rawStr_ = this->create(0);
rawStr_[0] = 0;
}
template <class Char, template <class> class SP>
template <class RandomAccessIterator> inline
Zbase<Char, SP>::Zbase(RandomAccessIterator first, RandomAccessIterator last)
{
rawStr_ = this->create(last - first);
*std::copy(first, last, rawStr_) = 0;
}
template <class Char, template <class> class SP> inline
Zbase<Char, SP>::Zbase(size_t count, Char fillChar)
{
rawStr_ = this->create(count);
std::fill(rawStr_, rawStr_ + count, fillChar);
rawStr_[count] = 0;
}
template <class Char, template <class> class SP> inline
Zbase<Char, SP>::Zbase(const Zbase<Char, SP>& str)
{
rawStr_ = this->clone(str.rawStr_);
}
template <class Char, template <class> class SP> inline
Zbase<Char, SP>::Zbase(Zbase<Char, SP>&& tmp) noexcept
{
rawStr_ = std::exchange(tmp.rawStr_, nullptr);
//usually nullptr would violate the class invarants, but it is good enough for the destructor!
//caveat: do not increment ref-count of an unshared string! We'd lose optimization opportunity of reusing its memory!
}
template <class Char, template <class> class SP> inline
Zbase<Char, SP>::Zbase(const Char* str1, size_t len1, const Char* str2, size_t len2)
{
rawStr_ = this->create(len1 + len2);
std::copy (str1, str1 + len1, rawStr_);
*std::copy(str2, str2 + len2, rawStr_ + len1) = 0;
}
template <class Char, template <class> class SP> inline
Zbase<Char, SP>::~Zbase()
{
static_assert(noexcept(this->~Zbase())); //has exception spec of compiler-generated destructor by default
this->destroy(rawStr_); //rawStr_ may be nullptr; see move constructor!
}
#if 0 //avoid redundant std::string API bloat!
template <class Char, template <class> class SP> inline
size_t Zbase<Char, SP>::find(const Zbase& str, size_t pos) const //returns "npos" if not found
{
assert(pos <= size());
const size_t len = size();
const Char* thisEnd = begin() + len; //respect embedded 0
const Char* it = searchFirst(begin() + std::min(pos, len), thisEnd,
str.begin(), str.end());
return it == thisEnd ? npos : it - begin();
}
template <class Char, template <class> class SP> inline
size_t Zbase<Char, SP>::find(const Char* str, size_t pos) const //returns "npos" if not found
{
assert(pos <= size());
const size_t len = size();
const Char* thisEnd = begin() + len; //respect embedded 0
const Char* it = searchFirst(begin() + std::min(pos, len), thisEnd,
str, str + strLength(str));
return it == thisEnd ? npos : it - begin();
}
template <class Char, template <class> class SP> inline
size_t Zbase<Char, SP>::find(Char ch, size_t pos) const //returns "npos" if not found
{
assert(pos <= size());
const size_t len = size();
const Char* thisEnd = begin() + len; //respect embedded 0
const Char* it = std::find(begin() + std::min(pos, len), thisEnd, ch);
return it == thisEnd ? npos : it - begin();
}
template <class Char, template <class> class SP> inline
size_t Zbase<Char, SP>::rfind(Char ch, size_t pos) const //returns "npos" if not found
{
assert(pos == npos || pos <= size());
const size_t len = size();
const Char* currEnd = begin() + (pos == npos ? len : std::min(pos + 1, len));
const Char* it = findLast(begin(), currEnd, ch);
return it == currEnd ? npos : it - begin();
}
template <class Char, template <class> class SP> inline
size_t Zbase<Char, SP>::rfind(const Char* str, size_t pos) const //returns "npos" if not found
{
assert(pos == npos || pos <= size());
const size_t strLen = strLength(str);
const size_t len = size();
const Char* currEnd = begin() + (pos == npos ? len : std::min(pos + strLen, len));
const Char* it = searchLast(begin(), currEnd,
str, str + strLen);
return it == currEnd ? npos : it - begin();
}
#endif
template <class Char, template <class> class SP> inline
void Zbase<Char, SP>::resize(size_t newSize, Char fillChar)
{
const size_t oldSize = size();
if (this->canWrite(rawStr_, newSize))
{
if (oldSize < newSize)
std::fill(rawStr_ + oldSize, rawStr_ + newSize, fillChar);
rawStr_[newSize] = 0;
this->setLength(rawStr_, newSize);
}
else
{
Char* newStr = this->create(newSize);
if (oldSize < newSize)
{
std::copy(rawStr_, rawStr_ + oldSize, newStr);
std::fill(newStr + oldSize, newStr + newSize, fillChar);
}
else
std::copy(rawStr_, rawStr_ + newSize, newStr);
newStr[newSize] = 0;
this->destroy(rawStr_);
rawStr_ = newStr;
}
}
template <class Char, template <class> class SP> inline
bool operator==(const Zbase<Char, SP>& lhs, const Zbase<Char, SP>& rhs)
{
return lhs.size() == rhs.size() && std::equal(lhs.begin(), lhs.end(), rhs.begin()); //respect embedded 0
}
template <class Char, template <class> class SP> inline
bool operator==(const Zbase<Char, SP>& lhs, const Char* rhs)
{
return lhs.size() == strLength(rhs) && std::equal(lhs.begin(), lhs.end(), rhs); //respect embedded 0
}
template <class Char, template <class> class SP> inline
size_t Zbase<Char, SP>::size() const
{
return SP<Char>::size(rawStr_);
}
template <class Char, template <class> class SP> inline
const Char& Zbase<Char, SP>::operator[](size_t pos) const
{
assert(pos < size()); //design by contract! no runtime check!
return rawStr_[pos];
}
template <class Char, template <class> class SP> inline
Char& Zbase<Char, SP>::operator[](size_t pos)
{
reserve(size()); //make unshared!
assert(pos < size()); //design by contract! no runtime check!
return rawStr_[pos];
}
template <class Char, template <class> class SP> inline
auto Zbase<Char, SP>::begin() -> iterator
{
reserve(size()); //make unshared!
return rawStr_;
}
template <class Char, template <class> class SP> inline
auto Zbase<Char, SP>::end() -> iterator
{
return begin() + size();
}
template <class Char, template <class> class SP> inline
void Zbase<Char, SP>::clear()
{
if (!empty())
{
if (this->canWrite(rawStr_, 0))
{
rawStr_[0] = 0; //keep allocated memory
this->setLength(rawStr_, 0); //
}
else
*this = Zbase();
}
}
template <class Char, template <class> class SP> inline
void Zbase<Char, SP>::reserve(size_t minCapacity) //make unshared and check capacity
{
if (!this->canWrite(rawStr_, minCapacity))
{
//allocate a new string
const size_t len = size();
Char* newStr = this->create(len, std::max(len, minCapacity)); //reserve() must NEVER shrink the string: logical const!
*std::copy(rawStr_, rawStr_ + len, newStr) = 0;
this->destroy(rawStr_);
rawStr_ = newStr;
}
}
template <class Char, template <class> class SP>
template <class RandomAccessIterator> inline
Zbase<Char, SP>& Zbase<Char, SP>::assign(RandomAccessIterator first, RandomAccessIterator last)
{
const size_t len = last - first;
if (this->canWrite(rawStr_, len))
{
*std::copy(first, last, rawStr_) = 0;
this->setLength(rawStr_, len);
}
else
*this = Zbase(first, last);
return *this;
}
template <class Char, template <class> class SP>
template <class RandomAccessIterator> inline
Zbase<Char, SP>& Zbase<Char, SP>::append(RandomAccessIterator first, RandomAccessIterator last)
{
const size_t len = last - first; //std::distance(first, last);
if (len > 0) //avoid making this string unshared for no reason
{
const size_t thisLen = size();
reserve(thisLen + len); //make unshared and check capacity
*std::copy(first, last, rawStr_ + thisLen) = 0;
this->setLength(rawStr_, thisLen + len);
}
return *this;
}
//don't use unifying assignment but save one move-construction in the r-value case instead!
template <class Char, template <class> class SP> inline
Zbase<Char, SP>& Zbase<Char, SP>::operator=(const Zbase<Char, SP>& str)
{
Zbase<Char, SP>(str).swap(*this);
return *this;
}
template <class Char, template <class> class SP> inline
Zbase<Char, SP>& Zbase<Char, SP>::operator=(Zbase<Char, SP>&& tmp) noexcept
{
//don't swap() but end rawStr_ life time immediately
this->destroy(rawStr_);
rawStr_ = std::exchange(tmp.rawStr_, nullptr);
return *this;
}
template <class Char, template <class> class SP> inline
void Zbase<Char, SP>::pop_back()
{
const size_t len = size();
assert(len > 0);
if (len > 0)
resize(len - 1);
}
}
//std::hash specialization in global namespace
template <class Char, template <class> class SP>
struct std::hash<zen::Zbase<Char, SP>>
{
using is_transparent = int; //allow heterogenous lookup!
template <class String>
size_t operator()(const String& str) const { return zen::hashString<size_t>(str); }
};
template <class Char, template <class> class SP>
struct std::equal_to<zen::Zbase<Char, SP>>
{
using is_transparent = int; //enable heterogenous lookup!
template <class String1, class String2>
bool operator()(const String1& lhs, const String2& rhs) const { return zen::equalString(lhs, rhs); }
};
#endif //STRING_BASE_H_083217454562342526