[libc-commits] [libc] [libc][cpp::string] Do not count null terminator in capacity() (PR #211918)
Jackson Stogel via libc-commits
libc-commits at lists.llvm.org
Fri Jul 24 14:56:43 PDT 2026
https://github.com/jtstogel updated https://github.com/llvm/llvm-project/pull/211918
>From 9e5db7d6dab2f1d143fdc32dbd5fcb67eeb96202 Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Fri, 24 Jul 2026 13:22:23 -0700
Subject: [PATCH] [libc][cpp::string] Do not include null terminator in
capacity()
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.
---
libc/src/__support/CPP/string.h | 38 +++++++++++++++------
libc/test/src/__support/CPP/string_test.cpp | 24 +++++++++++--
2 files changed, 49 insertions(+), 13 deletions(-)
diff --git a/libc/src/__support/CPP/string.h b/libc/src/__support/CPP/string.h
index 274c6b67cb4d8..0de0e11a583bf 100644
--- a/libc/src/__support/CPP/string.h
+++ b/libc/src/__support/CPP/string.h
@@ -45,8 +45,14 @@ class string {
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 @@ class string {
::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 @@ class string {
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;
+
+ if (buffer_ == get_empty_string()) {
+ buffer_ = realloc_or_die(nullptr, allocation_size);
+ buffer_[0] = NULL_CHARACTER;
+ } else {
+ buffer_ = realloc_or_die(buffer_, allocation_size);
+ }
- buffer_ = realloc_or_die(buffer_ == get_empty_string() ? nullptr : buffer_,
- new_capacity);
- capacity_ = new_capacity;
+ capacity_ = allocation_size;
}
LIBC_INLINE void resize(size_t size) {
@@ -163,7 +181,7 @@ class string {
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);
diff --git a/libc/test/src/__support/CPP/string_test.cpp b/libc/test/src/__support/CPP/string_test.cpp
index 4a8c043d5d7a8..ba1992dd2da39 100644
--- a/libc/test/src/__support/CPP/string_test.cpp
+++ b/libc/test/src/__support/CPP/string_test.cpp
@@ -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");
More information about the libc-commits
mailing list