/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include <algorithm> #include <string> #include <string_view> namespace android::mediautils { /* * FixedString is a stack allocatable string buffer that supports * simple appending of other strings and string_views. * * It is designed for no-malloc operation when std::string * small buffer optimization is insufficient. * * To keep code small, use asStringView() for operations on this. * * Notes: * 1) Appending beyond the internal buffer size results in truncation. * * Alternatives: * 1) If you want a sharable copy-on-write string implementation, * consider using the legacy android::String8(). * 2) Using std::string with a fixed stack allocator may suit your needs, * but exception avoidance is tricky. * 3) Using C++20 ranges https://en.cppreference.com/w/cpp/ranges if you don't * need backing store. Be careful about allocation with ranges. * * Good small sizes are multiples of 16 minus 2, e.g. 14, 30, 46, 62. * * Implementation Notes: * 1) No iterators or [] for FixedString - please convert to std::string_view. * 2) For small N (e.g. less than 62), consider a change to always zero fill and * potentially prevent always zero terminating (if one only does append). * * Possible arguments to create/append: * 1) A FixedString. * 2) A single char. * 3) A char * pointer. * 4) A std::string. * 5) A std::string_view (or something convertible to it). * * Example: * * FixedString s1(std::string("a")); // ctor * s1 << "bc" << 'd' << '\n'; // streaming append * s1 += "hello"; // += append * ASSERT_EQ(s1, "abcd\nhello"); */ template <uint32_t N> struct FixedString { // Find the best size type. using strsize_t = std::conditional_t<(N > 255), uint32_t, uint8_t>; // constructors FixedString() { // override default buffer_[0] = '\0'; } FixedString(const FixedString& other) { // override default. copyFrom<N>(other); } // The following constructor is not explicit to allow // FixedString<8> s = "abcd"; template <typename ...Types> FixedString(Types&&... args) { append(std::forward<Types>(args)...); } // copy assign (copyFrom checks for equality and returns *this). FixedString& operator=(const FixedString& other) { // override default. return copyFrom<N>(other); } template <typename ...Types> FixedString& operator=(Types&&... args) { size_ = 0; return append(std::forward<Types>(args)...); } // operator equals bool operator==(const char *s) const { return strncmp(c_str(), s, capacity() + 1) == 0; } bool operator==(const std::string_view s) const { return size() == s.size() && memcmp(data(), s.data(), size()) == 0; } template <uint32_t N_> bool operator==(const FixedString<N_>& s) const { return operator==(s.asStringView()); } // operator not-equals template <typename T> bool operator!=(const T& other) const { return !operator==(other); } // operator += template <typename ...Types> FixedString& operator+=(Types&&... args) { return append(std::forward<Types>(args)...); } // conversion to std::string_view. operator std::string_view() const { return asStringView(); } // basic observers size_t buffer_offset() const { return offsetof(std::decay_t<decltype(*this)>, buffer_); } static constexpr uint32_t capacity() { return N; } uint32_t size() const { return size_; } uint32_t remaining() const { return size_ >= N ? 0 : N - size_; } bool empty() const { return size_ == 0; } bool full() const { return size_ == N; } // when full, possible truncation risk. char * data() { return buffer_; } const char * data() const { return buffer_; } const char * c_str() const { return buffer_; } inline std::string_view asStringView() const { return { buffer_, static_cast<size_t>(size_) }; } inline std::string asString() const { return { buffer_, static_cast<size_t>(size_) }; } void clear() { size_ = 0; buffer_[0] = 0; } // Implementation of append - using templates // to guarantee precedence in the presence of ambiguity. // // Consider C++20 template overloading through constraints and concepts. template <typename T> FixedString& append(const T& t) { using decayT = std::decay_t<T>; if constexpr (is_specialization_v<decayT, FixedString>) { // A FixedString<U> if (size_ == 0) { // optimization to erase everything. return copyFrom(t); } else { return appendStringView({t.data(), t.size()}); } } else if constexpr(std::is_same_v<decayT, char>) { if (size_ < N) { buffer_[size_++] = t; buffer_[size_] = '\0'; } return *this; } else if constexpr(std::is_same_v<decayT, char *>) { // Some char* ptr. return appendString(t); } else if constexpr (std::is_convertible_v<decayT, std::string_view>) { // std::string_view, std::string, or some other convertible type. return appendStringView(t); } else /* constexpr */ { static_assert(dependent_false_v<T>, "non-matching append type"); } } FixedString& appendStringView(std::string_view s) { uint32_t total = std::min(static_cast<size_t>(N - size_), s.size()); memcpy(buffer_ + size_, s.data(), total); size_ += total; buffer_[size_] = '\0'; return *this; } FixedString& appendString(const char *s) { // strncpy zero pads to the end, // strlcpy returns total expected length, // we don't have strncpy_s in Bionic, // so we write our own here. while (size_ < N && *s != '\0') { buffer_[size_++] = *s++; } buffer_[size_] = '\0'; return *this; } // Copy initialize the struct. // Note: We are POD but customize the copying for acceleration // of moving small strings embedded in a large buffers. template <uint32_t U> FixedString& copyFrom(const FixedString<U>& other) { if ((void*)this != (void*)&other) { // not a self-assignment if (other.size() == 0) { size_ = 0; buffer_[0] = '\0'; return *this; } constexpr size_t kSizeToCopyWhole = 64; if constexpr (N == U && sizeof(*this) == sizeof(other) && sizeof(*this) <= kSizeToCopyWhole) { // As we have the same str size type, we can just // memcpy with fixed size, which can be easily optimized. memcpy(static_cast<void*>(this), static_cast<const void*>(&other), sizeof(*this)); return *this; } if constexpr (std::is_same_v<strsize_t, typename FixedString<U>::strsize_t>) { constexpr size_t kAlign = 8; // align to a multiple of 8. static_assert((kAlign & (kAlign - 1)) == 0); // power of 2. // we check any alignment issues. if (buffer_offset() == other.buffer_offset() && other.size() <= capacity()) { // improve on standard POD copying by reducing size. const size_t mincpy = buffer_offset() + other.size() + 1 /* nul */; const size_t maxcpy = std::min(sizeof(*this), sizeof(other)); const size_t cpysize = std::min(mincpy + kAlign - 1 & ~(kAlign - 1), maxcpy); memcpy(static_cast<void*>(this), static_cast<const void*>(&other), cpysize); return *this; } } size_ = std::min(other.size(), capacity()); memcpy(buffer_, other.data(), size_); buffer_[size_] = '\0'; // zero terminate. } return *this; } private: // Template helper methods template <typename Test, template <uint32_t> class Ref> struct is_specialization : std::false_type {}; template <template <uint32_t> class Ref, uint32_t UU> struct is_specialization<Ref<UU>, Ref>: std::true_type {}; template <typename Test, template <uint32_t> class Ref> static inline constexpr bool is_specialization_v = is_specialization<Test, Ref>::value; // For static assert(false) we need a template version to avoid early failure. template <typename T> static inline constexpr bool dependent_false_v = false; // POD variables strsize_t size_ = 0; char buffer_[N + 1 /* allow zero termination */]; }; // Stream operator syntactic sugar. // Example: // s << 'b' << "c" << "d" << '\n'; template <uint32_t N, typename ...Types> FixedString<N>& operator<<(FixedString<N>& fs, Types&&... args) { return fs.append(std::forward<Types>(args)...); } // We do not use a default size for fixed string as changing // the default size would lead to different behavior - we want the // size to be explicitly known. // FixedString62 of 62 chars fits in one typical cache line. using FixedString62 = FixedString<62>; // Slightly smaller using FixedString30 = FixedString<30>; // Since we have added copy and assignment optimizations, // we are no longer trivially assignable and copyable. // But we check standard layout here to prevent inclusion of unacceptable members or virtuals. static_assert(std::is_standard_layout_v<FixedString62>); static_assert(std::is_standard_layout_v<FixedString30>); } // namespace android::mediautils