[libcxx-commits] [libcxx] [libc++] Eliminate extra allocations from `std::move(oss).str()` (PR #67294)

Amirreza Ashouri via libcxx-commits libcxx-commits at lists.llvm.org
Wed Oct 11 05:05:01 PDT 2023


https://github.com/AMP999 updated https://github.com/llvm/llvm-project/pull/67294

>From 1b0dbb7befebfa893d838262566f0ccc19e0456e Mon Sep 17 00:00:00 2001
From: Amirreza Ashouri <ar.ashouri999 at gmail.com>
Date: Thu, 5 Oct 2023 00:18:38 +0330
Subject: [PATCH] [libc++] Eliminate extra allocations from
 `std::move(oss).str()`

Add test coverage for the new behaviors, especially to verify that
the returned string uses the correct allocator.

Fixes https://github.com/llvm/llvm-project/issues/64644
---
 libcxx/include/sstream                        |  10 +-
 libcxx/include/string                         |  21 ++-
 .../str.allocator_propagation.pass.cpp        | 144 ++++++++++++++++++
 .../istringstream.members/str.move.pass.cpp   |   8 +
 .../str.allocator_propagation.pass.cpp        | 115 ++++++++++++++
 .../ostringstream.members/str.move.pass.cpp   |   8 +
 .../stringbuf.members/str.move.pass.cpp       |  43 ++++++
 .../stringbuf/stringbuf.members/str.pass.cpp  |  45 +++++-
 .../stringbuf/stringbuf.members/view.pass.cpp |  28 ++++
 .../str.allocator_propagation.pass.cpp        | 144 ++++++++++++++++++
 .../stringstream.members/str.move.pass.cpp    |   8 +
 libcxx/test/support/test_allocator.h          |  25 +++
 12 files changed, 582 insertions(+), 17 deletions(-)
 create mode 100644 libcxx/test/std/input.output/string.streams/istringstream/istringstream.members/str.allocator_propagation.pass.cpp
 create mode 100644 libcxx/test/std/input.output/string.streams/ostringstream/ostringstream.members/str.allocator_propagation.pass.cpp
 create mode 100644 libcxx/test/std/input.output/string.streams/stringstream/stringstream.members/str.allocator_propagation.pass.cpp

diff --git a/libcxx/include/sstream b/libcxx/include/sstream
index 47c2d0553a57c74..c7705a8aab44dc9 100644
--- a/libcxx/include/sstream
+++ b/libcxx/include/sstream
@@ -399,12 +399,12 @@ public:
     _LIBCPP_HIDE_FROM_ABI_SSTREAM string_type str() const & { return str(__str_.get_allocator()); }
 
     _LIBCPP_HIDE_FROM_ABI_SSTREAM string_type str() && {
-        string_type __result;
         const basic_string_view<_CharT, _Traits> __view = view();
-        if (!__view.empty()) {
-            auto __pos = __view.data() - __str_.data();
-            __result.assign(std::move(__str_), __pos, __view.size());
-        }
+        typename string_type::size_type __pos = __view.empty() ? 0 : __view.data() - __str_.data();
+        // In C++23, this is just string_type(std::move(__str_), __pos, __view.size(), __str_.get_allocator());
+        // But we need something that works in C++20 also.
+        string_type __result(__str_.get_allocator());
+        __result.__move_assign(std::move(__str_), __pos, __view.size());
         __str_.clear();
         __init_buf_ptrs();
         return __result;
diff --git a/libcxx/include/string b/libcxx/include/string
index 33e87406a1156a6..b1f37d06a19d172 100644
--- a/libcxx/include/string
+++ b/libcxx/include/string
@@ -979,12 +979,7 @@ public:
 
     auto __len = std::min<size_type>(__n, __str.size() - __pos);
     if (__alloc_traits::is_always_equal::value || __alloc == __str.__alloc()) {
-      __r_.first() = __str.__r_.first();
-      __str.__r_.first() = __rep();
-
-      _Traits::move(data(), data() + __pos, __len);
-      __set_size(__len);
-      _Traits::assign(data()[__len], value_type());
+      __move_assign(std::move(__str), __pos, __len);
     } else {
       // Perform a copy because the allocators are not compatible.
       __init(__str.data() + __pos, __len);
@@ -1329,6 +1324,20 @@ public:
     return assign(__sv.data(), __sv.size());
   }
 
+#if _LIBCPP_STD_VER >= 20
+  _LIBCPP_HIDE_FROM_ABI constexpr
+  void __move_assign(basic_string&& __str, size_type __pos, size_type __len) {
+    // Pilfer the allocation from __str.
+    // Precondition: __alloc == __str.__alloc()
+    __r_.first() = __str.__r_.first();
+    __str.__default_init();
+
+    _Traits::move(data(), data() + __pos, __len);
+    __set_size(__len);
+    _Traits::assign(data()[__len], value_type());
+  }
+#endif
+
     _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20
     basic_string& assign(const basic_string& __str) { return *this = __str; }
 #ifndef _LIBCPP_CXX03_LANG
diff --git a/libcxx/test/std/input.output/string.streams/istringstream/istringstream.members/str.allocator_propagation.pass.cpp b/libcxx/test/std/input.output/string.streams/istringstream/istringstream.members/str.allocator_propagation.pass.cpp
new file mode 100644
index 000000000000000..ab41103aa856891
--- /dev/null
+++ b/libcxx/test/std/input.output/string.streams/istringstream/istringstream.members/str.allocator_propagation.pass.cpp
@@ -0,0 +1,144 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// TODO: Change to XFAIL once https://github.com/llvm/llvm-project/issues/40340 is fixed
+// UNSUPPORTED: availability-pmr-missing
+
+// This test ensures that we properly propagate allocators from istringstream's
+// inner string object to the new string returned from .str().
+// `str() const&` is specified to preserve the allocator (not copy the string).
+// `str() &&` isn't specified, but should preserve the allocator (move the string).
+
+#include <cassert>
+#include <memory>
+#include <memory_resource>
+#include <sstream>
+#include <string>
+#include <string_view>
+#include <type_traits>
+
+#include "make_string.h"
+#include "test_allocator.h"
+#include "test_macros.h"
+
+template <class CharT>
+void test_soccc_behavior() {
+  using Alloc = SocccAllocator<CharT>;
+  using SS    = std::basic_istringstream<CharT, std::char_traits<CharT>, Alloc>;
+  using S     = std::basic_string<CharT, std::char_traits<CharT>, Alloc>;
+  {
+    SS ss = SS(std::ios_base::in, Alloc(10));
+
+    // [stringbuf.members]/6 specifies that the allocator is copied,
+    // not select_on_container_copy_construction'ed.
+    //
+    S copied = ss.str();
+    assert(copied.get_allocator().count_ == 10);
+    assert(ss.rdbuf()->get_allocator().count_ == 10);
+    assert(copied.empty());
+
+    // sanity-check that SOCCC does in fact work
+    assert(S(copied).get_allocator().count_ == 11);
+
+    // [stringbuf.members]/10 doesn't specify the allocator to use,
+    // but copying the allocator as-if-by moving the string makes sense.
+    //
+    S moved = std::move(ss).str();
+    assert(moved.get_allocator().count_ == 10);
+    assert(ss.rdbuf()->get_allocator().count_ == 10);
+    assert(moved.empty());
+  }
+}
+
+template <class CharT,
+          class Base = std::basic_stringbuf<CharT, std::char_traits<CharT>, std::pmr::polymorphic_allocator<CharT>>>
+struct StringBuf : Base {
+  explicit StringBuf(std::pmr::memory_resource* mr) : Base(std::ios_base::in, mr) {}
+  void public_setg(int a, int b, int c) {
+    CharT* p = this->eback();
+    assert(this->view().data() == p);
+    this->setg(p + a, p + b, p + c);
+    assert(this->eback() == p + a);
+    assert(this->view().data() == p + a);
+  }
+};
+
+template <class CharT>
+void test_allocation_is_pilfered() {
+  using SS = std::basic_istringstream<CharT, std::char_traits<CharT>, std::pmr::polymorphic_allocator<CharT>>;
+  using S  = std::pmr::basic_string<CharT>;
+  alignas(void*) char buf[80 * sizeof(CharT)];
+  const CharT* initial =
+      MAKE_CSTRING(CharT, "a very long string that exceeds the small string optimization buffer length");
+  {
+    std::pmr::set_default_resource(std::pmr::null_memory_resource());
+    auto mr1 = std::pmr::monotonic_buffer_resource(buf, sizeof(buf), std::pmr::null_memory_resource());
+    SS ss    = SS(S(initial, &mr1));
+    S s      = std::move(ss).str();
+    assert(s == initial);
+  }
+  {
+    // Try moving-out-of a stringbuf whose view() is not the entire string.
+    // This is libc++'s behavior; libstdc++ doesn't allow such stringbufs to be created.
+    //
+    std::pmr::set_default_resource(std::pmr::null_memory_resource());
+    auto mr1 = std::pmr::monotonic_buffer_resource(buf, sizeof(buf), std::pmr::null_memory_resource());
+    auto src = StringBuf<CharT>(&mr1);
+    src.str(S(initial, &mr1));
+    src.public_setg(2, 6, 40);
+    SS ss(std::ios_base::in, &mr1);
+    *ss.rdbuf() = std::move(src);
+    LIBCPP_ASSERT(ss.view() == std::basic_string_view<CharT>(initial).substr(2, 38));
+    S s = std::move(ss).str();
+    LIBCPP_ASSERT(s == std::basic_string_view<CharT>(initial).substr(2, 38));
+  }
+}
+
+template <class CharT>
+void test_no_foreign_allocations() {
+  using SS = std::basic_istringstream<CharT, std::char_traits<CharT>, std::pmr::polymorphic_allocator<CharT>>;
+  using S  = std::pmr::basic_string<CharT>;
+  const CharT* initial =
+      MAKE_CSTRING(CharT, "a very long string that exceeds the small string optimization buffer length");
+  {
+    std::pmr::set_default_resource(std::pmr::null_memory_resource());
+    auto mr1 = std::pmr::monotonic_buffer_resource(std::pmr::new_delete_resource());
+    auto ss  = SS(S(initial, &mr1));
+    assert(ss.rdbuf()->get_allocator().resource() == &mr1);
+
+    // [stringbuf.members]/6 specifies that the result of `str() const &`
+    // does NOT use the default allocator; it uses the original allocator.
+    //
+    S copied = ss.str();
+    assert(copied.get_allocator().resource() == &mr1);
+    assert(ss.rdbuf()->get_allocator().resource() == &mr1);
+    assert(copied == initial);
+
+    // [stringbuf.members]/10 doesn't specify the allocator to use,
+    // but copying the allocator as-if-by moving the string makes sense.
+    //
+    S moved = std::move(ss).str();
+    assert(moved.get_allocator().resource() == &mr1);
+    assert(ss.rdbuf()->get_allocator().resource() == &mr1);
+    assert(moved == initial);
+  }
+}
+
+int main(int, char**) {
+  test_soccc_behavior<char>();
+  test_allocation_is_pilfered<char>();
+  test_no_foreign_allocations<char>();
+#ifndef TEST_HAS_NO_WIDE_CHARACTERS
+  test_soccc_behavior<wchar_t>();
+  test_allocation_is_pilfered<wchar_t>();
+  test_no_foreign_allocations<wchar_t>();
+#endif
+
+  return 0;
+}
diff --git a/libcxx/test/std/input.output/string.streams/istringstream/istringstream.members/str.move.pass.cpp b/libcxx/test/std/input.output/string.streams/istringstream/istringstream.members/str.move.pass.cpp
index 546f82166aaefa7..0bd076af5e9cd69 100644
--- a/libcxx/test/std/input.output/string.streams/istringstream/istringstream.members/str.move.pass.cpp
+++ b/libcxx/test/std/input.output/string.streams/istringstream/istringstream.members/str.move.pass.cpp
@@ -37,6 +37,14 @@ static void test() {
     assert(s.empty());
     assert(ss.view().empty());
   }
+  {
+    std::basic_istringstream<CharT> ss(
+        STR("a very long string that exceeds the small string optimization buffer length"));
+    const CharT* p             = ss.view().data();
+    std::basic_string<CharT> s = std::move(ss).str();
+    assert(s.data() == p); // the allocation was pilfered
+    assert(ss.view().empty());
+  }
 }
 
 int main(int, char**) {
diff --git a/libcxx/test/std/input.output/string.streams/ostringstream/ostringstream.members/str.allocator_propagation.pass.cpp b/libcxx/test/std/input.output/string.streams/ostringstream/ostringstream.members/str.allocator_propagation.pass.cpp
new file mode 100644
index 000000000000000..a5ee2afab11e99e
--- /dev/null
+++ b/libcxx/test/std/input.output/string.streams/ostringstream/ostringstream.members/str.allocator_propagation.pass.cpp
@@ -0,0 +1,115 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// TODO: Change to XFAIL once https://github.com/llvm/llvm-project/issues/40340 is fixed
+// UNSUPPORTED: availability-pmr-missing
+
+// This test ensures that we properly propagate allocators from ostringstream's
+// inner string object to the new string returned from .str().
+// `str() const&` is specified to preserve the allocator (not copy the string).
+// `str() &&` isn't specified, but should preserve the allocator (move the string).
+
+#include <cassert>
+#include <memory>
+#include <memory_resource>
+#include <sstream>
+#include <string>
+#include <type_traits>
+
+#include "make_string.h"
+#include "test_allocator.h"
+#include "test_macros.h"
+
+template <class CharT>
+void test_soccc_behavior() {
+  using Alloc = SocccAllocator<CharT>;
+  using SS    = std::basic_ostringstream<CharT, std::char_traits<CharT>, Alloc>;
+  using S     = std::basic_string<CharT, std::char_traits<CharT>, Alloc>;
+  {
+    SS ss = SS(std::ios_base::out, Alloc(10));
+
+    // [stringbuf.members]/6 specifies that the allocator is copied,
+    // not select_on_container_copy_construction'ed.
+    //
+    S copied = ss.str();
+    assert(copied.get_allocator().count_ == 10);
+    assert(ss.rdbuf()->get_allocator().count_ == 10);
+    assert(copied.empty());
+
+    // sanity-check that SOCCC does in fact work
+    assert(S(copied).get_allocator().count_ == 11);
+
+    // [stringbuf.members]/10 doesn't specify the allocator to use,
+    // but copying the allocator as-if-by moving the string makes sense.
+    //
+    S moved = std::move(ss).str();
+    assert(moved.get_allocator().count_ == 10);
+    assert(ss.rdbuf()->get_allocator().count_ == 10);
+    assert(moved.empty());
+  }
+}
+
+template <class CharT>
+void test_allocation_is_pilfered() {
+  using SS = std::basic_ostringstream<CharT, std::char_traits<CharT>, std::pmr::polymorphic_allocator<CharT>>;
+  using S  = std::pmr::basic_string<CharT>;
+  alignas(void*) char buf[80 * sizeof(CharT)];
+  const CharT* initial =
+      MAKE_CSTRING(CharT, "a very long string that exceeds the small string optimization buffer length");
+  {
+    std::pmr::set_default_resource(std::pmr::null_memory_resource());
+    auto mr1 = std::pmr::monotonic_buffer_resource(buf, sizeof(buf), std::pmr::null_memory_resource());
+    SS ss    = SS(S(initial, &mr1));
+    S s      = std::move(ss).str();
+    assert(s == initial);
+  }
+}
+
+template <class CharT>
+void test_no_foreign_allocations() {
+  using SS = std::basic_ostringstream<CharT, std::char_traits<CharT>, std::pmr::polymorphic_allocator<CharT>>;
+  using S  = std::pmr::basic_string<CharT>;
+  const CharT* initial =
+      MAKE_CSTRING(CharT, "a very long string that exceeds the small string optimization buffer length");
+  {
+    std::pmr::set_default_resource(std::pmr::null_memory_resource());
+    auto mr1 = std::pmr::monotonic_buffer_resource(std::pmr::new_delete_resource());
+    auto ss  = SS(S(initial, &mr1));
+    assert(ss.rdbuf()->get_allocator().resource() == &mr1);
+
+    // [stringbuf.members]/6 specifies that the result of `str() const &`
+    // does NOT use the default allocator; it uses the original allocator.
+    //
+    S copied = ss.str();
+    assert(copied.get_allocator().resource() == &mr1);
+    assert(ss.rdbuf()->get_allocator().resource() == &mr1);
+    assert(copied == initial);
+
+    // [stringbuf.members]/10 doesn't specify the allocator to use,
+    // but copying the allocator as-if-by moving the string makes sense.
+    //
+    S moved = std::move(ss).str();
+    assert(moved.get_allocator().resource() == &mr1);
+    assert(ss.rdbuf()->get_allocator().resource() == &mr1);
+    assert(moved == initial);
+  }
+}
+
+int main(int, char**) {
+  test_soccc_behavior<char>();
+  test_allocation_is_pilfered<char>();
+  test_no_foreign_allocations<char>();
+#ifndef TEST_HAS_NO_WIDE_CHARACTERS
+  test_soccc_behavior<wchar_t>();
+  test_allocation_is_pilfered<wchar_t>();
+  test_no_foreign_allocations<wchar_t>();
+#endif
+
+  return 0;
+}
diff --git a/libcxx/test/std/input.output/string.streams/ostringstream/ostringstream.members/str.move.pass.cpp b/libcxx/test/std/input.output/string.streams/ostringstream/ostringstream.members/str.move.pass.cpp
index 57f2384bae52c61..0e1c06f19193388 100644
--- a/libcxx/test/std/input.output/string.streams/ostringstream/ostringstream.members/str.move.pass.cpp
+++ b/libcxx/test/std/input.output/string.streams/ostringstream/ostringstream.members/str.move.pass.cpp
@@ -37,6 +37,14 @@ static void test() {
     assert(s.empty());
     assert(ss.view().empty());
   }
+  {
+    std::basic_ostringstream<CharT> ss(
+        STR("a very long string that exceeds the small string optimization buffer length"));
+    const CharT* p             = ss.view().data();
+    std::basic_string<CharT> s = std::move(ss).str();
+    assert(s.data() == p); // the allocation was pilfered
+    assert(ss.view().empty());
+  }
 }
 
 int main(int, char**) {
diff --git a/libcxx/test/std/input.output/string.streams/stringbuf/stringbuf.members/str.move.pass.cpp b/libcxx/test/std/input.output/string.streams/stringbuf/stringbuf.members/str.move.pass.cpp
index 0f0f540a9c2474d..9d75bf938ad7566 100644
--- a/libcxx/test/std/input.output/string.streams/stringbuf/stringbuf.members/str.move.pass.cpp
+++ b/libcxx/test/std/input.output/string.streams/stringbuf/stringbuf.members/str.move.pass.cpp
@@ -37,6 +37,48 @@ static void test() {
     assert(s.empty());
     assert(buf.view().empty());
   }
+  {
+    std::basic_stringbuf<CharT> buf(STR("a very long string that exceeds the small string optimization buffer length"));
+    const CharT* p             = buf.view().data();
+    std::basic_string<CharT> s = std::move(buf).str();
+    assert(s.data() == p); // the allocation was pilfered
+    assert(buf.view().empty());
+  }
+}
+
+struct StringBuf : std::stringbuf {
+  using basic_stringbuf::basic_stringbuf;
+  void public_setg(int a, int b, int c) {
+    char* p = eback();
+    this->setg(p + a, p + b, p + c);
+  }
+};
+
+static void test_altered_sequence_pointers() {
+  {
+    auto src = StringBuf("hello world", std::ios_base::in);
+    src.public_setg(4, 6, 9);
+    std::stringbuf dest;
+    dest             = std::move(src);
+    std::string view = std::string(dest.view());
+    std::string str  = std::move(dest).str();
+    assert(view == str);
+    LIBCPP_ASSERT(str == "o wor");
+    assert(dest.str().empty());
+    assert(dest.view().empty());
+  }
+  {
+    auto src = StringBuf("hello world", std::ios_base::in);
+    src.public_setg(4, 6, 9);
+    std::stringbuf dest;
+    dest.swap(src);
+    std::string view = std::string(dest.view());
+    std::string str  = std::move(dest).str();
+    assert(view == str);
+    LIBCPP_ASSERT(str == "o wor");
+    assert(dest.str().empty());
+    assert(dest.view().empty());
+  }
 }
 
 int main(int, char**) {
@@ -44,5 +86,6 @@ int main(int, char**) {
 #ifndef TEST_HAS_NO_WIDE_CHARACTERS
   test<wchar_t>();
 #endif
+  test_altered_sequence_pointers();
   return 0;
 }
diff --git a/libcxx/test/std/input.output/string.streams/stringbuf/stringbuf.members/str.pass.cpp b/libcxx/test/std/input.output/string.streams/stringbuf/stringbuf.members/str.pass.cpp
index 18a2337f6b7833f..8cd3840b6841f78 100644
--- a/libcxx/test/std/input.output/string.streams/stringbuf/stringbuf.members/str.pass.cpp
+++ b/libcxx/test/std/input.output/string.streams/stringbuf/stringbuf.members/str.pass.cpp
@@ -14,18 +14,51 @@
 // void str(const basic_string<charT,traits,Allocator>& s);
 
 #include <sstream>
+#include <string>
 #include <cassert>
 
 #include "test_macros.h"
 
+struct StringBuf : std::stringbuf {
+  explicit StringBuf(const char* s, std::ios_base::openmode mode) : basic_stringbuf(s, mode) {}
+  void public_setg(int a, int b, int c) {
+    char* p = eback();
+    this->setg(p + a, p + b, p + c);
+  }
+};
+
+static void test_altered_sequence_pointers() {
+  {
+    StringBuf src("hello world", std::ios_base::in);
+    src.public_setg(4, 6, 9);
+    std::stringbuf dest;
+    dest            = std::move(src);
+    std::string str = dest.str();
+    assert(5 <= str.size() && str.size() <= 11);
+    LIBCPP_ASSERT(str == "o wor");
+    LIBCPP_ASSERT(dest.str() == "o wor");
+  }
+  {
+    StringBuf src("hello world", std::ios_base::in);
+    src.public_setg(4, 6, 9);
+    std::stringbuf dest;
+    dest.swap(src);
+    std::string str = dest.str();
+    assert(5 <= str.size() && str.size() <= 11);
+    LIBCPP_ASSERT(str == "o wor");
+    LIBCPP_ASSERT(dest.str() == "o wor");
+  }
+}
+
 int main(int, char**)
 {
-    {
-        std::stringbuf buf("testing");
-        assert(buf.str() == "testing");
-        buf.str("another test");
-        assert(buf.str() == "another test");
-    }
+  test_altered_sequence_pointers();
+  {
+    std::stringbuf buf("testing");
+    assert(buf.str() == "testing");
+    buf.str("another test");
+    assert(buf.str() == "another test");
+  }
 #ifndef TEST_HAS_NO_WIDE_CHARACTERS
     {
         std::wstringbuf buf(L"testing");
diff --git a/libcxx/test/std/input.output/string.streams/stringbuf/stringbuf.members/view.pass.cpp b/libcxx/test/std/input.output/string.streams/stringbuf/stringbuf.members/view.pass.cpp
index 4aa2e4ab2351067..67ff506bb9dc48c 100644
--- a/libcxx/test/std/input.output/string.streams/stringbuf/stringbuf.members/view.pass.cpp
+++ b/libcxx/test/std/input.output/string.streams/stringbuf/stringbuf.members/view.pass.cpp
@@ -50,10 +50,38 @@ static void test() {
   static_assert(std::is_same_v<decltype(tbuf.view()), std::basic_string_view<CharT, my_char_traits<CharT>>>);
 }
 
+struct StringBuf : std::stringbuf {
+  using basic_stringbuf::basic_stringbuf;
+  void public_setg(int a, int b, int c) {
+    char* p = eback();
+    this->setg(p + a, p + b, p + c);
+  }
+};
+
+static void test_altered_sequence_pointers() {
+  {
+    auto src = StringBuf("hello world", std::ios_base::in);
+    src.public_setg(4, 6, 9);
+    std::stringbuf dest;
+    dest = std::move(src);
+    assert(dest.view() == dest.str());
+    LIBCPP_ASSERT(dest.view() == "o wor");
+  }
+  {
+    auto src = StringBuf("hello world", std::ios_base::in);
+    src.public_setg(4, 6, 9);
+    std::stringbuf dest;
+    dest.swap(src);
+    assert(dest.view() == dest.str());
+    LIBCPP_ASSERT(dest.view() == "o wor");
+  }
+}
+
 int main(int, char**) {
   test<char>();
 #ifndef TEST_HAS_NO_WIDE_CHARACTERS
   test<wchar_t>();
 #endif
+  test_altered_sequence_pointers();
   return 0;
 }
diff --git a/libcxx/test/std/input.output/string.streams/stringstream/stringstream.members/str.allocator_propagation.pass.cpp b/libcxx/test/std/input.output/string.streams/stringstream/stringstream.members/str.allocator_propagation.pass.cpp
new file mode 100644
index 000000000000000..46a9213eaf91967
--- /dev/null
+++ b/libcxx/test/std/input.output/string.streams/stringstream/stringstream.members/str.allocator_propagation.pass.cpp
@@ -0,0 +1,144 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: c++03, c++11, c++14, c++17
+// TODO: Change to XFAIL once https://github.com/llvm/llvm-project/issues/40340 is fixed
+// UNSUPPORTED: availability-pmr-missing
+
+// This test ensures that we properly propagate allocators from stringstream's
+// inner string object to the new string returned from .str().
+// `str() const&` is specified to preserve the allocator (not copy the string).
+// `str() &&` isn't specified, but should preserve the allocator (move the string).
+
+#include <cassert>
+#include <memory>
+#include <memory_resource>
+#include <sstream>
+#include <string>
+#include <string_view>
+#include <type_traits>
+
+#include "make_string.h"
+#include "test_allocator.h"
+#include "test_macros.h"
+
+template <class CharT>
+void test_soccc_behavior() {
+  using Alloc = SocccAllocator<CharT>;
+  using SS    = std::basic_stringstream<CharT, std::char_traits<CharT>, Alloc>;
+  using S     = std::basic_string<CharT, std::char_traits<CharT>, Alloc>;
+  {
+    SS ss = SS(std::ios_base::out, Alloc(10));
+
+    // [stringbuf.members]/6 specifies that the allocator is copied,
+    // not select_on_container_copy_construction'ed.
+    //
+    S copied = ss.str();
+    assert(copied.get_allocator().count_ == 10);
+    assert(ss.rdbuf()->get_allocator().count_ == 10);
+    assert(copied.empty());
+
+    // sanity-check that SOCCC does in fact work
+    assert(S(copied).get_allocator().count_ == 11);
+
+    // [stringbuf.members]/10 doesn't specify the allocator to use,
+    // but copying the allocator as-if-by moving the string makes sense.
+    //
+    S moved = std::move(ss).str();
+    assert(moved.get_allocator().count_ == 10);
+    assert(ss.rdbuf()->get_allocator().count_ == 10);
+    assert(moved.empty());
+  }
+}
+
+template <class CharT,
+          class Base = std::basic_stringbuf<CharT, std::char_traits<CharT>, std::pmr::polymorphic_allocator<CharT>>>
+struct StringBuf : Base {
+  explicit StringBuf(std::pmr::memory_resource* mr) : Base(std::ios_base::in, mr) {}
+  void public_setg(int a, int b, int c) {
+    CharT* p = this->eback();
+    assert(this->view().data() == p);
+    this->setg(p + a, p + b, p + c);
+    assert(this->eback() == p + a);
+    assert(this->view().data() == p + a);
+  }
+};
+
+template <class CharT>
+void test_allocation_is_pilfered() {
+  using SS = std::basic_stringstream<CharT, std::char_traits<CharT>, std::pmr::polymorphic_allocator<CharT>>;
+  using S  = std::pmr::basic_string<CharT>;
+  alignas(void*) char buf[80 * sizeof(CharT)];
+  const CharT* initial =
+      MAKE_CSTRING(CharT, "a very long string that exceeds the small string optimization buffer length");
+  {
+    std::pmr::set_default_resource(std::pmr::null_memory_resource());
+    auto mr1 = std::pmr::monotonic_buffer_resource(buf, sizeof(buf), std::pmr::null_memory_resource());
+    SS ss    = SS(S(initial, &mr1));
+    S s      = std::move(ss).str();
+    assert(s == initial);
+  }
+  {
+    // Try moving-out-of a stringbuf whose view() is not the entire string.
+    // This is libc++'s behavior; libstdc++ doesn't allow such stringbufs to be created.
+    //
+    std::pmr::set_default_resource(std::pmr::null_memory_resource());
+    auto mr1 = std::pmr::monotonic_buffer_resource(buf, sizeof(buf), std::pmr::null_memory_resource());
+    auto src = StringBuf<CharT>(&mr1);
+    src.str(S(initial, &mr1));
+    src.public_setg(2, 6, 40);
+    SS ss(std::ios_base::in, &mr1);
+    *ss.rdbuf() = std::move(src);
+    LIBCPP_ASSERT(ss.view() == std::basic_string_view<CharT>(initial).substr(2, 38));
+    S s = std::move(ss).str();
+    LIBCPP_ASSERT(s == std::basic_string_view<CharT>(initial).substr(2, 38));
+  }
+}
+
+template <class CharT>
+void test_no_foreign_allocations() {
+  using SS = std::basic_stringstream<CharT, std::char_traits<CharT>, std::pmr::polymorphic_allocator<CharT>>;
+  using S  = std::pmr::basic_string<CharT>;
+  const CharT* initial =
+      MAKE_CSTRING(CharT, "a very long string that exceeds the small string optimization buffer length");
+  {
+    std::pmr::set_default_resource(std::pmr::null_memory_resource());
+    auto mr1 = std::pmr::monotonic_buffer_resource(std::pmr::new_delete_resource());
+    auto ss  = SS(S(initial, &mr1));
+    assert(ss.rdbuf()->get_allocator().resource() == &mr1);
+
+    // [stringbuf.members]/6 specifies that the result of `str() const &`
+    // does NOT use the default allocator; it uses the original allocator.
+    //
+    S copied = ss.str();
+    assert(copied.get_allocator().resource() == &mr1);
+    assert(ss.rdbuf()->get_allocator().resource() == &mr1);
+    assert(copied == initial);
+
+    // [stringbuf.members]/10 doesn't specify the allocator to use,
+    // but copying the allocator as-if-by moving the string makes sense.
+    //
+    S moved = std::move(ss).str();
+    assert(moved.get_allocator().resource() == &mr1);
+    assert(ss.rdbuf()->get_allocator().resource() == &mr1);
+    assert(moved == initial);
+  }
+}
+
+int main(int, char**) {
+  test_soccc_behavior<char>();
+  test_allocation_is_pilfered<char>();
+  test_no_foreign_allocations<char>();
+#ifndef TEST_HAS_NO_WIDE_CHARACTERS
+  test_soccc_behavior<wchar_t>();
+  test_allocation_is_pilfered<wchar_t>();
+  test_no_foreign_allocations<wchar_t>();
+#endif
+
+  return 0;
+}
diff --git a/libcxx/test/std/input.output/string.streams/stringstream/stringstream.members/str.move.pass.cpp b/libcxx/test/std/input.output/string.streams/stringstream/stringstream.members/str.move.pass.cpp
index 35349c9c288ec1a..56a0d84fb68edc1 100644
--- a/libcxx/test/std/input.output/string.streams/stringstream/stringstream.members/str.move.pass.cpp
+++ b/libcxx/test/std/input.output/string.streams/stringstream/stringstream.members/str.move.pass.cpp
@@ -37,6 +37,14 @@ static void test() {
     assert(s.empty());
     assert(ss.view().empty());
   }
+  {
+    std::basic_stringstream<CharT> ss(
+        STR("a very long string that exceeds the small string optimization buffer length"));
+    const CharT* p             = ss.view().data();
+    std::basic_string<CharT> s = std::move(ss).str();
+    assert(s.data() == p); // the allocation was pilfered
+    assert(ss.view().empty());
+  }
 }
 
 int main(int, char**) {
diff --git a/libcxx/test/support/test_allocator.h b/libcxx/test/support/test_allocator.h
index 9330150a838514b..3bde73183ab6e39 100644
--- a/libcxx/test/support/test_allocator.h
+++ b/libcxx/test/support/test_allocator.h
@@ -475,4 +475,29 @@ TEST_CONSTEXPR inline bool operator!=(limited_allocator<T, N> const& LHS, limite
   return !(LHS == RHS);
 }
 
+// Track the "provenance" of this allocator instance: how many times was
+// select_on_container_copy_construction called in order to produce it?
+//
+template <class T>
+struct SocccAllocator {
+  using value_type = T;
+
+  int count_ = 0;
+  explicit SocccAllocator(int i) : count_(i) {}
+
+  template <class U>
+  SocccAllocator(const SocccAllocator<U>& a) : count_(a.count_) {}
+
+  T* allocate(std::size_t n) { return std::allocator<T>().allocate(n); }
+  void deallocate(T* p, std::size_t n) { std::allocator<T>().deallocate(p, n); }
+
+  SocccAllocator select_on_container_copy_construction() const { return SocccAllocator(count_ + 1); }
+
+  bool operator==(const SocccAllocator&) const { return true; }
+
+  using propagate_on_container_copy_assignment = std::false_type;
+  using propagate_on_container_move_assignment = std::false_type;
+  using propagate_on_container_swap            = std::false_type;
+};
+
 #endif // TEST_ALLOCATOR_H



More information about the libcxx-commits mailing list