// // stuff library - A library of small C++ utilities // author: Xavier Thomas // // You should have received a copy of the MIT License // along with this software. If not, see . // // Immutable strings Python style // // Benefits: // - Performant short strings (SBO) // - Implicit sharing ('free' copy) // - Safe for concurent use // - Allocator support // TODO: // - make sure API is complete and on par with std::string // - C++11 compatibility // - micro-benchmarks // - use-case benchmarks #pragma once //#include //#include #include #include #include #include #include #include #include #include #include #include #include namespace stuff { namespace detail { // TODO: move generic memory stuff out // Empty type with alignement requirement, used to rebind allocators template struct alignas(Alignment) AlignedEmpty {}; // Count value to pass to Allocator for a given buffer length in bytes template [[nodiscard]] constexpr std::size_t alloc_count(std::size_t size) noexcept { // Integer division, round up return (size + sizeof(T) - 1) / sizeof(T); } template [[nodiscard]] std::byte* allocate_bytes(const Allocator& alloc, std::size_t size) { using Alloc_traits = typename std::allocator_traits; using Empty = AlignedEmpty; using AEmpty = typename Alloc_traits::template rebind_alloc; using AEmpty_traits = typename Alloc_traits::template rebind_traits; AEmpty alloc_empty(alloc); std::byte* p = reinterpret_cast( AEmpty_traits::allocate(alloc_empty, alloc_count(size))); // Check user provided allocator did respect alignment requirements assert( reinterpret_cast(p) % Alignment == 0 && "allocator does not respect alignment"); return p; } template void deallocate_bytes( const Allocator& alloc, std::byte* ptr, std::size_t size) { using Alloc_traits = typename std::allocator_traits; using Empty = AlignedEmpty; using AEmpty = typename Alloc_traits::template rebind_alloc; using AEmpty_traits = typename Alloc_traits::template rebind_traits; AEmpty alloc_empty(alloc); AEmpty_traits::deallocate( alloc_empty, reinterpret_cast(ptr), alloc_count(size)); } // END of generic memmory stuff template struct SharedStr { std::atomic ref_count = 0; [[no_unique_address]] Allocator allocator; CharT data[1]; // first string element for alignment SharedStr() = delete; explicit SharedStr(const Allocator& alloc) noexcept : allocator(alloc) {} // Size of allocated buffer in bytes for a given string length [[nodiscard]] static constexpr std::size_t alloc_size(std::size_t string_length) noexcept { // We need length + 1 but SharedStr already comes with space for // one elem return sizeof(SharedStr) + (string_length * sizeof(CharT)); } }; // Allocate and construct a char buffer and a reference count in one allocation template [[nodiscard]] SharedStr* allocate_shared_str_default_init(const Allocator& alloc, std::size_t length) { using alloc_traits = typename std::allocator_traits; using SS = SharedStr; SS* ssp = reinterpret_cast( allocate_bytes(alloc, SS::alloc_size(length))); // Construct SharedString // Equivalent to placement new: // SharedStr* result = new(p) SharedStr(); // but with custom allocator support using ASharedString = typename alloc_traits::template rebind_alloc; using ASharedString_traits = typename alloc_traits::template rebind_traits; ASharedString alloc_ss(alloc); ASharedString_traits::construct(alloc_ss, ssp, alloc); // Init lifetime of (construct) the rest of the string elements to avoid // UB but should not generate any code // No way do to default init using Allocator(traits), uses placement new for (std::size_t i = 1; i < length + 1; i++) { // alloc_traits::construct(alloc, &result->data()[i]); // value init ::new (&ssp->data[i]) CharT; // default init. Note: no () after type } return ssp; } template void deallocate_shared_str( SharedStr* ssp, std::size_t length) { using alloc_traits = std::allocator_traits; using SS = SharedStr; // Note: Should we use alloc_traits::destroy or should we call destructor? // For now we call destructor to stay symetric with allocate_shared_str for (std::size_t i = 1; i < length + 1; i++) { // alloc_traits::destroy(alloc, &ss->data[i]); ssp->data[i].~CharT(); } // ss->~SharedStr(); using ASharedString = typename alloc_traits::template rebind_alloc; using ASharedString_traits = typename alloc_traits::template rebind_traits; ASharedString alloc_ss(ssp->allocator); ASharedString_traits::destroy(alloc_ss, ssp); deallocate_bytes( ssp->allocator, reinterpret_cast(ssp), SS::alloc_size(length)); } template struct LongStr { static constexpr std::size_t SIZE_BITS = 8 * sizeof(std::size_t) - 1; static constexpr std::size_t MAX_SIZE = (1ul << SIZE_BITS) - 1; bool is_long_ : 1; std::size_t size_ : SIZE_BITS; SharedStr* ssp_; LongStr() = delete; LongStr(const Allocator& alloc, std::size_t size) : is_long_(true), size_(size & MAX_SIZE) { assert(size <= MAX_SIZE); ssp_ = allocate_shared_str_default_init(alloc, size); assert(ssp_ != nullptr); ++(ssp_->ref_count); } LongStr(const LongStr& other) noexcept : is_long_(true), size_(other.size_), ssp_(other.ssp_) { // We could allow the copy of 'moved from objects' but we choose // to not pay the price of a branch here, for now. assert(ssp_ != nullptr); ++(ssp_->ref_count); } LongStr(LongStr&& other) noexcept : is_long_(true), size_(other.size_), ssp_(other.ssp_) { other.ssp_ = nullptr; } ~LongStr() noexcept { if (ssp_ != nullptr && --(ssp_->ref_count) == 0) { deallocate_shared_str(ssp_, size_); ssp_ = nullptr; } } }; template struct ShortStr { static constexpr std::size_t ALLOCATOR_SIZE = std::is_empty_v ? 0 : sizeof(Allocator); static constexpr std::size_t SIZE_BITS = 8 * sizeof(std::uint8_t) - 1; static constexpr std::uint8_t MAX_SIZE = (1ul << SIZE_BITS) - 1; static constexpr std::size_t MIN_BUFFER_SIZE = sizeof(LongStr) - 1 - ALLOCATOR_SIZE; static constexpr std::size_t BUFFER_SIZE = N == 0 ? MIN_BUFFER_SIZE : N; static_assert(N <= MAX_SIZE, "SBO buffer size too big"); static_assert(BUFFER_SIZE <= MAX_SIZE, "You found a bug in libstuff, contact the developpers"); bool is_long_ : 1; std::uint8_t size_ : SIZE_BITS; std::array data_; [[no_unique_address]] Allocator allocator_; ShortStr() = delete; explicit ShortStr(const Allocator& alloc, std::uint8_t size = 0) noexcept : is_long_(false), size_(size & MAX_SIZE), allocator_(alloc) { assert(size < BUFFER_SIZE); } }; template struct UnionStr { using TShort = ShortStr; using TLong = LongStr; static_assert( N != static_cast(0) || sizeof(TShort) == sizeof(TLong), "You found a bug in libstuff, contact the developpers"); union { TLong long_; TShort short_; }; UnionStr() = delete; explicit UnionStr(const Allocator& alloc) noexcept : short_(alloc) {} UnionStr(const UnionStr& other) noexcept { if (other.is_long()) { new (&long_) TLong(other.long_); } else { new (&short_) TShort(other.short_); } } UnionStr(UnionStr&& other) noexcept { if (other.is_long()) { new (&long_) TLong(std::move(other.long_)); } else { new (&short_) TShort(other.short_); } } UnionStr(const Allocator& alloc, std::size_t size) { if (size >= TShort::BUFFER_SIZE) { new (&long_) TLong(alloc, size); } else { new (&short_) TShort(alloc, static_cast(size)); } } ~UnionStr() { if (is_long()) { long_.~LongStr(); } else { short_.~ShortStr(); } } UnionStr& operator=(UnionStr& other) = delete; UnionStr& operator=(UnionStr&& other) = delete; [[nodiscard]] bool is_long() const noexcept { return long_.is_long_; } [[nodiscard]] std::size_t length() const noexcept { return is_long() ? long_.size_ : short_.size_; } [[nodiscard]] CharT* data() noexcept { return is_long() ? long_.ssp_->data : short_.data_.data(); } [[nodiscard]] const CharT* data() const noexcept { return is_long() ? long_.ssp_->data : short_.data_.data(); } [[nodiscard]] Allocator get_allocator() const noexcept { return is_long() ? long_.ssp_->allocator : short_.allocator_; } }; } // namespace detail template < typename CharT = char, typename Traits = std::char_traits, std::uint8_t N = 0, typename Allocator = std::allocator> class String { public: // types using value_type = CharT; using pointer = CharT*; using const_pointer = const CharT*; using reference = CharT&; using const_reference = const CharT&; using const_iterator = const CharT*; using iterator = const_iterator; using reverse_iterator = std::reverse_iterator; using const_reverse_iterator = std::reverse_iterator; using size_type = std::size_t; using difference_type = std::ptrdiff_t; using allocator_type = Allocator; static_assert( std::is_same_v, "Invalid Allocator::value_type"); static_assert( std::is_standard_layout>::value && std::is_standard_layout< detail::ShortStr>::value && std::is_standard_layout< detail::UnionStr>::value, "You found a bug in libstuff, contact the developpers"); private: using ViewT = std::basic_string_view; public: // Maximum size of a string that still fits the small buffer and // thus for which allocation is optimized away static constexpr std::size_t SMALL_STRING_SIZE = detail::ShortStr::BUFFER_SIZE - 1; static constexpr std::size_t npos = ViewT::npos; public: String() noexcept(noexcept(Allocator())) : String(Allocator()) {} explicit String(const Allocator& alloc) noexcept : str_(alloc) { Traits::assign(str_.data()[0], CharT()); } explicit String(const CharT* str, const Allocator& alloc = Allocator()) : str_(alloc, std::strlen(str)) { std::memcpy(str_.data(), str, str_.length() + 1); } String( const CharT* str, std::size_t count, const Allocator& alloc = Allocator()) : str_(alloc, count) { std::memcpy(str_.data(), str, count); Traits::assign(str_.data()[count], CharT()); } // Do not "select" allocator to see if it propagates on copy. // Since we do not duplicate the allocated container area // but share it, it seems logical to always propagate the allocator. String(const String& other) noexcept : str_(other.str_) {} // Note: we could avoid copy if (alloc == other.allocator_) String(const String& other, const Allocator& alloc) noexcept : str_(alloc, other.length()) { std::memcpy(str_.data(), other.data(), other.length() + 1); } String(String&& other) = default; // Note: we could avoid copy if (alloc == other.allocator_) String(String&& other, const Allocator& alloc) noexcept : str_(alloc, other.length()) { std::memcpy(str_.data(), other.data(), other.length() + 1); } String& operator=(String& other) = delete; String& operator=(String&& other) = delete; [[nodiscard]] Allocator get_allocator() const noexcept { return str_.get_allocator(); } // Iterators [[nodiscard]] const_iterator begin() const noexcept { return cbegin(); } [[nodiscard]] const_iterator cbegin() const noexcept { return data(); } [[nodiscard]] const_iterator end() const noexcept { return cend(); } [[nodiscard]] const_iterator cend() const noexcept { return &data()[length()]; } [[nodiscard]] const_reverse_iterator rbegin() const noexcept { return crbegin(); } [[nodiscard]] const_reverse_iterator crbegin() const noexcept { return const_reverse_iterator(&data()[length()]); } [[nodiscard]] const_reverse_iterator rend() const noexcept { return crend(); } [[nodiscard]] const_reverse_iterator crend() const noexcept { return const_reverse_iterator(data()); } // Capacity [[nodiscard]] std::size_t length() const noexcept { return str_.length(); } [[nodiscard]] std::size_t size() const noexcept { return length(); } [[nodiscard]] bool empty() const noexcept { return length() == 0; } [[nodiscard]] bool is_empty() const noexcept { return length() == 0; } // Access [[nodiscard]] const CharT& operator[](std::size_t pos) const { return data()[pos]; } [[nodiscard]] const CharT& at(std::size_t pos) const { check_bounds(pos); return data()[pos]; } [[nodiscard]] constexpr const_reference front() const { return data()[0]; } [[nodiscard]] constexpr const_reference back() const { return data()[length() - 1]; } [[nodiscard]] const CharT* c_str() const { return data(); } [[nodiscard]] const CharT* data() const { return str_.data(); } // Comparisson [[nodiscard]] int compare(const String& other) const noexcept { return compare(static_cast(other)); } [[nodiscard]] int compare(const ViewT str) const noexcept { return static_cast(this).compare(str); } // Implicit conversion operator std::basic_string_view() const noexcept { return ViewT(data(), size()); } // Operations [[nodiscard]] bool starts_with(const ViewT sv) const noexcept { return static_cast(*this).starts_with(sv); } [[nodiscard]] bool starts_with(CharT c) const noexcept { return static_cast(*this).starts_with(c); } [[nodiscard]] bool starts_with(const CharT* s) const { return starts_with(ViewT(s)); } [[nodiscard]] bool ends_with(const ViewT sv) const noexcept { return static_cast(*this).ends_with(sv); } [[nodiscard]] bool ends_with(CharT c) const noexcept { return static_cast(*this).ends_with(c); } [[nodiscard]] bool ends_with(const CharT* s) const { return ends_with(ViewT(s)); } [[nodiscard]] bool contains(const ViewT sv) const noexcept { return static_cast(*this).find(sv) != npos; } [[nodiscard]] bool contains(CharT c) const noexcept { return static_cast(*this).find(c) != npos; } [[nodiscard]] bool contains(const CharT* s) const { return contains(ViewT(s)); } [[nodiscard]] ViewT substr_view(std::size_t pos = 0, std::size_t count = npos) const noexcept { return static_cast(*this).substr(pos, count); } // NOTE: We do like std::string and use a default constructed allocator // instead of copying this->allocator_ [[nodiscard]] String substr(std::size_t pos = 0, std::size_t count = npos) const { return substr(Allocator(), pos, count); } [[nodiscard]] String substr( const Allocator& alloc, std::size_t pos = 0, std::size_t count = npos) const { const auto sv = substr_view(pos, count); return String(sv.data(), sv.length(), alloc); } [[nodiscard]] std::size_t find(ViewT sv, std::size_t pos = 0) const noexcept { return static_cast(*this).find(sv, pos); } // TODO more find() rfind() ... // TODO replace() std::string style but const and returning a new string // Python-like replace // NOTE: Default constructed alloc, NOT a copy of this->allocator_ [[nodiscard]] String replace( ViewT old, ViewT n, std::size_t count = std::numeric_limits::max()) const { return replace(Allocator(), old, n, count); } [[nodiscard]] String replace( const Allocator alloc, ViewT old, ViewT n, std::size_t count = std::numeric_limits::max()) const { std::size_t pos = 0; std::size_t c = 0; while (c < count) { pos = find(old, pos); if (pos == npos) { break; } c++; pos = pos + old.length(); } std::ptrdiff_t diff = static_cast(old.length()) - static_cast(n.length()); auto len = static_cast(length()) - static_cast(c) * diff; String out(static_cast(len), alloc); CharT* it_out = out.str_.data(); c = 0; pos = 0; while (c < count) { std::size_t found_pos = find(old, pos); if (found_pos == npos) { break; } std::size_t l = found_pos - pos; std::memcpy(it_out, &data()[pos], l); it_out += l; std::memcpy(it_out, n.data(), n.length()); it_out += n.length(); pos = found_pos + old.length(); c++; } std::size_t l = length() - pos; std::memcpy(it_out, &data()[pos], l); it_out += l; Traits::assign(it_out[0], CharT()); return out; } // Python-like join // NOTE: Default constructed alloc, NOT a copy of this->allocator_ template // requires (... && std::is_convertible_v) [[nodiscard]] String join(const Args&... args) const { return join(Allocator(), args...); } template // requires (... && std::is_convertible_v) [[nodiscard]] String join(const Allocator& alloc, const Args&... args) const { constexpr size_t n_args = sizeof...(Args); std::array args_array{{args...}}; return join(alloc, args_array.cbegin(), args_array.cend()); } private: // TODO make const if possible // join()/replace() make this difficult to do detail::UnionStr str_; // Alocate if necessary and set size but requires data to be filled later explicit String( const std::size_t size, const Allocator& alloc = Allocator()) : str_(alloc, size) {} void check_bounds(size_t index) const { if (index >= this->size()) { // const auto what = std::format( // "String::check_bounds: " // "index (which is {}) " // ">= this->size() (which is {})", // index, // this->size()); std::array what; std::snprintf( what.data(), 128, "String::check_bounds: " "index (which is %lu) " ">= this->size() (which is %lu)", static_cast(index), static_cast(this->size())); throw std::out_of_range(what.data()); } } template // requires std::is_same_v< // std::iterator_traits::value_type, // ViewT> [[nodiscard]] String join(const Allocator& alloc, Iter first, Iter last) const { std::size_t len = 0; std::size_t n = 0; for (auto it = first; it != last; it++) { len += it->length(); n++; } len += (n - 1) * length(); String ret(len, alloc); auto it = first; CharT* dest = ret.str_.data(); std::memcpy(dest, it->data(), it->length()); dest += it->length(); it++; for (; it != last; it++) { std::memcpy(dest, data(), length()); dest += length(); std::memcpy(dest, it->data(), it->length()); dest += it->length(); } Traits::assign(dest[0], CharT()); return ret; } }; // Non-member functions // Comparisson template constexpr bool operator==( const String& lhs, const String& rhs) { using ViewT = std::basic_string_view; return static_cast(lhs) == static_cast(rhs); } template constexpr bool operator==( const String& lhs, const CharT* rhs) { using ViewT = std::basic_string_view; return static_cast(lhs) == static_cast(rhs); } template constexpr bool operator==( const CharT* lhs, const String& rhs) { using ViewT = std::basic_string_view; return static_cast(lhs) == static_cast(rhs); } // C++20 // template // constexpr auto operator <=> ( // const String& lhs, // const String& rhs) { // return static_cast>(lhs) // <=> std::basic_string_view(rhs); // } // // template // constexpr auto operator <=> ( // const String& lhs, // const CharT* rhs ) { // using ViewT = std::basic_string_view; // auto comp = lhs.compare(ViewT(rhs)); // if (comp < 0 ) { return std::strong_ordering::less; } // else if (comp == 0) { return std::strong_oredering::equal; } // else { return std::strong_ordering::greater; } // } // Pre C+20 template bool operator!=( const String& lhs, const String& rhs) { return static_cast>(lhs) != static_cast>(rhs); } template bool operator<( const String& lhs, const String& rhs) { return static_cast>(lhs) < static_cast>(rhs); } template bool operator<=( const String& lhs, const String& rhs) { return static_cast>(lhs) <= static_cast>(rhs); } template bool operator>( const String& lhs, const String& rhs) { return static_cast>(lhs) > static_cast>(rhs); } template bool operator>=( const String& lhs, const String& rhs) { return static_cast>(lhs) >= static_cast>(rhs); } template constexpr bool operator!=( const String& lhs, const CharT* rhs) { using ViewT = std::basic_string_view; return static_cast(lhs) != static_cast(rhs); } template constexpr bool operator!=( const CharT* lhs, const String& rhs) { using ViewT = std::basic_string_view; return static_cast(lhs) != static_cast(rhs); } template constexpr bool operator<( const String& lhs, const CharT* rhs) { using ViewT = std::basic_string_view; return static_cast(lhs) < static_cast(rhs); } template constexpr bool operator<( const CharT* lhs, const String& rhs) { using ViewT = std::basic_string_view; return static_cast(lhs) < static_cast(rhs); } template constexpr bool operator<=( const String& lhs, const CharT* rhs) { using ViewT = std::basic_string_view; return static_cast(lhs) <= static_cast(rhs); } template constexpr bool operator<=( const CharT* lhs, const String& rhs) { using ViewT = std::basic_string_view; return static_cast(lhs) <= static_cast(rhs); } template constexpr bool operator>( const String& lhs, const CharT* rhs) { using ViewT = std::basic_string_view; return static_cast(lhs) > static_cast(rhs); } template constexpr bool operator>( const CharT* lhs, const String& rhs) { using ViewT = std::basic_string_view; return static_cast(lhs) > static_cast(rhs); } template constexpr bool operator>=( const String& lhs, const CharT* rhs) { using ViewT = std::basic_string_view; return static_cast(lhs) >= static_cast(rhs); } template constexpr bool operator>=( const CharT* lhs, const String& rhs) { using ViewT = std::basic_string_view; return static_cast(lhs) >= static_cast(rhs); } // End of pre-C++20 comparisson stuff // iostream support template std::ostream& operator<<(std::ostream& os, const String& s) { os << static_cast>(s); return os; } namespace pmr { template < typename CharT = char, typename Traits = std::char_traits, uint8_t N = 0> using String = ::stuff::String>; } // namespace pmr } // namespace stuff