Skip to content

Commit d4373aa

Browse files
committed
[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 #64644
1 parent c5b617c commit d4373aa

File tree

12 files changed

+582
-17
lines changed

12 files changed

+582
-17
lines changed

libcxx/include/sstream

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -400,12 +400,12 @@ public:
400400
_LIBCPP_HIDE_FROM_ABI_SSTREAM string_type str() const & { return str(__str_.get_allocator()); }
401401

402402
_LIBCPP_HIDE_FROM_ABI_SSTREAM string_type str() && {
403-
string_type __result;
404403
const basic_string_view<_CharT, _Traits> __view = view();
405-
if (!__view.empty()) {
406-
auto __pos = __view.data() - __str_.data();
407-
__result.assign(std::move(__str_), __pos, __view.size());
408-
}
404+
typename string_type::size_type __pos = __view.empty() ? 0 : __view.data() - __str_.data();
405+
// In C++23, this is just string_type(std::move(__str_), __pos, __view.size(), __str_.get_allocator());
406+
// But we need something that works in C++20 also.
407+
string_type __result(__str_.get_allocator());
408+
__result.__move_assign(std::move(__str_), __pos, __view.size());
409409
__str_.clear();
410410
__init_buf_ptrs();
411411
return __result;

libcxx/include/string

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -979,12 +979,7 @@ public:
979979

980980
auto __len = std::min<size_type>(__n, __str.size() - __pos);
981981
if (__alloc_traits::is_always_equal::value || __alloc == __str.__alloc()) {
982-
__r_.first() = __str.__r_.first();
983-
__str.__r_.first() = __rep();
984-
985-
_Traits::move(data(), data() + __pos, __len);
986-
__set_size(__len);
987-
_Traits::assign(data()[__len], value_type());
982+
__move_assign(std::move(__str), __pos, __len);
988983
} else {
989984
// Perform a copy because the allocators are not compatible.
990985
__init(__str.data() + __pos, __len);
@@ -1329,6 +1324,20 @@ public:
13291324
return assign(__sv.data(), __sv.size());
13301325
}
13311326

1327+
#if _LIBCPP_STD_VER >= 20
1328+
_LIBCPP_HIDE_FROM_ABI constexpr
1329+
void __move_assign(basic_string&& __str, size_type __pos, size_type __len) {
1330+
// Pilfer the allocation from __str.
1331+
_LIBCPP_ASSERT_INTERNAL(__alloc() == __str.__alloc(), "__move_assign called with wrong allocator");
1332+
__r_.first() = __str.__r_.first();
1333+
__str.__r_.first() = __rep();
1334+
1335+
_Traits::move(data(), data() + __pos, __len);
1336+
__set_size(__len);
1337+
_Traits::assign(data()[__len], value_type());
1338+
}
1339+
#endif
1340+
13321341
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20
13331342
basic_string& assign(const basic_string& __str) { return *this = __str; }
13341343
#ifndef _LIBCPP_CXX03_LANG
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
// UNSUPPORTED: c++03, c++11, c++14, c++17
10+
// TODO: Change to XFAIL once https://github.com/llvm/llvm-project/issues/40340 is fixed
11+
// UNSUPPORTED: availability-pmr-missing
12+
13+
// This test ensures that we properly propagate allocators from istringstream's
14+
// inner string object to the new string returned from .str().
15+
// `str() const&` is specified to preserve the allocator (not copy the string).
16+
// `str() &&` isn't specified, but should preserve the allocator (move the string).
17+
18+
#include <cassert>
19+
#include <memory>
20+
#include <memory_resource>
21+
#include <sstream>
22+
#include <string>
23+
#include <string_view>
24+
#include <type_traits>
25+
26+
#include "make_string.h"
27+
#include "test_allocator.h"
28+
#include "test_macros.h"
29+
30+
template <class CharT>
31+
void test_soccc_behavior() {
32+
using Alloc = SocccAllocator<CharT>;
33+
using SS = std::basic_istringstream<CharT, std::char_traits<CharT>, Alloc>;
34+
using S = std::basic_string<CharT, std::char_traits<CharT>, Alloc>;
35+
{
36+
SS ss = SS(std::ios_base::in, Alloc(10));
37+
38+
// [stringbuf.members]/6 specifies that the allocator is copied,
39+
// not select_on_container_copy_construction'ed.
40+
//
41+
S copied = ss.str();
42+
assert(copied.get_allocator().count_ == 10);
43+
assert(ss.rdbuf()->get_allocator().count_ == 10);
44+
assert(copied.empty());
45+
46+
// sanity-check that SOCCC does in fact work
47+
assert(S(copied).get_allocator().count_ == 11);
48+
49+
// [stringbuf.members]/10 doesn't specify the allocator to use,
50+
// but copying the allocator as-if-by moving the string makes sense.
51+
//
52+
S moved = std::move(ss).str();
53+
assert(moved.get_allocator().count_ == 10);
54+
assert(ss.rdbuf()->get_allocator().count_ == 10);
55+
assert(moved.empty());
56+
}
57+
}
58+
59+
template <class CharT,
60+
class Base = std::basic_stringbuf<CharT, std::char_traits<CharT>, std::pmr::polymorphic_allocator<CharT>>>
61+
struct StringBuf : Base {
62+
explicit StringBuf(std::pmr::memory_resource* mr) : Base(std::ios_base::in, mr) {}
63+
void public_setg(int a, int b, int c) {
64+
CharT* p = this->eback();
65+
assert(this->view().data() == p);
66+
this->setg(p + a, p + b, p + c);
67+
assert(this->eback() == p + a);
68+
assert(this->view().data() == p + a);
69+
}
70+
};
71+
72+
template <class CharT>
73+
void test_allocation_is_pilfered() {
74+
using SS = std::basic_istringstream<CharT, std::char_traits<CharT>, std::pmr::polymorphic_allocator<CharT>>;
75+
using S = std::pmr::basic_string<CharT>;
76+
alignas(void*) char buf[80 * sizeof(CharT)];
77+
const CharT* initial =
78+
MAKE_CSTRING(CharT, "a very long string that exceeds the small string optimization buffer length");
79+
{
80+
std::pmr::set_default_resource(std::pmr::null_memory_resource());
81+
auto mr1 = std::pmr::monotonic_buffer_resource(buf, sizeof(buf), std::pmr::null_memory_resource());
82+
SS ss = SS(S(initial, &mr1));
83+
S s = std::move(ss).str();
84+
assert(s == initial);
85+
}
86+
{
87+
// Try moving-out-of a stringbuf whose view() is not the entire string.
88+
// This is libc++'s behavior; libstdc++ doesn't allow such stringbufs to be created.
89+
//
90+
std::pmr::set_default_resource(std::pmr::null_memory_resource());
91+
auto mr1 = std::pmr::monotonic_buffer_resource(buf, sizeof(buf), std::pmr::null_memory_resource());
92+
auto src = StringBuf<CharT>(&mr1);
93+
src.str(S(initial, &mr1));
94+
src.public_setg(2, 6, 40);
95+
SS ss(std::ios_base::in, &mr1);
96+
*ss.rdbuf() = std::move(src);
97+
LIBCPP_ASSERT(ss.view() == std::basic_string_view<CharT>(initial).substr(2, 38));
98+
S s = std::move(ss).str();
99+
LIBCPP_ASSERT(s == std::basic_string_view<CharT>(initial).substr(2, 38));
100+
}
101+
}
102+
103+
template <class CharT>
104+
void test_no_foreign_allocations() {
105+
using SS = std::basic_istringstream<CharT, std::char_traits<CharT>, std::pmr::polymorphic_allocator<CharT>>;
106+
using S = std::pmr::basic_string<CharT>;
107+
const CharT* initial =
108+
MAKE_CSTRING(CharT, "a very long string that exceeds the small string optimization buffer length");
109+
{
110+
std::pmr::set_default_resource(std::pmr::null_memory_resource());
111+
auto mr1 = std::pmr::monotonic_buffer_resource(std::pmr::new_delete_resource());
112+
auto ss = SS(S(initial, &mr1));
113+
assert(ss.rdbuf()->get_allocator().resource() == &mr1);
114+
115+
// [stringbuf.members]/6 specifies that the result of `str() const &`
116+
// does NOT use the default allocator; it uses the original allocator.
117+
//
118+
S copied = ss.str();
119+
assert(copied.get_allocator().resource() == &mr1);
120+
assert(ss.rdbuf()->get_allocator().resource() == &mr1);
121+
assert(copied == initial);
122+
123+
// [stringbuf.members]/10 doesn't specify the allocator to use,
124+
// but copying the allocator as-if-by moving the string makes sense.
125+
//
126+
S moved = std::move(ss).str();
127+
assert(moved.get_allocator().resource() == &mr1);
128+
assert(ss.rdbuf()->get_allocator().resource() == &mr1);
129+
assert(moved == initial);
130+
}
131+
}
132+
133+
int main(int, char**) {
134+
test_soccc_behavior<char>();
135+
test_allocation_is_pilfered<char>();
136+
test_no_foreign_allocations<char>();
137+
#ifndef TEST_HAS_NO_WIDE_CHARACTERS
138+
test_soccc_behavior<wchar_t>();
139+
test_allocation_is_pilfered<wchar_t>();
140+
test_no_foreign_allocations<wchar_t>();
141+
#endif
142+
143+
return 0;
144+
}

libcxx/test/std/input.output/string.streams/istringstream/istringstream.members/str.move.pass.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ static void test() {
3737
assert(s.empty());
3838
assert(ss.view().empty());
3939
}
40+
{
41+
std::basic_istringstream<CharT> ss(
42+
STR("a very long string that exceeds the small string optimization buffer length"));
43+
const CharT* p = ss.view().data();
44+
std::basic_string<CharT> s = std::move(ss).str();
45+
assert(s.data() == p); // the allocation was pilfered
46+
assert(ss.view().empty());
47+
}
4048
}
4149

4250
int main(int, char**) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
// UNSUPPORTED: c++03, c++11, c++14, c++17
10+
// TODO: Change to XFAIL once https://github.com/llvm/llvm-project/issues/40340 is fixed
11+
// UNSUPPORTED: availability-pmr-missing
12+
13+
// This test ensures that we properly propagate allocators from ostringstream's
14+
// inner string object to the new string returned from .str().
15+
// `str() const&` is specified to preserve the allocator (not copy the string).
16+
// `str() &&` isn't specified, but should preserve the allocator (move the string).
17+
18+
#include <cassert>
19+
#include <memory>
20+
#include <memory_resource>
21+
#include <sstream>
22+
#include <string>
23+
#include <type_traits>
24+
25+
#include "make_string.h"
26+
#include "test_allocator.h"
27+
#include "test_macros.h"
28+
29+
template <class CharT>
30+
void test_soccc_behavior() {
31+
using Alloc = SocccAllocator<CharT>;
32+
using SS = std::basic_ostringstream<CharT, std::char_traits<CharT>, Alloc>;
33+
using S = std::basic_string<CharT, std::char_traits<CharT>, Alloc>;
34+
{
35+
SS ss = SS(std::ios_base::out, Alloc(10));
36+
37+
// [stringbuf.members]/6 specifies that the allocator is copied,
38+
// not select_on_container_copy_construction'ed.
39+
//
40+
S copied = ss.str();
41+
assert(copied.get_allocator().count_ == 10);
42+
assert(ss.rdbuf()->get_allocator().count_ == 10);
43+
assert(copied.empty());
44+
45+
// sanity-check that SOCCC does in fact work
46+
assert(S(copied).get_allocator().count_ == 11);
47+
48+
// [stringbuf.members]/10 doesn't specify the allocator to use,
49+
// but copying the allocator as-if-by moving the string makes sense.
50+
//
51+
S moved = std::move(ss).str();
52+
assert(moved.get_allocator().count_ == 10);
53+
assert(ss.rdbuf()->get_allocator().count_ == 10);
54+
assert(moved.empty());
55+
}
56+
}
57+
58+
template <class CharT>
59+
void test_allocation_is_pilfered() {
60+
using SS = std::basic_ostringstream<CharT, std::char_traits<CharT>, std::pmr::polymorphic_allocator<CharT>>;
61+
using S = std::pmr::basic_string<CharT>;
62+
alignas(void*) char buf[80 * sizeof(CharT)];
63+
const CharT* initial =
64+
MAKE_CSTRING(CharT, "a very long string that exceeds the small string optimization buffer length");
65+
{
66+
std::pmr::set_default_resource(std::pmr::null_memory_resource());
67+
auto mr1 = std::pmr::monotonic_buffer_resource(buf, sizeof(buf), std::pmr::null_memory_resource());
68+
SS ss = SS(S(initial, &mr1));
69+
S s = std::move(ss).str();
70+
assert(s == initial);
71+
}
72+
}
73+
74+
template <class CharT>
75+
void test_no_foreign_allocations() {
76+
using SS = std::basic_ostringstream<CharT, std::char_traits<CharT>, std::pmr::polymorphic_allocator<CharT>>;
77+
using S = std::pmr::basic_string<CharT>;
78+
const CharT* initial =
79+
MAKE_CSTRING(CharT, "a very long string that exceeds the small string optimization buffer length");
80+
{
81+
std::pmr::set_default_resource(std::pmr::null_memory_resource());
82+
auto mr1 = std::pmr::monotonic_buffer_resource(std::pmr::new_delete_resource());
83+
auto ss = SS(S(initial, &mr1));
84+
assert(ss.rdbuf()->get_allocator().resource() == &mr1);
85+
86+
// [stringbuf.members]/6 specifies that the result of `str() const &`
87+
// does NOT use the default allocator; it uses the original allocator.
88+
//
89+
S copied = ss.str();
90+
assert(copied.get_allocator().resource() == &mr1);
91+
assert(ss.rdbuf()->get_allocator().resource() == &mr1);
92+
assert(copied == initial);
93+
94+
// [stringbuf.members]/10 doesn't specify the allocator to use,
95+
// but copying the allocator as-if-by moving the string makes sense.
96+
//
97+
S moved = std::move(ss).str();
98+
assert(moved.get_allocator().resource() == &mr1);
99+
assert(ss.rdbuf()->get_allocator().resource() == &mr1);
100+
assert(moved == initial);
101+
}
102+
}
103+
104+
int main(int, char**) {
105+
test_soccc_behavior<char>();
106+
test_allocation_is_pilfered<char>();
107+
test_no_foreign_allocations<char>();
108+
#ifndef TEST_HAS_NO_WIDE_CHARACTERS
109+
test_soccc_behavior<wchar_t>();
110+
test_allocation_is_pilfered<wchar_t>();
111+
test_no_foreign_allocations<wchar_t>();
112+
#endif
113+
114+
return 0;
115+
}

libcxx/test/std/input.output/string.streams/ostringstream/ostringstream.members/str.move.pass.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ static void test() {
3737
assert(s.empty());
3838
assert(ss.view().empty());
3939
}
40+
{
41+
std::basic_ostringstream<CharT> ss(
42+
STR("a very long string that exceeds the small string optimization buffer length"));
43+
const CharT* p = ss.view().data();
44+
std::basic_string<CharT> s = std::move(ss).str();
45+
assert(s.data() == p); // the allocation was pilfered
46+
assert(ss.view().empty());
47+
}
4048
}
4149

4250
int main(int, char**) {

libcxx/test/std/input.output/string.streams/stringbuf/stringbuf.members/str.move.pass.cpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,55 @@ static void test() {
3737
assert(s.empty());
3838
assert(buf.view().empty());
3939
}
40+
{
41+
std::basic_stringbuf<CharT> buf(STR("a very long string that exceeds the small string optimization buffer length"));
42+
const CharT* p = buf.view().data();
43+
std::basic_string<CharT> s = std::move(buf).str();
44+
assert(s.data() == p); // the allocation was pilfered
45+
assert(buf.view().empty());
46+
}
47+
}
48+
49+
struct StringBuf : std::stringbuf {
50+
using basic_stringbuf::basic_stringbuf;
51+
void public_setg(int a, int b, int c) {
52+
char* p = eback();
53+
this->setg(p + a, p + b, p + c);
54+
}
55+
};
56+
57+
static void test_altered_sequence_pointers() {
58+
{
59+
auto src = StringBuf("hello world", std::ios_base::in);
60+
src.public_setg(4, 6, 9);
61+
std::stringbuf dest;
62+
dest = std::move(src);
63+
std::string view = std::string(dest.view());
64+
std::string str = std::move(dest).str();
65+
assert(view == str);
66+
LIBCPP_ASSERT(str == "o wor");
67+
assert(dest.str().empty());
68+
assert(dest.view().empty());
69+
}
70+
{
71+
auto src = StringBuf("hello world", std::ios_base::in);
72+
src.public_setg(4, 6, 9);
73+
std::stringbuf dest;
74+
dest.swap(src);
75+
std::string view = std::string(dest.view());
76+
std::string str = std::move(dest).str();
77+
assert(view == str);
78+
LIBCPP_ASSERT(str == "o wor");
79+
assert(dest.str().empty());
80+
assert(dest.view().empty());
81+
}
4082
}
4183

4284
int main(int, char**) {
4385
test<char>();
4486
#ifndef TEST_HAS_NO_WIDE_CHARACTERS
4587
test<wchar_t>();
4688
#endif
89+
test_altered_sequence_pointers();
4790
return 0;
4891
}

0 commit comments

Comments
 (0)