[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:
Jackson Stogel 2026-07-24 15:59:21 -07:00 committed by GitHub
parent 89ee858e94
commit 921f7ca6be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 49 additions and 13 deletions

View file

@ -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);

View file

@ -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");