Repository: microsoft/STL
Pull request #5409: `<string>`: Fix construction and insertion of `basic_string` with volatile ranges
Author: login: frederick-vs-ja
Labels: name: bug
description: Something isn't working
name: ranges
description: C++20/23 ranges
Created: 2025-04-16T07:08:17Z | Merged: 2025-04-22T22:47:39Z

Formerly, `(const) volatile CharT` arrays were inconsistently handled in legacy iterator pair functions and new functions added in C++23 WG21-P1206R7. In legacy functions, `(const) volatile CharT*` were not specially treated due to `_Is_elem_cptr`, but in P1206R7 functions, they were consistently treated with `(const) CharT*` due to `_Contiguous_range_of`.

This PR chooses to follow the convention of `_Contiguous_range_of` and changes `_Is_elem_cptr` to `_Is_elem_cvptr` to handle `(const) volatile CharT*`. For volatile contiguous ranges, we need to use non-vectorized element-wise assignment.

Fixes #5402.

Linked issue #5402: `<string>`: cannot be constructed from a `volatile` char array using the `from_range` constructor
```cpp
#include <ranges>

volatile char vs[42] = {};
std::string s1(std::begin(vs), std::end(vs));              // ok
std::string s2(std::from_range, vs | std::views::reverse); // ok
std::string s3(std::from_range, vs);                       // not ok

```
https://godbolt.org/z/vqqEP4c86
`s3` should not be rejected, but MSV-STL expected the char array not to be `volatile`, leading to a hard error in the function body.
Linked issue comments: user: login: frederick-vs-ja
created_at: 2025-04-16T03:41:48Z
body: `append_range`, `assign_range`, `insert_range`, and `replace_with_range` also need to be fixed.
author_association: CONTRIBUTOR

Diff:
diff --git a/stl/inc/xstring b/stl/inc/xstring
index 85f7f8a89fe..5d5be98ae12 100644
--- a/stl/inc/xstring
+++ b/stl/inc/xstring
@@ -527,6 +527,49 @@ struct _String_constructor_concat_tag {
     explicit _String_constructor_concat_tag() = default;
 };
 
+template <class _Traits, class _UElem>
+_CONSTEXPR20 void _Traits_copy_batch(_Out_writes_all_(_Count) _Traits_ch_t<_Traits>* const _First1,
+    _In_reads_(_Count) const _UElem* const _First2, const size_t _Count) noexcept {
+    _STL_INTERNAL_STATIC_ASSERT(_Is_any_of_v<_UElem, _Traits_ch_t<_Traits>, volatile _Traits_ch_t<_Traits>>);
+
+    if constexpr (is_volatile_v<_UElem>) {
+        for (size_t _Idx = 0; _Idx != _Count; ++_Idx) {
+            _Traits::assign(_First1[_Idx], _Traits_ch_t<_Traits>{_First2[_Idx]});
+        }
+    } else {
+        (void) _Traits::copy(_First1, _First2, _Count);
+    }
+}
+
+template <class _Traits, class _UElem>
+_CONSTEXPR20 void _Traits_move_batch(_Out_writes_all_(_Count) _Traits_ch_t<_Traits>* const _First1,
+    _In_reads_(_Count) const _UElem* const _First2, const size_t _Count) noexcept {
+    _STL_INTERNAL_STATIC_ASSERT(_Is_any_of_v<_UElem, _Traits_ch_t<_Traits>, volatile _Traits_ch_t<_Traits>>);
+
+    if constexpr (is_volatile_v<_UElem>) {
+        bool _Loop_forward = true;
+
+        for (const _UElem* _Src = _First2; _Src != _First2 + _Count; ++_Src) {
+            if (_First1 == _Src) {
+                _Loop_forward = false;
+                break;
+            }
+        }
+
+        if (_Loop_forward) {
+            for (size_t _Idx = 0; _Idx != _Count; ++_Idx) {
+                _Traits::assign(_First1[_Idx], _Traits_ch_t<_Traits>{_First2[_Idx]});
+            }
+        } else {
+            for (size_t _Idx = _Count; _Idx != 0; --_Idx) {
+                _Traits::assign(_First1[_Idx - 1], _Traits_ch_t<_Traits>{_First2[_Idx - 1]});
+            }
+        }
+    } else {
+        (void) _Traits::move(_First1, _First2, _Count);
+    }
+}
+
 [[noreturn]] inline void _Xlen_string() {
     _Xlength_error("string too long");
 }
@@ -606,8 +649,10 @@ private:
 
     template <class _Iter>
     // TRANSITION, /clr:pure is incompatible with templated static constexpr data members
-    // static constexpr bool _Is_elem_cptr =_Is_any_of_v<_Iter, const _Elem* const, _Elem* const, const _Elem*, _Elem*>;
-    using _Is_elem_cptr = bool_constant<_Is_any_of_v<_Iter, const _Elem* const, _Elem* const, const _Elem*, _Elem*>>;
+    // static constexpr bool _Is_elem_cvptr =
+    //     _Is_any_of_v<remove_const_t<_Iter>, _Elem*, const _Elem*, volatile _Elem*, const volatile _Elem*>;
+    using _Is_elem_cvptr = bool_constant<
+        _Is_any_of_v<remove_const_t<_Iter>, _Elem*, const _Elem*, volatile _Elem*, const volatile _Elem*>>;
 
 #if _HAS_CXX17
     template <class _StringViewIsh>
@@ -803,7 +848,7 @@ public:
         if (_UFirst == _ULast) {
             _Construct_empty();
         } else {
-            if constexpr (_Is_elem_cptr<decltype(_UFirst)>::value) {
+            if constexpr (_Is_elem_cvptr<decltype(_UFirst)>::value) {
                 _Construct<_Construct_strategy::_From_ptr>(
                     _UFirst, _STD _Convert_size<size_type>(static_cast<size_t>(_ULast - _UFirst)));
             } else if constexpr (_Is_cpp17_fwd_iter_v<decltype(_UFirst)>) {
@@ -875,7 +920,7 @@ private:
         if constexpr (_Strat == _Construct_strategy::_From_char) {
             _STL_INTERNAL_STATIC_ASSERT(is_same_v<_Char_or_ptr, _Elem>);
         } else {
-            _STL_INTERNAL_STATIC_ASSERT(_Is_elem_cptr<_Char_or_ptr>::value);
+            _STL_INTERNAL_STATIC_ASSERT(_Is_elem_cvptr<_Char_or_ptr>::value);
         }
 
         if (_Count > max_size()) {
@@ -894,7 +939,7 @@ private:
                 _Traits::assign(_My_data._Bx._Buf, _Count, _Arg);
                 _Traits::assign(_My_data._Bx._Buf[_Count], _Elem());
             } else if constexpr (_Strat == _Construct_strategy::_From_ptr) {
-                _Traits::copy(_My_data._Bx._Buf, _Arg, _Count);
+                _STD _Traits_copy_batch<_Traits>(_My_data._Bx._Buf, _Arg, _Count);
                 _Traits::assign(_My_data._Bx._Buf[_Count], _Elem());
             } else { // _Strat == _Construct_strategy::_From_string
 #ifdef _INSERT_STRING_ANNOTATION
@@ -918,7 +963,7 @@ private:
             _Traits::assign(_Unfancy(_New_ptr), _Count, _Arg);
             _Traits::assign(_Unfancy(_New_ptr)[_Count], _Elem());
         } else if constexpr (_Strat == _Construct_strategy::_From_ptr) {
-            _Traits::copy(_Unfancy(_New_ptr), _Arg, _Count);
+            _STD _Traits_copy_batch<_Traits>(_Unfancy(_New_ptr), _Arg, _Count);
             _Traits::assign(_Unfancy(_New_ptr)[_Count], _Elem());
         } else { // _Strat == _Construct_strategy::_From_string
             _Traits::copy(_Unfancy(_New_ptr), _Arg, _Count + 1);
@@ -1480,21 +1525,21 @@ public:
     }
 
     _CONSTEXPR20 basic_string& append(const basic_string& _Right) {
-        return append(_Right._Mypair._Myval2._Myptr(), _Right._Mypair._Myval2._Mysize);
+        return _Append(_Right._Mypair._Myval2._Myptr(), _Right._Mypair._Myval2._Mysize);
     }
 
     _CONSTEXPR20 basic_string& append(const basic_string& _Right, const size_type _Roff, size_type _Count = npos) {
         // append _Right [_Roff, _Roff + _Count)
         _Right._Mypair._Myval2._Check_offset(_Roff);
         _Count = _Right._Mypair._Myval2._Clamp_suffix_size(_Roff, _Count);
-        return append(_Right._Mypair._Myval2._Myptr() + _Roff, _Count);
+        return _Append(_Right._Mypair._Myval2._Myptr() + _Roff, _Count);
     }
 
 #if _HAS_CXX17
     template <class _StringViewIsh, _Is_string_view_ish<_StringViewIsh> = 0>
     _CONSTEXPR20 basic_string& append(const _StringViewIsh& _Right) {
         const basic_string_view<_Elem, _Traits> _As_view = _Right;
-        return append(_As_view.data(), _Convert_size<size_type>(_As_view.size()));
+        return _Append(_As_view.data(), _Convert_size<size_type>(_As_view.size()));
     }
 
     template <class _StringViewIsh, _Is_string_view_ish<_StringViewIsh> = 0>
@@ -1509,29 +1554,11 @@ public:
     _CONSTEXPR20 basic_string& append(
         _In_reads_(_Count) const _Elem* const _Ptr, _CRT_GUARDOVERFLOW const size_type _Count) {
         // append [_Ptr, _Ptr + _Count)
-        const size_type _Old_size = _Mypair._Myval2._Mysize;
-        if (_Count <= _Mypair._Myval2._Myres - _Old_size) {
-            _ASAN_STRING_MODIFY(*this, _Old_size, _Old_size + _Count);
-            _Mypair._Myval2._Mysize = _Old_size + _Count;
-            _Elem* const _Old_ptr   = _Mypair._Myval2._Myptr();
-            _Traits::move(_Old_ptr + _Old_size, _Ptr, _Count);
-            _Traits::assign(_Old_ptr[_Old_size + _Count], _Elem());
-            return *this;
-        }
-
-        return _Reallocate_grow_by(
-            _Count,
-            [](_Elem* const _New_ptr, const _Elem* const _Old_ptr, const size_type _Old_size, const _Elem* const _Ptr,
-                const size_type _Count) _STATIC_LAMBDA {
-                _Traits::copy(_New_ptr, _Old_ptr, _Old_size);
-                _Traits::copy(_New_ptr + _Old_size, _Ptr, _Count);
-                _Traits::assign(_New_ptr[_Old_size + _Count], _Elem());
-            },
-            _Ptr, _Count);
+        return _Append(_Ptr, _Count);
     }
 
     _CONSTEXPR20 basic_string& append(_In_z_ const _Elem* const _Ptr) { // append [_Ptr, <null>)
-        return append(_Ptr, _Convert_size<size_type>(_Traits::length(_Ptr)));
+        return _Append(_Ptr, _Convert_size<size_type>(_Traits::length(_Ptr)));
     }
 
     _CONSTEXPR20 basic_string& append(_CRT_GUARDOVERFLOW const size_type _Count, const _Elem _Ch) {
@@ -1563,11 +1590,11 @@ public:
         _STD _Adl_verify_range(_First, _Last);
         const auto _UFirst = _STD _Get_unwrapped(_First);
         const auto _ULast  = _STD _Get_unwrapped(_Last);
-        if constexpr (_Is_elem_cptr<decltype(_UFirst)>::value) {
-            return append(_UFirst, _STD _Convert_size<size_type>(static_cast<size_t>(_ULast - _UFirst)));
+        if constexpr (_Is_elem_cvptr<decltype(_UFirst)>::value) {
+            return _Append(_UFirst, _STD _Convert_size<size_type>(static_cast<size_t>(_ULast - _UFirst)));
         } else {
             const basic_string _Right(_UFirst, _ULast, get_allocator());
-            return append(_Right._Mypair._Myval2._Myptr(), _Right._Mypair._Myval2._Mysize);
+            return _Append(_Right._Mypair._Myval2._Myptr(), _Right._Mypair._Myval2._Mysize);
         }
     }
 
@@ -1576,10 +1603,10 @@ public:
     constexpr basic_string& append_range(_Rng&& _Range) {
         if constexpr (_RANGES sized_range<_Rng> && _Contiguous_range_of<_Rng, _Elem>) {
             const auto _Count = _Convert_size<size_type>(_To_unsigned_like(_RANGES size(_Range)));
-            return append(_RANGES data(_Range), _Count);
+            return _Append(_RANGES data(_Range), _Count);
         } else {
             const basic_string _Right(from_range, _Range, get_allocator());
-            return append(_Right._Mypair._Myval2._Myptr(), _Right._Mypair._Myval2._Mysize);
+            return _Append(_Right._Mypair._Myval2._Myptr(), _Right._Mypair._Myval2._Mysize);
         }
     }
 #endif // _HAS_CXX23
@@ -1593,14 +1620,14 @@ public:
         // assign _Right [_Roff, _Roff + _Count)
         _Right._Mypair._Myval2._Check_offset(_Roff);
         _Count = _Right._Mypair._Myval2._Clamp_suffix_size(_Roff, _Count);
-        return assign(_Right._Mypair._Myval2._Myptr() + _Roff, _Count);
+        return _Assign(_Right._Mypair._Myval2._Myptr() + _Roff, _Count);
     }
 
 #if _HAS_CXX17
     template <class _StringViewIsh, _Is_string_view_ish<_StringViewIsh> = 0>
     _CONSTEXPR20 basic_string& assign(const _StringViewIsh& _Right) {
         const basic_string_view<_Elem, _Traits> _As_view = _Right;
-        return assign(_As_view.data(), _Convert_size<size_type>(_As_view.size()));
+        return _Assign(_As_view.data(), _Convert_size<size_type>(_As_view.size()));
     }
 
     template <class _StringViewIsh, _Is_string_view_ish<_StringViewIsh> = 0>
@@ -1615,27 +1642,11 @@ public:
     _CONSTEXPR20 basic_string& assign(
         _In_reads_(_Count) const _Elem* const _Ptr, _CRT_GUARDOVERFLOW const size_type _Count) {
         // assign [_Ptr, _Ptr + _Count)
-        if (_Count <= _Mypair._Myval2._Myres) {
-            _ASAN_STRING_REMOVE(*this);
-            _Elem* const _Old_ptr   = _Mypair._Myval2._Myptr();
-            _Mypair._Myval2._Mysize = _Count;
-            _Traits::move(_Old_ptr, _Ptr, _Count);
-            _Traits::assign(_Old_ptr[_Count], _Elem());
-            _ASAN_STRING_CREATE(*this);
-            return *this;
-        }
-
-        return _Reallocate_for(
-            _Count,
-            [](_Elem* const _New_ptr, const size_type _Count, const _Elem* const _Ptr) _STATIC_LAMBDA {
-                _Traits::copy(_New_ptr, _Ptr, _Count);
-                _Traits::assign(_New_ptr[_Count], _Elem());
-            },
-            _Ptr);
+        return _Assign(_Ptr, _Count);
     }
 
     _CONSTEXPR20 basic_string& assign(_In_z_ const _Elem* const _Ptr) {
-        return assign(_Ptr, _Convert_size<size_type>(_Traits::length(_Ptr)));
+        return _Assign(_Ptr, _Convert_size<size_type>(_Traits::length(_Ptr)));
     }
 
     _CONSTEXPR20 basic_string& assign(_CRT_GUARDOVERFLOW const size_type _Count, const _Elem _Ch) {
@@ -1663,8 +1674,8 @@ public:
         _STD _Adl_verify_range(_First, _Last);
         const auto _UFirst = _STD _Get_unwrapped(_First);
         const auto _ULast  = _STD _Get_unwrapped(_Last);
-        if constexpr (_Is_elem_cptr<decltype(_UFirst)>::value) {
-            return assign(_UFirst, _STD _Convert_size<size_type>(static_cast<size_t>(_ULast - _UFirst)));
+        if constexpr (_Is_elem_cvptr<decltype(_UFirst)>::value) {
+            return _Assign(_UFirst, _STD _Convert_size<size_type>(static_cast<size_t>(_ULast - _UFirst)));
         } else {
             basic_string _Right(_UFirst, _ULast, get_allocator());
             if (_Mypair._Myval2._Myres < _Right._Mypair._Myval2._Myres) {
@@ -1672,7 +1683,7 @@ public:
                 _Swap_data(_Right);
                 return *this;
             } else {
-                return assign(_Right._Mypair._Myval2._Myptr(), _Right._Mypair._Myval2._Mysize);
+                return _Assign(_Right._Mypair._Myval2._Myptr(), _Right._Mypair._Myval2._Mysize);
             }
         }
     }
@@ -1682,7 +1693,7 @@ public:
     constexpr basic_string& assign_range(_Rng&& _Range) {
         if constexpr (_RANGES sized_range<_Rng> && _Contiguous_range_of<_Rng, _Elem>) {
             const auto _Count = _Convert_size<size_type>(_To_unsigned_like(_RANGES size(_Range)));
-            return assign(_RANGES data(_Range), _Count);
+            return _Assign(_RANGES data(_Range), _Count);
         } else {
             basic_string _Right(from_range, _Range, get_allocator());
             if (_Mypair._Myval2._Myres < _Right._Mypair._Myval2._Myres) {
@@ -1690,7 +1701,7 @@ public:
                 _Swap_data(_Right);
                 return *this;
             } else {
-                return assign(_Right._Mypair._Myval2._Myptr(), _Right._Mypair._Myval2._Mysize);
+                return _Assign(_Right._Mypair._Myval2._Myptr(), _Right._Mypair._Myval2._Mysize);
             }
         }
     }
@@ -1698,7 +1709,7 @@ public:
 
     _CONSTEXPR20 basic_string& insert(const size_type _Off, const basic_string& _Right) {
         // insert _Right at _Off
-        return insert(_Off, _Right._Mypair._Myval2._Myptr(), _Right._Mypair._Myval2._Mysize);
+        return _Insert(_Off, _Right._Mypair._Myval2._Myptr(), _Right._Mypair._Myval2._Mysize);
     }
 
     _CONSTEXPR20 basic_string& insert(
@@ -1706,7 +1717,7 @@ public:
         // insert _Right [_Roff, _Roff + _Count) at _Off
         _Right._Mypair._Myval2._Check_offset(_Roff);
         _Count = _Right._Mypair._Myval2._Clamp_suffix_size(_Roff, _Count);
-        return insert(_Off, _Right._Mypair._Myval2._Myptr() + _Roff, _Count);
+        return _Insert(_Off, _Right._Mypair._Myval2._Myptr() + _Roff, _Count);
     }
 
 #if _HAS_CXX17
@@ -1714,7 +1725,7 @@ public:
     _CONSTEXPR20 basic_string& insert(const size_type _Off, const _StringViewIsh& _Right) {
         // insert _Right at _Off
         const basic_string_view<_Elem, _Traits> _As_view = _Right;
-        return insert(_Off, _As_view.data(), _Convert_size<size_type>(_As_view.size()));
+        return _Insert(_Off, _As_view.data(), _Convert_size<size_type>(_As_view.size()));
     }
 
     template <class _StringViewIsh, _Is_string_view_ish<_StringViewIsh> = 0>
@@ -1729,55 +1740,12 @@ public:
     _CONSTEXPR20 basic_string& insert(
         const size_type _Off, _In_reads_(_Count) const _Elem* const _Ptr, _CRT_GUARDOVERFLOW const size_type _Count) {
         // insert [_Ptr, _Ptr + _Count) at _Off
-        _Mypair._Myval2._Check_offset(_Off);
-        const size_type _Old_size = _Mypair._Myval2._Mysize;
-
-        // We can't check for overlapping ranges when constant evaluated since comparison of pointers into string
-        // literals is unspecified, so always reallocate and copy to the new buffer if constant evaluated.
-#if _HAS_CXX20
-        const bool _Check_overlap = _Count <= _Mypair._Myval2._Myres - _Old_size && !_STD is_constant_evaluated();
-#else // ^^^ _HAS_CXX20 / !_HAS_CXX20 vvv
-        const bool _Check_overlap = _Count <= _Mypair._Myval2._Myres - _Old_size;
-#endif // ^^^ !_HAS_CXX20 ^^^
-
-        if (_Check_overlap) {
-            _ASAN_STRING_MODIFY(*this, _Old_size, _Old_size + _Count);
-            _Mypair._Myval2._Mysize = _Old_size + _Count;
-            _Elem* const _Old_ptr   = _Mypair._Myval2._Myptr();
-            _Elem* const _Insert_at = _Old_ptr + _Off;
-            // the range [_Ptr, _Ptr + _Ptr_shifted_after) is left alone by moving the suffix out,
-            // while the range [_Ptr + _Ptr_shifted_after, _Ptr + _Count) shifts down by _Count
-            size_type _Ptr_shifted_after;
-            if (_Ptr + _Count <= _Insert_at || _Ptr > _Old_ptr + _Old_size) {
-                // inserted content is before the shifted region, or does not alias
-                _Ptr_shifted_after = _Count; // none of _Ptr's data shifts
-            } else if (_Insert_at <= _Ptr) { // all of [_Ptr, _Ptr + _Count) shifts
-                _Ptr_shifted_after = 0;
-            } else { // [_Ptr, _Ptr + _Count) contains _Insert_at, so only the part after _Insert_at shifts
-                _Ptr_shifted_after = static_cast<size_type>(_Insert_at - _Ptr);
-            }
-
-            _Traits::move(_Insert_at + _Count, _Insert_at, _Old_size - _Off + 1); // move suffix + null down
-            _Traits::copy(_Insert_at, _Ptr, _Ptr_shifted_after);
-            _Traits::copy(
-                _Insert_at + _Ptr_shifted_after, _Ptr + _Count + _Ptr_shifted_after, _Count - _Ptr_shifted_after);
-            return *this;
-        }
-
-        return _Reallocate_grow_by(
-            _Count,
-            [](_Elem* const _New_ptr, const _Elem* const _Old_ptr, const size_type _Old_size, const size_type _Off,
-                const _Elem* const _Ptr, const size_type _Count) _STATIC_LAMBDA {
-                _Traits::copy(_New_ptr, _Old_ptr, _Off);
-                _Traits::copy(_New_ptr + _Off, _Ptr, _Count);
-                _Traits::copy(_New_ptr + _Off + _Count, _Old_ptr + _Off, _Old_size - _Off + 1);
-            },
-            _Off, _Ptr, _Count);
+        return _Insert(_Off, _Ptr, _Count);
     }
 
     _CONSTEXPR20 basic_string& insert(const size_type _Off, _In_z_ const _Elem* const _Ptr) {
         // insert [_Ptr, <null>) at _Off
-        return insert(_Off, _Ptr, _Convert_size<size_type>(_Traits::length(_Ptr)));
+        return _Insert(_Off, _Ptr, _Convert_size<size_type>(_Traits::length(_Ptr)));
     }
 
     _CONSTEXPR20 basic_string& insert(
@@ -1836,11 +1804,11 @@ public:
         _STD _Adl_verify_range(_First, _Last);
         const auto _UFirst = _STD _Get_unwrapped(_First);
         const auto _ULast  = _STD _Get_unwrapped(_Last);
-        if constexpr (_Is_elem_cptr<decltype(_UFirst)>::value) {
-            insert(_Off, _UFirst, _STD _Convert_size<size_type>(static_cast<size_t>(_ULast - _UFirst)));
+        if constexpr (_Is_elem_cvptr<decltype(_UFirst)>::value) {
+            _Insert(_Off, _UFirst, _STD _Convert_size<size_type>(static_cast<size_t>(_ULast - _UFirst)));
         } else {
             const basic_string _Right(_UFirst, _ULast, get_allocator());
-            insert(_Off, _Right._Mypair._Myval2._Myptr(), _Right._Mypair._Myval2._Mysize);
+            _Insert(_Off, _Right._Mypair._Myval2._Myptr(), _Right._Mypair._Myval2._Mysize);
         }
 
         return begin() + static_cast<difference_type>(_Off);
@@ -1856,10 +1824,10 @@ public:
 
         if constexpr (_RANGES sized_range<_Rng> && _Contiguous_range_of<_Rng, _Elem>) {
             const auto _Count = _Convert_size<size_type>(_To_unsigned_like(_RANGES size(_Range)));
-            insert(_Off, _RANGES data(_Range), _Count);
+            _Insert(_Off, _RANGES data(_Range), _Count);
         } else {
             const basic_string _Right(from_range, _Range, get_allocator());
-            insert(_Off, _Right._Mypair._Myval2._Myptr(), _Right._Mypair._Myval2._Mysize);
+            _Insert(_Off, _Right._Mypair._Myval2._Myptr(), _Right._Mypair._Myval2._Mysize);
         }
 
         return begin() + static_cast<difference_type>(_Off);
@@ -1918,7 +1886,7 @@ public:
 
     _CONSTEXPR20 basic_string& replace(const size_type _Off, const size_type _Nx, const basic_string& _Right) {
         // replace [_Off, _Off + _Nx) with _Right
-        return replace(_Off, _Nx, _Right._Mypair._Myval2._Myptr(), _Right._Mypair._Myval2._Mysize);
+        return _Replace(_Off, _Nx, _Right._Mypair._Myval2._Myptr(), _Right._Mypair._Myval2._Mysize);
     }
 
     _CONSTEXPR20 basic_string& replace(const size_type _Off, size_type _Nx, const basic_string& _Right,
@@ -1926,7 +1894,7 @@ public:
         // replace [_Off, _Off + _Nx) with _Right [_Roff, _Roff + _Count)
         _Right._Mypair._Myval2._Check_offset(_Roff);
         _Count = _Right._Mypair._Myval2._Clamp_suffix_size(_Roff, _Count);
-        return replace(_Off, _Nx, _Right._Mypair._Myval2._Myptr() + _Roff, _Count);
+        return _Replace(_Off, _Nx, _Right._Mypair._Myval2._Myptr() + _Roff, _Count);
     }
 
 #if _HAS_CXX17
@@ -1934,7 +1902,7 @@ public:
     _CONSTEXPR20 basic_string& replace(const size_type _Off, const size_type _Nx, const _StringViewIsh& _Right) {
         // replace [_Off, _Off + _Nx) with _Right
         basic_string_view<_Elem, _Traits> _As_view = _Right;
-        return replace(_Off, _Nx, _As_view.data(), _Convert_size<size_type>(_As_view.size()));
+        return _Replace(_Off, _Nx, _As_view.data(), _Convert_size<size_type>(_As_view.size()));
     }
 
     template <class _StringViewIsh, _Is_string_view_ish<_StringViewIsh> = 0>
@@ -1949,78 +1917,12 @@ public:
     _CONSTEXPR20 basic_string& replace(
         const size_type _Off, size_type _Nx, _In_reads_(_Count) const _Elem* const _Ptr, const size_type _Count) {
         // replace [_Off, _Off + _Nx) with [_Ptr, _Ptr + _Count)
-        _Mypair._Myval2._Check_offset(_Off);
-        _Nx = _Mypair._Myval2._Clamp_suffix_size(_Off, _Nx);
-        if (_Nx == _Count) { // size doesn't change, so a single move does the trick
-            _Traits::move(_Mypair._Myval2._Myptr() + _Off, _Ptr, _Count);
-            return *this;
-        }
-
-        const size_type _Old_size    = _Mypair._Myval2._Mysize;
-        const size_type _Suffix_size = _Old_size - _Nx - _Off + 1;
-        if (_Count < _Nx) { // suffix shifts backwards; we don't have to move anything out of the way
-            _Elem* const _Old_ptr   = _Mypair._Myval2._Myptr();
-            _Elem* const _Insert_at = _Old_ptr + _Off;
-            _Traits::move(_Insert_at, _Ptr, _Count);
-            _Traits::move(_Insert_at + _Count, _Insert_at + _Nx, _Suffix_size);
-
-            const auto _New_size = _Old_size - (_Nx - _Count);
-            _ASAN_STRING_MODIFY(*this, _Old_size, _New_size);
-            _Mypair._Myval2._Mysize = _New_size;
-            return *this;
-        }
-
-        const size_type _Growth = static_cast<size_type>(_Count - _Nx);
-
-        // checking for overlapping ranges is technically UB (considering string literals), so just always reallocate
-        // and copy to the new buffer if constant evaluated
-#if _HAS_CXX20
-        if (!_STD is_constant_evaluated())
-#endif // _HAS_CXX20
-        {
-            if (_Growth <= _Mypair._Myval2._Myres - _Old_size) { // growth fits
-                _ASAN_STRING_MODIFY(*this, _Old_size, _Old_size + _Growth);
-                _Mypair._Myval2._Mysize = _Old_size + _Growth;
-                _Elem* const _Old_ptr   = _Mypair._Myval2._Myptr();
-                _Elem* const _Insert_at = _Old_ptr + _Off;
-                _Elem* const _Suffix_at = _Insert_at + _Nx;
-
-                size_type _Ptr_shifted_after; // see rationale in insert
-                if (_Ptr + _Count <= _Insert_at || _Ptr > _Old_ptr + _Old_size) {
-                    _Ptr_shifted_after = _Count;
-                } else if (_Suffix_at <= _Ptr) {
-                    _Ptr_shifted_after = 0;
-                } else {
-                    _Ptr_shifted_after = static_cast<size_type>(_Suffix_at - _Ptr);
-                }
-
-                _Traits::move(_Suffix_at + _Growth, _Suffix_at, _Suffix_size);
-                // next case must be move, in case _Ptr begins before _Insert_at and contains part of the hole;
-                // this case doesn't occur in insert because the new content must come from outside the removed
-                // content there (because in insert there is no removed content)
-                _Traits::move(_Insert_at, _Ptr, _Ptr_shifted_after);
-                // the next case can be copy, because it comes from the chunk moved out of the way in the
-                // first move, and the hole we're filling can't alias the chunk we moved out of the way
-                _Traits::copy(
-                    _Insert_at + _Ptr_shifted_after, _Ptr + _Growth + _Ptr_shifted_after, _Count - _Ptr_shifted_after);
-                return *this;
-            }
-        }
-
-        return _Reallocate_grow_by(
-            _Growth,
-            [](_Elem* const _New_ptr, const _Elem* const _Old_ptr, const size_type _Old_size, const size_type _Off,
-                const size_type _Nx, const _Elem* const _Ptr, const size_type _Count) _STATIC_LAMBDA {
-                _Traits::copy(_New_ptr, _Old_ptr, _Off);
-                _Traits::copy(_New_ptr + _Off, _Ptr, _Count);
-                _Traits::copy(_New_ptr + _Off + _Count, _Old_ptr + _Off + _Nx, _Old_size - _Nx - _Off + 1);
-            },
-            _Off, _Nx, _Ptr, _Count);
+        return _Replace(_Off, _Nx, _Ptr, _Count);
     }
 
     _CONSTEXPR20 basic_string& replace(const size_type _Off, const size_type _Nx, _In_z_ const _Elem* const _Ptr) {
         // replace [_Off, _Off + _Nx) with [_Ptr, <null>)
-        return replace(_Off, _Nx, _Ptr, _Convert_size<size_type>(_Traits::length(_Ptr)));
+        return _Replace(_Off, _Nx, _Ptr, _Convert_size<size_type>(_Traits::length(_Ptr)));
     }
 
     _CONSTEXPR20 basic_string& replace(const size_type _Off, size_type _Nx, const size_type _Count, const _Elem _Ch) {
@@ -2129,12 +2031,12 @@ public:
         _STD _Adl_verify_range(_First2, _Last2);
         const auto _UFirst2 = _STD _Get_unwrapped(_First2);
         const auto _ULast2  = _STD _Get_unwrapped(_Last2);
-        if constexpr (_Is_elem_cptr<decltype(_UFirst2)>::value) {
-            return replace(
+        if constexpr (_Is_elem_cvptr<decltype(_UFirst2)>::value) {
+            return _Replace(
                 _Off, _Length, _UFirst2, _STD _Convert_size<size_type>(static_cast<size_t>(_ULast2 - _UFirst2)));
         } else {
             const basic_string _Right(_UFirst2, _ULast2, get_allocator());
-            return replace(_Off, _Length, _Right._Mypair._Myval2._Myptr(), _Right._Mypair._Myval2._Mysize);
+            return _Replace(_Off, _Length, _Right._Mypair._Myval2._Myptr(), _Right._Mypair._Myval2._Mysize);
         }
     }
 
@@ -2150,10 +2052,10 @@ public:
 
         if constexpr (_RANGES sized_range<_Rng> && _Contiguous_range_of<_Rng, _Elem>) {
             const auto _Count = _Convert_size<size_type>(_To_unsigned_like(_RANGES size(_Range)));
-            return replace(_Off, _Length, _RANGES data(_Range), _Count);
+            return _Replace(_Off, _Length, _RANGES data(_Range), _Count);
         } else {
             const basic_string _Right(from_range, _Range, get_allocator());
-            return replace(_Off, _Length, _Right._Mypair._Myval2._Myptr(), _Right._Mypair._Myval2._Mysize);
+            return _Replace(_Off, _Length, _Right._Mypair._Myval2._Myptr(), _Right._Mypair._Myval2._Mysize);
         }
     }
 #endif // _HAS_CXX23
@@ -3077,6 +2979,185 @@ private:
         _Traits::assign(_Mypair._Myval2._Myptr()[_New_size], _Elem());
     }
 
+    template <class _UElem>
+    _CONSTEXPR20 basic_string& _Append(
+        _In_reads_(_Count) const _UElem* const _Ptr, _CRT_GUARDOVERFLOW const size_type _Count) {
+        // append [_Ptr, _Ptr + _Count), handling volatile-qualified _Elem
+        _STL_INTERNAL_STATIC_ASSERT(_Is_any_of_v<_UElem, _Elem, volatile _Elem>);
+
+        const size_type _Old_size = _Mypair._Myval2._Mysize;
+        if (_Count <= _Mypair._Myval2._Myres - _Old_size) {
+            _ASAN_STRING_MODIFY(*this, _Old_size, _Old_size + _Count);
+            _Mypair._Myval2._Mysize = _Old_size + _Count;
+            _Elem* const _Old_ptr   = _Mypair._Myval2._Myptr();
+            _STD _Traits_move_batch<_Traits>(_Old_ptr + _Old_size, _Ptr, _Count);
+            _Traits::assign(_Old_ptr[_Old_size + _Count], _Elem());
+            return *this;
+        }
+
+        return _Reallocate_grow_by(
+            _Count,
+            [](_Elem* const _New_ptr, const _Elem* const _Old_ptr, const size_type _Old_size, const _UElem* const _Ptr,
+                const size_type _Count) _STATIC_LAMBDA {
+                _Traits::copy(_New_ptr, _Old_ptr, _Old_size);
+                _STD _Traits_copy_batch<_Traits>(_New_ptr + _Old_size, _Ptr, _Count);
+                _Traits::assign(_New_ptr[_Old_size + _Count], _Elem());
+            },
+            _Ptr, _Count);
+    }
+
+    template <class _UElem>
+    _CONSTEXPR20 basic_string& _Assign(
+        _In_reads_(_Count) const _UElem* const _Ptr, _CRT_GUARDOVERFLOW const size_type _Count) {
+        // assign [_Ptr, _Ptr + _Count), handling volatile-qualified _Elem
+        _STL_INTERNAL_STATIC_ASSERT(_Is_any_of_v<_UElem, _Elem, volatile _Elem>);
+
+        if (_Count <= _Mypair._Myval2._Myres) {
+            _ASAN_STRING_REMOVE(*this);
+            _Elem* const _Old_ptr   = _Mypair._Myval2._Myptr();
+            _Mypair._Myval2._Mysize = _Count;
+            _STD _Traits_move_batch<_Traits>(_Old_ptr, _Ptr, _Count);
+            _Traits::assign(_Old_ptr[_Count], _Elem());
+            _ASAN_STRING_CREATE(*this);
+            return *this;
+        }
+
+        return _Reallocate_for(
+            _Count,
+            [](_Elem* const _New_ptr, const size_type _Count, const _UElem* const _Ptr) _STATIC_LAMBDA {
+                _STD _Traits_copy_batch<_Traits>(_New_ptr, _Ptr, _Count);
+                _Traits::assign(_New_ptr[_Count], _Elem());
+            },
+            _Ptr);
+    }
+
+    template <class _UElem>
+    _CONSTEXPR20 basic_string& _Insert(
+        const size_type _Off, _In_reads_(_Count) const _UElem* const _Ptr, _CRT_GUARDOVERFLOW const size_type _Count) {
+        // insert [_Ptr, _Ptr + _Count) at _Off, handling volatile-qualified _Elem
+        _STL_INTERNAL_STATIC_ASSERT(_Is_any_of_v<_UElem, _Elem, volatile _Elem>);
+
+        _Mypair._Myval2._Check_offset(_Off);
+        const size_type _Old_size = _Mypair._Myval2._Mysize;
+
+        // We can't check for overlapping ranges when constant evaluated since comparison of pointers into string
+        // literals is unspecified, so always reallocate and copy to the new buffer if constant evaluated.
+#if _HAS_CXX20
+        const bool _Check_overlap = _Count <= _Mypair._Myval2._Myres - _Old_size && !_STD is_constant_evaluated();
+#else // ^^^ _HAS_CXX20 / !_HAS_CXX20 vvv
+        const bool _Check_overlap = _Count <= _Mypair._Myval2._Myres - _Old_size;
+#endif // ^^^ !_HAS_CXX20 ^^^
+
+        if (_Check_overlap) {
+            _ASAN_STRING_MODIFY(*this, _Old_size, _Old_size + _Count);
+            _Mypair._Myval2._Mysize = _Old_size + _Count;
+            _Elem* const _Old_ptr   = _Mypair._Myval2._Myptr();
+            _Elem* const _Insert_at = _Old_ptr + _Off;
+            // the range [_Ptr, _Ptr + _Ptr_shifted_after) is left alone by moving the suffix out,
+            // while the range [_Ptr + _Ptr_shifted_after, _Ptr + _Count) shifts down by _Count
+            size_type _Ptr_shifted_after;
+            if (_Ptr + _Count <= _Insert_at || _Ptr > _Old_ptr + _Old_size) {
+                // inserted content is before the shifted region, or does not alias
+                _Ptr_shifted_after = _Count; // none of _Ptr's data shifts
+            } else if (_Insert_at <= _Ptr) { // all of [_Ptr, _Ptr + _Count) shifts
+                _Ptr_shifted_after = 0;
+            } else { // [_Ptr, _Ptr + _Count) contains _Insert_at, so only the part after _Insert_at shifts
+                _Ptr_shifted_after = static_cast<size_type>(_Insert_at - _Ptr);
+            }
+
+            _Traits::move(_Insert_at + _Count, _Insert_at, _Old_size - _Off + 1); // move suffix + null down
+            _STD _Traits_copy_batch<_Traits>(_Insert_at, _Ptr, _Ptr_shifted_after);
+            _STD _Traits_copy_batch<_Traits>(
+                _Insert_at + _Ptr_shifted_after, _Ptr + _Count + _Ptr_shifted_after, _Count - _Ptr_shifted_after);
+            return *this;
+        }
+
+        return _Reallocate_grow_by(
+            _Count,
+            [](_Elem* const _New_ptr, const _Elem* const _Old_ptr, const size_type _Old_size, const size_type _Off,
+                const _UElem* const _Ptr, const size_type _Count) _STATIC_LAMBDA {
+                _Traits::copy(_New_ptr, _Old_ptr, _Off);
+                _STD _Traits_copy_batch<_Traits>(_New_ptr + _Off, _Ptr, _Count);
+                _Traits::copy(_New_ptr + _Off + _Count, _Old_ptr + _Off, _Old_size - _Off + 1);
+            },
+            _Off, _Ptr, _Count);
+    }
+
+    template <class _UElem>
+    _CONSTEXPR20 basic_string& _Replace(
+        const size_type _Off, size_type _Nx, _In_reads_(_Count) const _UElem* const _Ptr, const size_type _Count) {
+        // replace [_Off, _Off + _Nx) with [_Ptr, _Ptr + _Count), handling volatile-qualified _Elem
+        _STL_INTERNAL_STATIC_ASSERT(_Is_any_of_v<_UElem, _Elem, volatile _Elem>);
+
+        _Mypair._Myval2._Check_offset(_Off);
+        _Nx = _Mypair._Myval2._Clamp_suffix_size(_Off, _Nx);
+        if (_Nx == _Count) { // size doesn't change, so a single move does the trick
+            _STD _Traits_move_batch<_Traits>(_Mypair._Myval2._Myptr() + _Off, _Ptr, _Count);
+            return *this;
+        }
+
+        const size_type _Old_size    = _Mypair._Myval2._Mysize;
+        const size_type _Suffix_size = _Old_size - _Nx - _Off + 1;
+        if (_Count < _Nx) { // suffix shifts backwards; we don't have to move anything out of the way
+            _Elem* const _Old_ptr   = _Mypair._Myval2._Myptr();
+            _Elem* const _Insert_at = _Old_ptr + _Off;
+            _STD _Traits_move_batch<_Traits>(_Insert_at, _Ptr, _Count);
+            _Traits::move(_Insert_at + _Count, _Insert_at + _Nx, _Suffix_size);
+
+            const auto _New_size = _Old_size - (_Nx - _Count);
+            _ASAN_STRING_MODIFY(*this, _Old_size, _New_size);
+            _Mypair._Myval2._Mysize = _New_size;
+            return *this;
+        }
+
+        const size_type _Growth = static_cast<size_type>(_Count - _Nx);
+
+        // checking for overlapping ranges is technically UB (considering string literals), so just always reallocate
+        // and copy to the new buffer if constant evaluated
+#if _HAS_CXX20
+        if (!_STD is_constant_evaluated())
+#endif // _HAS_CXX20
+        {
+            if (_Growth <= _Mypair._Myval2._Myres - _Old_size) { // growth fits
+                _ASAN_STRING_MODIFY(*this, _Old_size, _Old_size + _Growth);
+                _Mypair._Myval2._Mysize = _Old_size + _Growth;
+                _Elem* const _Old_ptr   = _Mypair._Myval2._Myptr();
+                _Elem* const _Insert_at = _Old_ptr + _Off;
+                _Elem* const _Suffix_at = _Insert_at + _Nx;
+
+                size_type _Ptr_shifted_after; // see rationale in insert
+                if (_Ptr + _Count <= _Insert_at || _Ptr > _Old_ptr + _Old_size) {
+                    _Ptr_shifted_after = _Count;
+                } else if (_Suffix_at <= _Ptr) {
+                    _Ptr_shifted_after = 0;
+                } else {
+                    _Ptr_shifted_after = static_cast<size_type>(_Suffix_at - _Ptr);
+                }
+
+                _Traits::move(_Suffix_at + _Growth, _Suffix_at, _Suffix_size);
+                // next case must be move, in case _Ptr begins before _Insert_at and contains part of the hole;
+                // this case doesn't occur in insert because the new content must come from outside the removed
+                // content there (because in insert there is no removed content)
+                _STD _Traits_move_batch<_Traits>(_Insert_at, _Ptr, _Ptr_shifted_after);
+                // the next case can be copy, because it comes from the chunk moved out of the way in the
+                // first move, and the hole we're filling can't alias the chunk we moved out of the way
+                _STD _Traits_copy_batch<_Traits>(
+                    _Insert_at + _Ptr_shifted_after, _Ptr + _Growth + _Ptr_shifted_after, _Count - _Ptr_shifted_after);
+                return *this;
+            }
+        }
+
+        return _Reallocate_grow_by(
+            _Growth,
+            [](_Elem* const _New_ptr, const _Elem* const _Old_ptr, const size_type _Old_size, const size_type _Off,
+                const size_type _Nx, const _UElem* const _Ptr, const size_type _Count) _STATIC_LAMBDA {
+                _Traits::copy(_New_ptr, _Old_ptr, _Off);
+                _STD _Traits_copy_batch<_Traits>(_New_ptr + _Off, _Ptr, _Count);
+                _Traits::copy(_New_ptr + _Off + _Count, _Old_ptr + _Off + _Nx, _Old_size - _Nx - _Off + 1);
+            },
+            _Off, _Nx, _Ptr, _Count);
+    }
+
     _CONSTEXPR20 void _Tidy_deallocate() noexcept { // initialize buffer, deallocating any storage
         auto& _My_data = _Mypair._Myval2;
         _My_data._Orphan_all();
diff --git a/tests/std/test.lst b/tests/std/test.lst
index 292e4255c97..15d28a83d04 100644
--- a/tests/std/test.lst
+++ b/tests/std/test.lst
@@ -258,6 +258,7 @@ tests\GH_005090_stl_hardening
 tests\GH_005204_regex_collating_ranges
 tests\GH_005244_regex_escape_sequences
 tests\GH_005315_destructor_tombstones
+tests\GH_005402_string_with_volatile_range
 tests\LWG2381_num_get_floating_point
 tests\LWG2597_complex_branch_cut
 tests\LWG3018_shared_ptr_function
diff --git a/tests/std/tests/GH_005402_string_with_volatile_range/env.lst b/tests/std/tests/GH_005402_string_with_volatile_range/env.lst
new file mode 100644
index 00000000000..19f025bd0e6
--- /dev/null
+++ b/tests/std/tests/GH_005402_string_with_volatile_range/env.lst
@@ -0,0 +1,4 @@
+# Copyright (c) Microsoft Corporation.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+RUNALL_INCLUDE ..\usual_matrix.lst
diff --git a/tests/std/tests/GH_005402_string_with_volatile_range/test.cpp b/tests/std/tests/GH_005402_string_with_volatile_range/test.cpp
new file mode 100644
index 00000000000..4ed2c373830
--- /dev/null
+++ b/tests/std/tests/GH_005402_string_with_volatile_range/test.cpp
@@ -0,0 +1,138 @@
+// Copyright (c) Microsoft Corporation.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include <cassert>
+#include <string>
+
+#if _HAS_CXX23
+#include <ranges>
+#endif // _HAS_CXX23
+
+using namespace std;
+
+template <class Str>
+void test() {
+    using CharT = typename Str::value_type;
+    {
+        volatile CharT arr[1]{CharT{'*'}};
+
+        Str s(begin(arr), end(arr));
+        assert(s.size() == 1);
+        assert(s[0] == CharT{'*'});
+
+        s.append(begin(arr), end(arr));
+        assert(s.size() == 2);
+        assert(s[0] == CharT{'*'});
+        assert(s[1] == CharT{'*'});
+
+        s.assign(begin(arr), end(arr));
+        assert(s.size() == 1);
+        assert(s[0] == CharT{'*'});
+
+        s.insert(s.begin(), begin(arr), end(arr));
+        assert(s.size() == 2);
+        assert(s[0] == CharT{'*'});
+        assert(s[1] == CharT{'*'});
+
+        s.replace(s.begin(), s.end(), begin(arr), end(arr));
+        assert(s.size() == 1);
+        assert(s[0] == CharT{'*'});
+    }
+    {
+        const volatile CharT arr[1]{CharT{'*'}};
+
+        Str s(begin(arr), end(arr));
+        assert(s.size() == 1);
+        assert(s[0] == CharT{'*'});
+
+        s.append(begin(arr), end(arr));
+        assert(s.size() == 2);
+        assert(s[0] == CharT{'*'});
+        assert(s[1] == CharT{'*'});
+
+        s.assign(begin(arr), end(arr));
+        assert(s.size() == 1);
+        assert(s[0] == CharT{'*'});
+
+        s.insert(s.begin(), begin(arr), end(arr));
+        assert(s.size() == 2);
+        assert(s[0] == CharT{'*'});
+        assert(s[1] == CharT{'*'});
+
+        s.replace(s.begin(), s.end(), begin(arr), end(arr));
+        assert(s.size() == 1);
+        assert(s[0] == CharT{'*'});
+    }
+#if _HAS_CXX23
+    {
+        volatile CharT arr[1]{CharT{'*'}};
+
+        Str s = {from_range, arr};
+        assert(s.size() == 1);
+        assert(s[0] == CharT{'*'});
+
+        s.append_range(arr);
+        assert(s.size() == 2);
+        assert(s[0] == CharT{'*'});
+        assert(s[1] == CharT{'*'});
+
+        s.assign_range(arr);
+        assert(s.size() == 1);
+        assert(s[0] == CharT{'*'});
+
+        s.insert_range(s.begin(), arr);
+        assert(s.size() == 2);
+        assert(s[0] == CharT{'*'});
+        assert(s[1] == CharT{'*'});
+
+        s.replace_with_range(s.begin(), s.end(), arr);
+        assert(s.size() == 1);
+        assert(s[0] == CharT{'*'});
+    }
+    {
+        const volatile CharT arr[1]{CharT{'*'}};
+
+        Str s = {from_range, arr};
+        assert(s.size() == 1);
+        assert(s[0] == CharT{'*'});
+
+        s.append_range(arr);
+        assert(s.size() == 2);
+        assert(s[0] == CharT{'*'});
+        assert(s[1] == CharT{'*'});
+
+        s.assign_range(arr);
+        assert(s.size() == 1);
+        assert(s[0] == CharT{'*'});
+
+        s.insert_range(s.begin(), arr);
+        assert(s.size() == 2);
+        assert(s[0] == CharT{'*'});
+        assert(s[1] == CharT{'*'});
+
+        s.replace_with_range(s.begin(), s.end(), arr);
+        assert(s.size() == 1);
+        assert(s[0] == CharT{'*'});
+    }
+#endif // _HAS_CXX23
+}
+
+int main() {
+    test<string>();
+#ifdef __cpp_char8_t
+    test<u8string>();
+#endif // defined(__cpp_char8_t)
+    test<u16string>();
+    test<u32string>();
+    test<wstring>();
+
+#if _HAS_CXX17
+    test<pmr::string>();
+#ifdef __cpp_char8_t
+    test<pmr::u8string>();
+#endif // defined(__cpp_char8_t)
+    test<pmr::u16string>();
+    test<pmr::u32string>();
+    test<pmr::wstring>();
+#endif // _HAS_CXX17
+}


Reviews:
user: login: StephanTLavavej
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-19T16:28:03Z
user: login: StephanTLavavej
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-19T16:29:09Z
user: login: frederick-vs-ja
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-19T16:39:04Z
user: login: StephanTLavavej
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-19T16:44:13Z

Review comments:
diff_hunk: @@ -527,6 +527,49 @@ struct _String_constructor_concat_tag {
     explicit _String_constructor_concat_tag() = default;
 };
 
+template <class _Traits, class _UElem>
+_CONSTEXPR20 void _Traits_copy_batch(_Out_writes_all_(_Count) _Traits_ch_t<_Traits>* const _First1,
+    _In_reads_(_Count) const _UElem* const _First2, const size_t _Count) noexcept {
+    _STL_INTERNAL_STATIC_ASSERT(_Is_any_of_v<_UElem, _Traits_ch_t<_Traits>, volatile _Traits_ch_t<_Traits>>);
+
+    if constexpr (is_volatile_v<_UElem>) {
+        for (size_t _Idx = 0; _Idx != _Count; ++_Idx) {
+            _Traits::assign(_First1[_Idx], _Traits_ch_t<_Traits>(_First2[_Idx]));
path: stl/inc/xstring
user: login: StephanTLavavej
body: `_Traits_ch_t<_Traits>(meow)` is a functional-style cast from `volatile Character` to `Character`, which is then passed to `const Character&`. We generally avoid functional-style casts for fundamental types. Occurs below.
created_at: 2025-04-19T15:44:27Z
author_association: MEMBER
diff_hunk: @@ -3077,6 +2979,185 @@ private:
         _Traits::assign(_Mypair._Myval2._Myptr()[_New_size], _Elem());
     }
 
+    template <class _UElem>
+    _CONSTEXPR20 basic_string& _Append(
+        _In_reads_(_Count) const _UElem* const _Ptr, _CRT_GUARDOVERFLOW const size_type _Count) {
+        // append [_Ptr, _Ptr + _Count), handling volatile-qualified _Elem
+        _STL_INTERNAL_STATIC_ASSERT(_Is_any_of_v<_UElem, _Elem, volatile _Elem>);
+
+        const size_type _Old_size = _Mypair._Myval2._Mysize;
+        if (_Count <= _Mypair._Myval2._Myres - _Old_size) {
+            _ASAN_STRING_MODIFY(*this, _Old_size, _Old_size + _Count);
+            _Mypair._Myval2._Mysize = _Old_size + _Count;
+            _Elem* const _Old_ptr   = _Mypair._Myval2._Myptr();
+            _STD _Traits_move_batch<_Traits>(_Old_ptr + _Old_size, _Ptr, _Count);
+            _Traits::assign(_Old_ptr[_Old_size + _Count], _Elem());
+            return *this;
+        }
+
+        return _Reallocate_grow_by(
+            _Count,
+            [](_Elem* const _New_ptr, const _Elem* const _Old_ptr, const size_type _Old_size, const _UElem* const _Ptr,
+                const size_type _Count) _STATIC_LAMBDA {
+                _Traits::copy(_New_ptr, _Old_ptr, _Old_size);
+                _STD _Traits_copy_batch<_Traits>(_New_ptr + _Old_size, _Ptr, _Count);
+                _Traits::assign(_New_ptr[_Old_size + _Count], _Elem());
+            },
+            _Ptr, _Count);
+    }
+
+    template <class _UElem>
+    _CONSTEXPR20 basic_string& _Assign(
+        _In_reads_(_Count) const _UElem* const _Ptr, _CRT_GUARDOVERFLOW const size_type _Count) {
+        // assign [_Ptr, _Ptr + _Count), handling volatile-qualified _Elem
+        _STL_INTERNAL_STATIC_ASSERT(_Is_any_of_v<_UElem, _Elem, volatile _Elem>);
+
+        if (_Count <= _Mypair._Myval2._Myres) {
+            _ASAN_STRING_REMOVE(*this);
+            _Elem* const _Old_ptr   = _Mypair._Myval2._Myptr();
+            _Mypair._Myval2._Mysize = _Count;
+            _STD _Traits_move_batch<_Traits>(_Old_ptr, _Ptr, _Count);
+            _Traits::assign(_Old_ptr[_Count], _Elem());
+            _ASAN_STRING_CREATE(*this);
+            return *this;
+        }
+
+        return _Reallocate_for(
+            _Count,
+            [](_Elem* const _New_ptr, const size_type _Count, const _UElem* const _Ptr) _STATIC_LAMBDA {
+                _STD _Traits_copy_batch<_Traits>(_New_ptr, _Ptr, _Count);
+                _Traits::assign(_New_ptr[_Count], _Elem());
+            },
+            _Ptr);
+    }
+
+    template <class _UElem>
+    _CONSTEXPR20 basic_string& _Insert(
+        const size_type _Off, _In_reads_(_Count) const _UElem* const _Ptr, _CRT_GUARDOVERFLOW const size_type _Count) {
+        // insert [_Ptr, _Ptr + _Count) at _Off, handling volatile-qualified _Elem
+        _STL_INTERNAL_STATIC_ASSERT(_Is_any_of_v<_UElem, _Elem, volatile _Elem>);
+
+        _Mypair._Myval2._Check_offset(_Off);
+        const size_type _Old_size = _Mypair._Myval2._Mysize;
+
+        // We can't check for overlapping ranges when constant evaluated since comparison of pointers into string
+        // literals is unspecified, so always reallocate and copy to the new buffer if constant evaluated.
+#if _HAS_CXX20
+        const bool _Check_overlap = _Count <= _Mypair._Myval2._Myres - _Old_size && !_STD is_constant_evaluated();
+#else // ^^^ _HAS_CXX20 / !_HAS_CXX20 vvv
+        const bool _Check_overlap = _Count <= _Mypair._Myval2._Myres - _Old_size;
+#endif // ^^^ !_HAS_CXX20 ^^^
+
+        if (_Check_overlap) {
+            _ASAN_STRING_MODIFY(*this, _Old_size, _Old_size + _Count);
+            _Mypair._Myval2._Mysize = _Old_size + _Count;
+            _Elem* const _Old_ptr   = _Mypair._Myval2._Myptr();
+            _Elem* const _Insert_at = _Old_ptr + _Off;
+            // the range [_Ptr, _Ptr + _Ptr_shifted_after) is left alone by moving the suffix out,
+            // while the range [_Ptr + _Ptr_shifted_after, _Ptr + _Count) shifts down by _Count
+            size_type _Ptr_shifted_after;
+            if (_Ptr + _Count <= _Insert_at || _Ptr > _Old_ptr + _Old_size) {
+                // inserted content is before the shifted region, or does not alias
+                _Ptr_shifted_after = _Count; // none of _Ptr's data shifts
+            } else if (_Insert_at <= _Ptr) { // all of [_Ptr, _Ptr + _Count) shifts
+                _Ptr_shifted_after = 0;
+            } else { // [_Ptr, _Ptr + _Count) contains _Insert_at, so only the part after _Insert_at shifts
+                _Ptr_shifted_after = static_cast<size_type>(_Insert_at - _Ptr);
+            }
+
+            _Traits::move(_Insert_at + _Count, _Insert_at, _Old_size - _Off + 1); // move suffix + null down
+            _STD _Traits_move_batch<_Traits>(_Insert_at, _Ptr, _Ptr_shifted_after);
path: stl/inc/xstring
user: login: StephanTLavavej
body: This is changing from `_Traits::copy(_Insert_at, _Ptr, _Ptr_shifted_after);`.

`_Traits_move_batch` appears to have been taken from `replace`/`_Replace`, which has a comment:

```
// next case must be move, in case _Ptr begins before _Insert_at and contains part of the hole;
// this case doesn't occur in insert because the new content must come from outside the removed
// content there (because in insert there is no removed content)
```

I believe that `_Insert` should use `_Traits_copy_batch` here.
created_at: 2025-04-19T16:03:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,137 @@
+// Copyright (c) Microsoft Corporation.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include <cassert>
+#if _HAS_CXX23
+#include <ranges>
+#endif // _HAS_CXX23
path: tests/std/tests/GH_005402_string_with_volatile_range/test.cpp
user: login: StephanTLavavej
body: Nitpick: This interferes with sorting and we should group it separately.

(There are a number of pre-existing cases of this that I'll get around to cleaning up someday)
created_at: 2025-04-19T16:14:54Z
author_association: MEMBER
diff_hunk: @@ -527,6 +527,49 @@ struct _String_constructor_concat_tag {
     explicit _String_constructor_concat_tag() = default;
 };
 
+template <class _Traits, class _UElem>
+_CONSTEXPR20 void _Traits_copy_batch(_Out_writes_all_(_Count) _Traits_ch_t<_Traits>* const _First1,
+    _In_reads_(_Count) const _UElem* const _First2, const size_t _Count) noexcept {
+    _STL_INTERNAL_STATIC_ASSERT(_Is_any_of_v<_UElem, _Traits_ch_t<_Traits>, volatile _Traits_ch_t<_Traits>>);
+
+    if constexpr (is_volatile_v<_UElem>) {
+        for (size_t _Idx = 0; _Idx != _Count; ++_Idx) {
+            _Traits::assign(_First1[_Idx], _Traits_ch_t<_Traits>(_First2[_Idx]));
path: stl/inc/xstring
user: login: frederick-vs-ja
body: Hmm, it seems that there can be weird char-like types such that the list-initialization of `X` from a `volatile X` lvalue calls an `initializer_list` constructor. But as such types are quite contrived, perhaps we can handle them later...
created_at: 2025-04-19T16:39:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -527,6 +527,49 @@ struct _String_constructor_concat_tag {
     explicit _String_constructor_concat_tag() = default;
 };
 
+template <class _Traits, class _UElem>
+_CONSTEXPR20 void _Traits_copy_batch(_Out_writes_all_(_Count) _Traits_ch_t<_Traits>* const _First1,
+    _In_reads_(_Count) const _UElem* const _First2, const size_t _Count) noexcept {
+    _STL_INTERNAL_STATIC_ASSERT(_Is_any_of_v<_UElem, _Traits_ch_t<_Traits>, volatile _Traits_ch_t<_Traits>>);
+
+    if constexpr (is_volatile_v<_UElem>) {
+        for (size_t _Idx = 0; _Idx != _Count; ++_Idx) {
+            _Traits::assign(_First1[_Idx], _Traits_ch_t<_Traits>(_First2[_Idx]));
path: stl/inc/xstring
user: login: StephanTLavavej
body: Thanks. That may be technically true, but in practice I would be happy with "later" being "never". Basically nobody uses exotic character types - at most we rarely see someone trying to use `unsigned int` or fundamental types like that.

(I suppose this could also be written with `static_cast` which would draw even more attention to what's happening; that might be a worthwhile justification.)
created_at: 2025-04-19T16:44:13Z
author_association: MEMBER

PR comments:
user: login: StephanTLavavej
created_at: 2025-04-19T16:30:09Z
body: Thanks! :heart_eyes_cat: This diff looked a lot scarier than it actually was - just had to deal with the code movement.

Please double-check my `_Traits_copy_batch` change.


author_association: MEMBER
user: login: StephanTLavavej
created_at: 2025-04-22T10:14:59Z
body: I'm mirroring this to the MSVC-internal repo - please notify me if any further changes are pushed.
author_association: MEMBER
user: login: StephanTLavavej
created_at: 2025-04-22T22:47:58Z
body: Thanks for fixing everyone's favorite cv-qualifier! :joy_cat: :cat: :cat2:
author_association: MEMBER