[libcxx-commits] [libcxx] [libc++] Avoid non-trivial assignment in `__uninitialized_allocator_copy_impl` (PR #196648)

Yuxuan Chen via libcxx-commits libcxx-commits at lists.llvm.org
Sat May 9 13:38:40 PDT 2026


https://github.com/yuxuanchen1997 updated https://github.com/llvm/llvm-project/pull/196648

>From ccc2b0c27874f397905c95023e515cab5a0502dd Mon Sep 17 00:00:00 2001
From: Yuxuan Chen <ych at meta.com>
Date: Fri, 8 May 2026 14:18:06 -0700
Subject: [PATCH] [libc++] Avoid non-trivial assignment in
 `__uninitialized_allocator_copy_impl`

__uninitialized_allocator_copy_impl has an optimization that replaces allocator_traits::construct with std::copy for raw pointer ranges when the element type is trivially copy constructible and trivially copy assignable.

The copy-assignment trait only checks whether assignment from const T& is trivial. That is weaker than the expression used by std::copy, which evaluates *out = *in. If overload resolution selects a different non-trivial assignment operator for that expression, std::copy can call that operator on uninitialized storage.

Const-qualify the input pointers in the optimized overload instead. This makes the std::copy expression assign from const T&, matching the existing is_trivially_copy_assignable check, preserving the optimized path when that assignment is trivial, and falling back to placement construction otherwise.

Add a vector copy-constructor regression test with a type whose defaulted copy assignment is trivial but whose templated assignment operator is selected for non-const lvalue sources.

Tested with:
build/bin/llvm-lit -q build/runtimes/runtimes-bins/libcxx/test --filter='(vector.cons/copy.pass|uninitialized_allocator_copy\\.pass)'
build/bin/llvm-lit -q build/runtimes/runtimes-bins/libcxx/test --param std=c++20 --filter='vector.cons/copy.pass'
build/bin/llvm-lit -q build/runtimes/runtimes-bins/libcxx/test --param std=c++11 --filter='vector.cons/copy.pass'
---
 .../__memory/uninitialized_algorithms.h       |  2 +-
 .../vector/vector.cons/copy.pass.cpp          | 77 ++++++++++++++++++-
 2 files changed, 77 insertions(+), 2 deletions(-)

diff --git a/libcxx/include/__memory/uninitialized_algorithms.h b/libcxx/include/__memory/uninitialized_algorithms.h
index 9182db4b412e3..2170e8a6fc924 100644
--- a/libcxx/include/__memory/uninitialized_algorithms.h
+++ b/libcxx/include/__memory/uninitialized_algorithms.h
@@ -491,7 +491,7 @@ template <class _Alloc,
                             __allocator_has_trivial_copy_construct_v<_Alloc, _In>,
                         int> = 0>
 _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 _Out*
-__uninitialized_allocator_copy_impl(_Alloc&, _In* __first1, _In* __last1, _Out* __first2) {
+__uninitialized_allocator_copy_impl(_Alloc&, const _In* __first1, const _In* __last1, _Out* __first2) {
   if (__libcpp_is_constant_evaluated()) {
     while (__first1 != __last1) {
       std::__construct_at(std::__to_address(__first2), *__first1);
diff --git a/libcxx/test/std/containers/sequences/vector/vector.cons/copy.pass.cpp b/libcxx/test/std/containers/sequences/vector/vector.cons/copy.pass.cpp
index ce5b42575b46f..9d1f2f2e17768 100644
--- a/libcxx/test/std/containers/sequences/vector/vector.cons/copy.pass.cpp
+++ b/libcxx/test/std/containers/sequences/vector/vector.cons/copy.pass.cpp
@@ -10,14 +10,84 @@
 
 // vector(const vector& v);
 
-#include <vector>
 #include <cassert>
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <memory>
+#include <type_traits>
+#include <vector>
 
 #include "test_macros.h"
 #include "test_allocator.h"
 #include "min_allocator.h"
 #include "asan_testing.h"
 
+#if TEST_STD_VER >= 11
+namespace gh196645 {
+
+class Optional {
+public:
+  Optional()                           = default;
+  Optional(const Optional&)            = default;
+  Optional& operator=(const Optional&) = default;
+  Optional(Optional&&) {}
+
+  template <class Arg>
+  Optional& operator=(Arg&& rhs) {
+    assert(value_ != Poison);
+    value_ = rhs.value_;
+    return *this;
+  }
+
+  TEST_CONSTEXPR_CXX20 bool hasValue() const { return value_ == HasValue; }
+
+private:
+  static constexpr std::uint32_t Poison   = 0xBEBEBEBE;
+  static constexpr std::uint32_t NoValue  = 0;
+  static constexpr std::uint32_t HasValue = 1;
+
+  std::uint32_t value_ = NoValue;
+};
+
+static_assert(std::is_trivially_copy_constructible<Optional>::value, "");
+static_assert(std::is_trivially_copy_assignable<Optional>::value, "");
+static_assert(!std::is_trivially_assignable<Optional&, Optional&>::value, "");
+
+template <class T>
+struct PoisoningAllocator {
+  using value_type = T;
+
+  TEST_CONSTEXPR_CXX20 PoisoningAllocator() = default;
+
+  template <class U>
+  TEST_CONSTEXPR_CXX20 PoisoningAllocator(const PoisoningAllocator<U>&) noexcept {}
+
+  TEST_CONSTEXPR_CXX20 T* allocate(std::size_t n) {
+    T* p = alloc_.allocate(n);
+    if (!TEST_IS_CONSTANT_EVALUATED)
+      std::memset(static_cast<void*>(p), 0xBE, n * sizeof(T));
+    return p;
+  }
+
+  TEST_CONSTEXPR_CXX20 void deallocate(T* p, std::size_t n) noexcept { alloc_.deallocate(p, n); }
+
+  template <class U>
+  friend bool operator==(const PoisoningAllocator&, const PoisoningAllocator<U>&) noexcept {
+    return true;
+  }
+
+  template <class U>
+  friend bool operator!=(const PoisoningAllocator&, const PoisoningAllocator<U>&) noexcept {
+    return false;
+  }
+
+  std::allocator<T> alloc_;
+};
+
+} // namespace gh196645
+#endif // TEST_STD_VER >= 11
+
 template <class C>
 TEST_CONSTEXPR_CXX20 void test(const C& x) {
   typename C::size_type s = x.size();
@@ -93,6 +163,11 @@ TEST_CONSTEXPR_CXX20 bool tests() {
     assert(is_contiguous_container_asan_correct(v));
     assert(is_contiguous_container_asan_correct(v2));
   }
+  {
+    std::vector<gh196645::Optional, gh196645::PoisoningAllocator<gh196645::Optional> > v(1);
+    std::vector<gh196645::Optional, gh196645::PoisoningAllocator<gh196645::Optional> > v2 = v;
+    assert(!v2[0].hasValue());
+  }
 #endif
 
   return true;



More information about the libcxx-commits mailing list