Skip to content

Commit b964419

Browse files
authored
[libcxx] Allow string to use SSO in constant evaluation. (#66576)
Previously, libcxx forced all strings created during constant evaluation to point to allocated memory. That was done due to implementation difficultites, but it turns out not to be necessary. This patch permits the use of SSO strings during constant evaluation, and also simplifies the implementation. This does have a downside in terms of enabling users to accidentally write non-portable code, however, which I've documented in UsingLibcxx.rst. In particular, whether `constinit std::string x = "...";` will successfully compile now depends on whether the string is smaller than the SSO capacity -- in libc++, up to 22 bytes on 64-bit platforms, and up to 10 bytes on 32-bit platforms. By comparison, libstdc++ and MSVC have an SSO capacity of 15 bytes, except that in libstdc++, constant-initialized strings cannot be used as function-locals because the object contains a pointer to itself. Closes #68434
1 parent 45a29bd commit b964419

File tree

4 files changed

+121
-81
lines changed

4 files changed

+121
-81
lines changed

libcxx/docs/UsingLibcxx.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,30 @@ Extensions to the C++23 modules ``std`` and ``std.compat``
545545
Like other major implementations, libc++ provides C++23 modules ``std`` and
546546
``std.compat`` in C++20 as an extension"
547547

548+
Constant-initialized std::string
549+
--------------------------------
550+
551+
As an implementation-specific optimization, ``std::basic_string`` (``std::string``,
552+
``std::wstring``, etc.) may either store the string data directly in the object, or else store a
553+
pointer to heap-allocated memory, depending on the length of the string.
554+
555+
As of C++20, the constructors are now declared ``constexpr``, which permits strings to be used
556+
during constant-evaluation time. In libc++, as in other common implementations, it is also possible
557+
to constant-initialize a string object (e.g. via declaring a variable with ``constinit`` or
558+
``constexpr``), but, only if the string is short enough to not require a heap allocation. Reliance
559+
upon this should be discouraged in portable code, as the allowed length differs based on the
560+
standard-library implementation and also based on whether the platform uses 32-bit or 64-bit
561+
pointers.
562+
563+
.. code-block:: cpp
564+
565+
// Non-portable: 11-char string works on 64-bit libc++, but not on 32-bit.
566+
constinit std::string x = "hello world";
567+
568+
// Prefer to use string_view, or remove constinit/constexpr from the variable definition:
569+
constinit std::string_view x = "hello world";
570+
std::string_view y = "hello world";
571+
548572
.. _turning-off-asan:
549573

550574
Turning off ASan annotation in containers

libcxx/include/string

Lines changed: 19 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -830,8 +830,8 @@ private:
830830
{
831831
union
832832
{
833-
__long __l;
834833
__short __s;
834+
__long __l;
835835
__raw __r;
836836
};
837837
};
@@ -879,19 +879,15 @@ public:
879879

880880
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 basic_string()
881881
_NOEXCEPT_(is_nothrow_default_constructible<allocator_type>::value)
882-
: __r_(__default_init_tag(), __default_init_tag()) {
883-
__default_init();
884-
}
882+
: __r_(__value_init_tag(), __default_init_tag()) {}
885883

886884
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 explicit basic_string(const allocator_type& __a)
887885
#if _LIBCPP_STD_VER <= 14
888886
_NOEXCEPT_(is_nothrow_copy_constructible<allocator_type>::value)
889887
#else
890888
_NOEXCEPT
891889
#endif
892-
: __r_(__default_init_tag(), __a) {
893-
__default_init();
894-
}
890+
: __r_(__value_init_tag(), __a) {}
895891

896892
_LIBCPP_CONSTEXPR_SINCE_CXX20 basic_string(const basic_string& __str)
897893
: __r_(__default_init_tag(), __alloc_traits::select_on_container_copy_construction(__str.__alloc())) {
@@ -917,7 +913,7 @@ public:
917913
_NOEXCEPT
918914
# endif
919915
: __r_(std::move(__str.__r_)) {
920-
__str.__default_init();
916+
__str.__r_.first() = __rep();
921917
}
922918

923919
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 basic_string(basic_string&& __str, const allocator_type& __a)
@@ -928,7 +924,7 @@ public:
928924
if (__libcpp_is_constant_evaluated())
929925
__r_.first() = __rep();
930926
__r_.first() = __str.__r_.first();
931-
__str.__default_init();
927+
__str.__r_.first() = __rep();
932928
}
933929
}
934930
#endif // _LIBCPP_CXX03_LANG
@@ -984,7 +980,7 @@ public:
984980
auto __len = std::min<size_type>(__n, __str.size() - __pos);
985981
if (__alloc_traits::is_always_equal::value || __alloc == __str.__alloc()) {
986982
__r_.first() = __str.__r_.first();
987-
__str.__default_init();
983+
__str.__r_.first() = __rep();
988984

989985
_Traits::move(data(), data() + __pos, __len);
990986
__set_size(__len);
@@ -1729,8 +1725,9 @@ private:
17291725

17301726
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20
17311727
bool __is_long() const _NOEXCEPT {
1732-
if (__libcpp_is_constant_evaluated())
1733-
return true;
1728+
if (__libcpp_is_constant_evaluated() && __builtin_constant_p(__r_.first().__l.__is_long_)) {
1729+
return __r_.first().__l.__is_long_;
1730+
}
17341731
return __r_.first().__s.__is_long_;
17351732
}
17361733

@@ -1746,26 +1743,8 @@ private:
17461743
#endif // _LIBCPP_STD_VER >= 20
17471744
}
17481745

1749-
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 void __default_init() {
1750-
__r_.first() = __rep();
1751-
if (__libcpp_is_constant_evaluated()) {
1752-
size_type __sz = __recommend(0) + 1;
1753-
pointer __ptr = __alloc_traits::allocate(__alloc(), __sz);
1754-
__begin_lifetime(__ptr, __sz);
1755-
__set_long_pointer(__ptr);
1756-
__set_long_cap(__sz);
1757-
__set_long_size(0);
1758-
}
1759-
}
1760-
1761-
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 void __deallocate_constexpr() {
1762-
if (__libcpp_is_constant_evaluated() && __get_pointer() != nullptr)
1763-
__alloc_traits::deallocate(__alloc(), __get_pointer(), __get_long_cap());
1764-
}
1765-
17661746
_LIBCPP_CONSTEXPR _LIBCPP_HIDE_FROM_ABI static bool __fits_in_sso(size_type __sz) {
1767-
// SSO is disabled during constant evaluation because `__is_long` isn't constexpr friendly
1768-
return !__libcpp_is_constant_evaluated() && (__sz < __min_cap);
1747+
return __sz < __min_cap;
17691748
}
17701749

17711750
template <class _Iterator, class _Sentinel>
@@ -1877,10 +1856,7 @@ private:
18771856
size_type __recommend(size_type __s) _NOEXCEPT
18781857
{
18791858
if (__s < __min_cap) {
1880-
if (__libcpp_is_constant_evaluated())
1881-
return static_cast<size_type>(__min_cap);
1882-
else
1883-
return static_cast<size_type>(__min_cap) - 1;
1859+
return static_cast<size_type>(__min_cap) - 1;
18841860
}
18851861
size_type __guess = __align_it<sizeof(value_type) < __alignment ?
18861862
__alignment/sizeof(value_type) : 1 > (__s+1) - 1;
@@ -2021,7 +1997,7 @@ private:
20211997
_LIBCPP_CONSTEXPR_SINCE_CXX20 basic_string& __assign_external(const value_type* __s, size_type __n);
20221998

20231999
// Assigns the value in __s, guaranteed to be __n < __min_cap in length.
2024-
inline basic_string& __assign_short(const value_type* __s, size_type __n) {
2000+
inline _LIBCPP_CONSTEXPR_SINCE_CXX20 basic_string& __assign_short(const value_type* __s, size_type __n) {
20252001
pointer __p = __is_long()
20262002
? (__set_long_size(__n), __get_long_pointer())
20272003
: (__set_short_size(__n), __get_short_pointer());
@@ -2235,7 +2211,7 @@ template <class _CharT, class _Traits, class _Allocator>
22352211
template <class _InputIterator, class _Sentinel>
22362212
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20
22372213
void basic_string<_CharT, _Traits, _Allocator>::__init_with_sentinel(_InputIterator __first, _Sentinel __last) {
2238-
__default_init();
2214+
__r_.first() = __rep();
22392215

22402216
#ifndef _LIBCPP_HAS_NO_EXCEPTIONS
22412217
try
@@ -2335,7 +2311,7 @@ basic_string<_CharT, _Traits, _Allocator>::__grow_by_and_replace
23352311
if (__sec_cp_sz != 0)
23362312
traits_type::copy(std::__to_address(__p) + __n_copy + __n_add,
23372313
std::__to_address(__old_p) + __n_copy + __n_del, __sec_cp_sz);
2338-
if (__old_cap+1 != __min_cap || __libcpp_is_constant_evaluated())
2314+
if (__old_cap+1 != __min_cap)
23392315
__alloc_traits::deallocate(__alloc(), __old_p, __old_cap+1);
23402316
__set_long_pointer(__p);
23412317
__set_long_cap(__allocation.count);
@@ -2375,7 +2351,7 @@ basic_string<_CharT, _Traits, _Allocator>::__grow_by(size_type __old_cap, size_t
23752351
traits_type::copy(std::__to_address(__p) + __n_copy + __n_add,
23762352
std::__to_address(__old_p) + __n_copy + __n_del,
23772353
__sec_cp_sz);
2378-
if (__libcpp_is_constant_evaluated() || __old_cap + 1 != __min_cap)
2354+
if (__old_cap + 1 != __min_cap)
23792355
__alloc_traits::deallocate(__alloc(), __old_p, __old_cap + 1);
23802356
__set_long_pointer(__p);
23812357
__set_long_cap(__allocation.count);
@@ -2538,12 +2514,8 @@ basic_string<_CharT, _Traits, _Allocator>::__move_assign(basic_string& __str, tr
25382514
}
25392515
__move_assign_alloc(__str);
25402516
__r_.first() = __str.__r_.first();
2541-
if (__libcpp_is_constant_evaluated()) {
2542-
__str.__default_init();
2543-
} else {
2544-
__str.__set_short_size(0);
2545-
traits_type::assign(__str.__get_short_pointer()[0], value_type());
2546-
}
2517+
__str.__set_short_size(0);
2518+
traits_type::assign(__str.__get_short_pointer()[0], value_type());
25472519
}
25482520

25492521
#endif
@@ -2829,13 +2801,6 @@ basic_string<_CharT, _Traits, _Allocator>::insert(size_type __pos, const value_t
28292801
if (__pos > __sz)
28302802
__throw_out_of_range();
28312803
size_type __cap = capacity();
2832-
if (__libcpp_is_constant_evaluated()) {
2833-
if (__cap - __sz >= __n)
2834-
__grow_by_and_replace(__cap, 0, __sz, __pos, 0, __n, __s);
2835-
else
2836-
__grow_by_and_replace(__cap, __sz + __n - __cap, __sz, __pos, 0, __n, __s);
2837-
return *this;
2838-
}
28392804
if (__cap - __sz >= __n)
28402805
{
28412806
if (__n)
@@ -2844,7 +2809,7 @@ basic_string<_CharT, _Traits, _Allocator>::insert(size_type __pos, const value_t
28442809
size_type __n_move = __sz - __pos;
28452810
if (__n_move != 0)
28462811
{
2847-
if (__p + __pos <= __s && __s < __p + __sz)
2812+
if (std::__is_pointer_in_range(__p + __pos, __p + __sz, __s))
28482813
__s += __n;
28492814
traits_type::move(__p + __pos + __n, __p + __pos, __n_move);
28502815
}
@@ -3867,7 +3832,7 @@ basic_string<_CharT, _Traits, _Allocator>::__clear_and_shrink() _NOEXCEPT
38673832
if(__is_long())
38683833
{
38693834
__alloc_traits::deallocate(__alloc(), __get_long_pointer(), capacity() + 1);
3870-
__default_init();
3835+
__r_.first() = __rep();
38713836
}
38723837
}
38733838

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
11+
// Ensure that strings which fit within the SSO size can be constant-initialized
12+
// as both a global and local.
13+
14+
#include <string>
15+
16+
#if __SIZE_WIDTH__ == 64
17+
#define LONGEST_STR "0123456789012345678901"
18+
#elif __SIZE_WIDTH__ == 32
19+
#define LONGEST_STR "0123456789"
20+
#else
21+
# error "std::size_t has an unexpected size"
22+
#endif
23+
24+
constinit std::string g_str = LONGEST_STR;
25+
void fn() {
26+
constexpr std::string l_str = LONGEST_STR;
27+
}

libcxx/test/std/utilities/template.bitset/bitset.members/to_string.pass.cpp

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,6 @@ TEST_CONSTEXPR_CXX23 bool test_to_string() {
4747
std::vector<std::bitset<N> > const cases = get_test_cases<N>();
4848
for (std::size_t c = 0; c != cases.size(); ++c) {
4949
std::bitset<N> const v = cases[c];
50-
#ifndef TEST_HAS_NO_WIDE_CHARACTERS
51-
{
52-
std::wstring s = v.template to_string<wchar_t, std::char_traits<wchar_t>, std::allocator<wchar_t> >();
53-
check_equal(s, v, L'0', L'1');
54-
}
55-
{
56-
std::wstring s = v.template to_string<wchar_t, std::char_traits<wchar_t> >();
57-
check_equal(s, v, L'0', L'1');
58-
}
59-
#endif
6050
{
6151
std::string s = v.template to_string<char>();
6252
check_equal(s, v, '0', '1');
@@ -65,49 +55,64 @@ TEST_CONSTEXPR_CXX23 bool test_to_string() {
6555
std::string s = v.to_string();
6656
check_equal(s, v, '0', '1');
6757
}
68-
#ifndef TEST_HAS_NO_WIDE_CHARACTERS
6958
{
70-
std::wstring s = v.template to_string<wchar_t, std::char_traits<wchar_t>, std::allocator<wchar_t> >('0');
71-
check_equal(s, v, L'0', L'1');
59+
std::string s = v.template to_string<char>('0');
60+
check_equal(s, v, '0', '1');
7261
}
7362
{
74-
std::wstring s = v.template to_string<wchar_t, std::char_traits<wchar_t> >('0');
75-
check_equal(s, v, L'0', L'1');
63+
std::string s = v.to_string('0');
64+
check_equal(s, v, '0', '1');
7665
}
77-
#endif
7866
{
79-
std::string s = v.template to_string<char>('0');
67+
std::string s = v.template to_string<char>('0', '1');
8068
check_equal(s, v, '0', '1');
8169
}
8270
{
83-
std::string s = v.to_string('0');
71+
std::string s = v.to_string('0', '1');
8472
check_equal(s, v, '0', '1');
8573
}
74+
{
75+
std::string s = v.to_string('x', 'y');
76+
check_equal(s, v, 'x', 'y');
77+
}
78+
}
79+
return true;
80+
}
81+
8682
#ifndef TEST_HAS_NO_WIDE_CHARACTERS
83+
template <std::size_t N>
84+
TEST_CONSTEXPR_CXX23 bool test_to_string_wchar() {
85+
std::vector<std::bitset<N> > const cases = get_test_cases<N>();
86+
for (std::size_t c = 0; c != cases.size(); ++c) {
87+
std::bitset<N> const v = cases[c];
8788
{
88-
std::wstring s = v.template to_string<wchar_t, std::char_traits<wchar_t>, std::allocator<wchar_t> >('0', '1');
89+
std::wstring s = v.template to_string<wchar_t, std::char_traits<wchar_t>, std::allocator<wchar_t> >();
8990
check_equal(s, v, L'0', L'1');
9091
}
9192
{
92-
std::wstring s = v.template to_string<wchar_t, std::char_traits<wchar_t> >('0', '1');
93+
std::wstring s = v.template to_string<wchar_t, std::char_traits<wchar_t> >();
9394
check_equal(s, v, L'0', L'1');
9495
}
95-
#endif
9696
{
97-
std::string s = v.template to_string<char>('0', '1');
98-
check_equal(s, v, '0', '1');
97+
std::wstring s = v.template to_string<wchar_t, std::char_traits<wchar_t>, std::allocator<wchar_t> >('0');
98+
check_equal(s, v, L'0', L'1');
9999
}
100100
{
101-
std::string s = v.to_string('0', '1');
102-
check_equal(s, v, '0', '1');
101+
std::wstring s = v.template to_string<wchar_t, std::char_traits<wchar_t> >('0');
102+
check_equal(s, v, L'0', L'1');
103103
}
104104
{
105-
std::string s = v.to_string('x', 'y');
106-
check_equal(s, v, 'x', 'y');
105+
std::wstring s = v.template to_string<wchar_t, std::char_traits<wchar_t>, std::allocator<wchar_t> >('0', '1');
106+
check_equal(s, v, L'0', L'1');
107+
}
108+
{
109+
std::wstring s = v.template to_string<wchar_t, std::char_traits<wchar_t> >('0', '1');
110+
check_equal(s, v, L'0', L'1');
107111
}
108112
}
109113
return true;
110114
}
115+
#endif
111116

112117
int main(int, char**) {
113118
test_to_string<0>();
@@ -130,5 +135,24 @@ int main(int, char**) {
130135
static_assert(test_to_string<65>());
131136
#endif
132137

138+
#ifndef TEST_HAS_NO_WIDE_CHARACTERS
139+
test_to_string_wchar<0>();
140+
test_to_string_wchar<1>();
141+
test_to_string_wchar<31>();
142+
test_to_string_wchar<32>();
143+
test_to_string_wchar<33>();
144+
test_to_string_wchar<63>();
145+
test_to_string_wchar<64>();
146+
test_to_string_wchar<65>();
147+
test_to_string_wchar<1000>(); // not in constexpr because of constexpr evaluation step limits
148+
#if TEST_STD_VER > 20
149+
static_assert(test_to_string_wchar<0>());
150+
static_assert(test_to_string_wchar<1>());
151+
static_assert(test_to_string_wchar<31>());
152+
static_assert(test_to_string_wchar<32>());
153+
static_assert(test_to_string_wchar<33>());
154+
static_assert(test_to_string_wchar<63>());
155+
#endif
156+
#endif
133157
return 0;
134158
}

0 commit comments

Comments
 (0)