[libc][cpp::string] Do not count null terminator in capacity() (#211918)
Currently, `capacity()` includes the null terminator of the backing buffer, which does not align with C++20's `std::string`. That is, `s.reserve(s.capacity())` should be a no-op. While there's no requirement these be in-sync, it is less surprising to align its behavior. Also, this fixes the case where `reserve()` is called on an empty string, where previously the null terminator was not set.
This commit is contained in:
parent
89ee858e94
commit
921f7ca6be
2 changed files with 49 additions and 13 deletions
|
|
@ -45,8 +45,14 @@ private:
|
|||
return const_cast<char *>(&NULL_CHARACTER);
|
||||
}
|
||||
|
||||
// Backing data for the represented string.
|
||||
// This may point to NULL_CHARACTER or a heap-allocated buffer.
|
||||
char *buffer_ = get_empty_string();
|
||||
|
||||
// Size of the string represented in buffer_.
|
||||
size_t size_ = 0;
|
||||
|
||||
// The size of buffer_.
|
||||
size_t capacity_ = 0;
|
||||
|
||||
constexpr void reset_no_deallocate() {
|
||||
|
|
@ -112,7 +118,13 @@ public:
|
|||
::free(buffer_);
|
||||
}
|
||||
|
||||
LIBC_INLINE constexpr size_t capacity() const { return capacity_; }
|
||||
// Returns the number of writable bytes in this string.
|
||||
// Does not include the string-managed null terminator.
|
||||
LIBC_INLINE constexpr size_t capacity() const {
|
||||
if (capacity_ == 0)
|
||||
return 0;
|
||||
return capacity_ - 1;
|
||||
}
|
||||
LIBC_INLINE constexpr size_t size() const { return size_; }
|
||||
LIBC_INLINE constexpr bool empty() const { return size_ == 0; }
|
||||
|
||||
|
|
@ -142,19 +154,25 @@ public:
|
|||
return string_view(buffer_, size_);
|
||||
}
|
||||
|
||||
LIBC_INLINE void reserve(size_t new_capacity) {
|
||||
++new_capacity; // Accounting for the terminating '\0'
|
||||
if (new_capacity <= capacity_)
|
||||
LIBC_INLINE void reserve(size_t new_cap) {
|
||||
size_t allocation_size = new_cap + 1; // +1 for terminating '\0'
|
||||
if (allocation_size <= capacity_)
|
||||
return;
|
||||
|
||||
// We extend the capacity to amortize buffer_ reallocations.
|
||||
// We choose to augment the value by 11 / 8, this is about +40% and division
|
||||
// by 8 is cheap. We guard the extension so the operation doesn't overflow.
|
||||
if (new_capacity < SIZE_MAX / 11)
|
||||
new_capacity = new_capacity * 11 / 8;
|
||||
if (allocation_size < SIZE_MAX / 11)
|
||||
allocation_size = allocation_size * 11 / 8;
|
||||
|
||||
buffer_ = realloc_or_die(buffer_ == get_empty_string() ? nullptr : buffer_,
|
||||
new_capacity);
|
||||
capacity_ = new_capacity;
|
||||
if (buffer_ == get_empty_string()) {
|
||||
buffer_ = realloc_or_die(nullptr, allocation_size);
|
||||
buffer_[0] = NULL_CHARACTER;
|
||||
} else {
|
||||
buffer_ = realloc_or_die(buffer_, allocation_size);
|
||||
}
|
||||
|
||||
capacity_ = allocation_size;
|
||||
}
|
||||
|
||||
LIBC_INLINE void resize(size_t size) {
|
||||
|
|
@ -163,7 +181,7 @@ public:
|
|||
if (size == size_)
|
||||
return;
|
||||
|
||||
if (size >= capacity_) {
|
||||
if (size > capacity()) {
|
||||
reserve(size);
|
||||
const size_t size_extension = size - size_;
|
||||
inline_memset(data() + size_, '\0', size_extension);
|
||||
|
|
|
|||
|
|
@ -173,12 +173,12 @@ TEST(LlvmLibcStringTest, ResizeCapacityAndNullTermination) {
|
|||
// One char
|
||||
a.resize(1);
|
||||
ASSERT_EQ(a.size(), size_t(1));
|
||||
ASSERT_GE(a.capacity(), size_t(2));
|
||||
ASSERT_GE(a.capacity(), size_t(1));
|
||||
ASSERT_EQ(a.data()[1], '\0');
|
||||
// Clear
|
||||
a.resize(0);
|
||||
ASSERT_EQ(a.size(), size_t(0));
|
||||
ASSERT_GE(a.capacity(), size_t(2));
|
||||
ASSERT_GE(a.capacity(), size_t(1));
|
||||
ASSERT_EQ(a.data()[0], '\0');
|
||||
// Resize and check zero initialized
|
||||
a.resize(10);
|
||||
|
|
@ -193,10 +193,28 @@ TEST(LlvmLibcStringTest, ResizeWithCapacityPlus1) {
|
|||
a.resize(32);
|
||||
|
||||
size_t previous_capacity = a.capacity();
|
||||
a.resize(previous_capacity);
|
||||
a.resize(previous_capacity + 1);
|
||||
ASSERT_GT(a.capacity(), previous_capacity);
|
||||
}
|
||||
|
||||
TEST(LlvmLibcStringTest, ReserveWithSameCapacityIsNop) {
|
||||
string a;
|
||||
a.resize(32);
|
||||
|
||||
size_t previous_capacity = a.capacity();
|
||||
// Since C++20, calling reserve with capacity less than or equal
|
||||
// to the current capacity should have no effect.
|
||||
a.reserve(previous_capacity);
|
||||
ASSERT_EQ(a.capacity(), previous_capacity);
|
||||
}
|
||||
|
||||
TEST(LlvmLibcStringTest, ReserveOnEmptyStringKeepsNullTerminator) {
|
||||
string s;
|
||||
s.reserve(10);
|
||||
ASSERT_EQ(s.size(), size_t(0));
|
||||
ASSERT_EQ(s[0], '\0');
|
||||
}
|
||||
|
||||
TEST(LlvmLibcStringTest, ConcatWithCString) {
|
||||
ASSERT_STREQ((string("a") + string("b")).c_str(), "ab");
|
||||
ASSERT_STREQ((string("a") + "b").c_str(), "ab");
|
||||
|
|
|
|||
Loading…
Reference in a new issue