[libcxx-commits] [libcxx] [libc++] Destroy elements when exceptions are thrown in __construct_at_end (PR #167112)

Louis Dionne via libcxx-commits libcxx-commits at lists.llvm.org
Mon Jun 22 20:07:59 PDT 2026


https://github.com/ldionne updated https://github.com/llvm/llvm-project/pull/167112

>From 65cb17f3d14f30626b1c2690c8b9f56713d25b49 Mon Sep 17 00:00:00 2001
From: kisuhorikka <kisuhorikka at gmail.com>
Date: Mon, 15 Dec 2025 21:19:32 +0800
Subject: [PATCH 1/3] [libc++] Destroy elements when exceptions are thrown in
 __construct_at_end

---
 .../__memory/uninitialized_algorithms.h       | 26 +++++++++++++++++++
 libcxx/include/__vector/vector.h              | 10 ++-----
 2 files changed, 28 insertions(+), 8 deletions(-)

diff --git a/libcxx/include/__memory/uninitialized_algorithms.h b/libcxx/include/__memory/uninitialized_algorithms.h
index 34d065dc973e5..09a1b43e00f17 100644
--- a/libcxx/include/__memory/uninitialized_algorithms.h
+++ b/libcxx/include/__memory/uninitialized_algorithms.h
@@ -124,6 +124,18 @@ uninitialized_fill(_ForwardIterator __first, _ForwardIterator __last, const _Tp&
 
 // uninitialized_fill_n
 
+template <class _Alloc, class _ForwardIterator, class _Size, class _Tp>
+_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 _ForwardIterator
+__uninitialized_allocator_fill_n(_Alloc& __alloc, _ForwardIterator __first, _Size __n, const _Tp& __x) {
+  _ForwardIterator __idx = __first;
+  auto __guard           = std::__make_exception_guard([&] { std::__destroy(__first, __idx); });
+  for (; __n > 0; ++__idx, (void)--__n)
+    allocator_traits<_Alloc>::construct(__alloc, std::__to_address(__idx), __x);
+  __guard.__complete();
+
+  return __idx;
+}
+
 template <class _ValueType, class _ForwardIterator, class _Size, class _Tp>
 inline _LIBCPP_HIDE_FROM_ABI _ForwardIterator
 __uninitialized_fill_n(_ForwardIterator __first, _Size __n, const _Tp& __x) {
@@ -143,6 +155,20 @@ uninitialized_fill_n(_ForwardIterator __first, _Size __n, const _Tp& __x) {
   return std::__uninitialized_fill_n<_ValueType>(__first, __n, __x);
 }
 
+// __uninitialized_allocator_value_construct_n
+
+template <class _Alloc, class _ForwardIterator, class _Size>
+_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 _ForwardIterator
+__uninitialized_allocator_value_construct_n(_Alloc& __alloc, _ForwardIterator __first, _Size __n) {
+  auto __idx   = __first;
+  auto __guard = std::__make_exception_guard([&] { std::__destroy(__first, __idx); });
+  for (; __n > 0; ++__idx, (void)--__n)
+    allocator_traits<_Alloc>::construct(__alloc, std::__to_address(__idx));
+  __guard.__complete();
+
+  return __idx;
+}
+
 #if _LIBCPP_STD_VER >= 17
 
 // uninitialized_default_construct
diff --git a/libcxx/include/__vector/vector.h b/libcxx/include/__vector/vector.h
index 316d3a9d10eff..14c387414518e 100644
--- a/libcxx/include/__vector/vector.h
+++ b/libcxx/include/__vector/vector.h
@@ -941,10 +941,7 @@ vector<_Tp, _Allocator>::__recommend(size_type __new_size) const {
 template <class _Tp, class _Allocator>
 _LIBCPP_CONSTEXPR_SINCE_CXX20 void vector<_Tp, _Allocator>::__construct_at_end(size_type __n) {
   _ConstructTransaction __tx(*this, __n);
-  const_pointer __new_end = __tx.__new_end_;
-  for (pointer __pos = __tx.__pos_; __pos != __new_end; __tx.__pos_ = ++__pos) {
-    __alloc_traits::construct(this->__alloc_, std::__to_address(__pos));
-  }
+  __tx.__pos_ = std::__uninitialized_allocator_value_construct_n(this->__alloc_, this->__end_, __n);
 }
 
 //  Copy constructs __n objects starting at __end_ from __x
@@ -957,10 +954,7 @@ template <class _Tp, class _Allocator>
 _LIBCPP_CONSTEXPR_SINCE_CXX20 inline void
 vector<_Tp, _Allocator>::__construct_at_end(size_type __n, const_reference __x) {
   _ConstructTransaction __tx(*this, __n);
-  const_pointer __new_end = __tx.__new_end_;
-  for (pointer __pos = __tx.__pos_; __pos != __new_end; __tx.__pos_ = ++__pos) {
-    __alloc_traits::construct(this->__alloc_, std::__to_address(__pos), __x);
-  }
+  __tx.__pos_ = std::__uninitialized_allocator_fill_n(this->__alloc_, this->__end_, __n, __x);
 }
 
 template <class _Tp, class _Allocator>

>From 3ed114d1682c5f86d792207703b15e12dc0becc4 Mon Sep 17 00:00:00 2001
From: Louis Dionne <ldionne.2 at gmail.com>
Date: Thu, 18 Jun 2026 17:27:53 -0400
Subject: [PATCH 2/3] [libc++] Add tests for element destruction in
 __construct_at_end exceptions

---
 .../std/containers/sequences/vector/common.h  | 22 +++++
 .../resize_size_exceptions.pass.cpp           | 81 ++++++++++++++++++
 .../resize_size_value_exceptions.pass.cpp     | 85 +++++++++++++++++++
 .../vector.cons/assign_size_value.pass.cpp    | 34 ++++++++
 .../insert_iter_size_value.pass.cpp           | 34 ++++++++
 5 files changed, 256 insertions(+)

diff --git a/libcxx/test/std/containers/sequences/vector/common.h b/libcxx/test/std/containers/sequences/vector/common.h
index 34453f8889b73..768d7f03fd703 100644
--- a/libcxx/test/std/containers/sequences/vector/common.h
+++ b/libcxx/test/std/containers/sequences/vector/common.h
@@ -110,6 +110,28 @@ struct move_only_throwing_t {
 
 #endif
 
+// A type whose default constructor throws after a configurable number of
+// successful constructions. Useful for tests that need to value construct
+// objects and ensure exception safety when construction throws.
+//
+// Has a non-throwing (const T&) constructor for setting up the vector with
+// known content before the test starts.
+template <typename T>
+struct throwing_default_t {
+  T data_;
+  static int throw_after;
+
+  throwing_default_t() : data_() {
+    if (throw_after == 0)
+      throw 0;
+    --throw_after;
+  }
+
+  throwing_default_t(const T& data) : data_(data) {}
+};
+template <typename T>
+int throwing_default_t<T>::throw_after = 0;
+
 template <typename T>
 struct throwing_data {
   T data_;
diff --git a/libcxx/test/std/containers/sequences/vector/vector.capacity/resize_size_exceptions.pass.cpp b/libcxx/test/std/containers/sequences/vector/vector.capacity/resize_size_exceptions.pass.cpp
index b5e12ea8e5d0a..33b8ace5ec5cc 100644
--- a/libcxx/test/std/containers/sequences/vector/vector.capacity/resize_size_exceptions.pass.cpp
+++ b/libcxx/test/std/containers/sequences/vector/vector.capacity/resize_size_exceptions.pass.cpp
@@ -95,6 +95,36 @@ void test_copy_ctor_exception_for_strong_guarantee(std::vector<throwing_data<T>,
   }
 }
 
+// Test the no-reallocation path of resize(n): reserve enough capacity ahead
+// of time so resize fits within it, and trigger the throw partway through
+// constructing the new elements.
+template <typename T, typename Alloc>
+void test_in_place_default_ctor_exception_for_strong_guarantee(
+    std::vector<throwing_default_t<T>, Alloc>& v, const std::vector<T>& values) {
+  assert(v.empty() && !values.empty());
+  std::size_t new_size = 2 * values.size();
+  v.reserve(new_size);
+  for (std::size_t i = 0; i < values.size(); ++i)
+    v.emplace_back(values[i]);
+
+  throwing_default_t<T>* old_data = v.data();
+  std::size_t old_size            = v.size();
+  std::size_t old_cap             = v.capacity();
+
+  // Trigger an exception halfway through new-element default construction.
+  throwing_default_t<T>::throw_after = static_cast<int>(values.size() / 2);
+
+  try {
+    v.resize(new_size);
+  } catch (...) {
+    assert(v.data() == old_data);
+    assert(v.size() == old_size);
+    assert(v.capacity() == old_cap);
+    for (std::size_t i = 0; i < v.size(); ++i)
+      assert(v[i].data_ == values[i]);
+  }
+}
+
 #if TEST_STD_VER >= 11
 
 template <typename T, typename Alloc>
@@ -287,6 +317,56 @@ void test_default_ctor_exceptions() {
     test_default_ctor_exception_for_strong_guarantee(v, in);
   }
   check_new_delete_called();
+
+  // Same per-allocator coverage but for the no-reallocation path of resize(n).
+  // See https://llvm.org/PR59293.
+  {
+    {
+      int a[] = {1, 2, 3, 4, 5};
+      std::vector<int> in(a, a + sizeof(a) / sizeof(a[0]));
+      std::vector<throwing_default_t<int> > v;
+      test_in_place_default_ctor_exception_for_strong_guarantee(v, in);
+    }
+    check_new_delete_called();
+
+    {
+      int a[] = {1, 2, 3, 4, 5};
+      std::vector<int> in(a, a + sizeof(a) / sizeof(a[0]));
+      std::vector<throwing_default_t<int>, min_allocator<throwing_default_t<int> > > v;
+      test_in_place_default_ctor_exception_for_strong_guarantee(v, in);
+    }
+    check_new_delete_called();
+
+    {
+      std::vector<int> in(10, 42);
+      std::vector<throwing_default_t<int>, safe_allocator<throwing_default_t<int> > > v;
+      test_in_place_default_ctor_exception_for_strong_guarantee(v, in);
+    }
+    check_new_delete_called();
+
+    {
+      std::vector<int> in(10, 42);
+      std::vector<throwing_default_t<int>, test_allocator<throwing_default_t<int> > > v;
+      test_in_place_default_ctor_exception_for_strong_guarantee(v, in);
+    }
+    check_new_delete_called();
+
+    {
+      std::vector<int> in(10, 42);
+      std::vector<throwing_default_t<int>, limited_allocator<throwing_default_t<int>, 100> > v;
+      test_in_place_default_ctor_exception_for_strong_guarantee(v, in);
+    }
+    check_new_delete_called();
+
+#if TEST_STD_VER >= 23
+    {
+      std::vector<int> in{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+      std::vector<throwing_default_t<int>, increasing_allocator<throwing_default_t<int>>> v;
+      test_in_place_default_ctor_exception_for_strong_guarantee(v, in);
+    }
+    check_new_delete_called();
+#endif
+  }
 }
 
 // Check the strong exception guarantee during copy-constructor failures
@@ -391,4 +471,5 @@ int main(int, char**) {
 #if TEST_STD_VER >= 11
   test_move_ctor_exceptions();
 #endif
+  return 0;
 }
diff --git a/libcxx/test/std/containers/sequences/vector/vector.capacity/resize_size_value_exceptions.pass.cpp b/libcxx/test/std/containers/sequences/vector/vector.capacity/resize_size_value_exceptions.pass.cpp
index 7217b47d69e4d..4180155ee9de5 100644
--- a/libcxx/test/std/containers/sequences/vector/vector.capacity/resize_size_value_exceptions.pass.cpp
+++ b/libcxx/test/std/containers/sequences/vector/vector.capacity/resize_size_value_exceptions.pass.cpp
@@ -70,6 +70,40 @@ void test_copy_ctor_exception_for_strong_guarantee(std::vector<throwing_data<T>,
   }
 }
 
+// Test the no-reallocation path of resize(n, x): reserve enough capacity
+// ahead of time so resize fits within it, and trigger the throw partway
+// through copying the new element.
+template <typename T, typename Alloc>
+void test_in_place_copy_ctor_exception_for_strong_guarantee(
+    std::vector<throwing_data<T>, Alloc>& v, const std::vector<T>& values) {
+  assert(v.empty() && !values.empty());
+  std::size_t new_size = 2 * values.size();
+  // Set the existing-elements counter high enough that fill never throws
+  // and existing elements never trigger the throw on later operations.
+  int throw_after_fill = values.size() + 1000;
+  v.reserve(new_size);
+  for (std::size_t i = 0; i < values.size(); ++i)
+    v.emplace_back(values[i], throw_after_fill);
+
+  throwing_data<T>* old_data = v.data();
+  std::size_t old_size       = v.size();
+  std::size_t old_cap        = v.capacity();
+
+  try {
+    // t carries its own counter so the throw fires partway through the new
+    // copies (independent of the existing-elements counter).
+    int n = static_cast<int>(values.size() / 2 + 1);
+    throwing_data<T> t(T(), n);
+    v.resize(new_size, t);
+  } catch (...) {
+    assert(v.data() == old_data);
+    assert(v.size() == old_size);
+    assert(v.capacity() == old_cap);
+    for (std::size_t i = 0; i < v.size(); ++i)
+      assert(v[i].data_ == values[i]);
+  }
+}
+
 // Check the strong exception guarantee during reallocation failures
 void test_allocation_exceptions() {
   //
@@ -223,9 +257,60 @@ void test_copy_ctor_exceptions() {
     test_copy_ctor_exception_for_strong_guarantee(v, in);
   }
   check_new_delete_called();
+
+  // Same per-allocator coverage but for the no-reallocation path of
+  // resize(n, x). See https://llvm.org/PR59293.
+  {
+    {
+      int a[] = {1, 2, 3, 4, 5};
+      std::vector<int> in(a, a + sizeof(a) / sizeof(a[0]));
+      std::vector<throwing_data<int> > v;
+      test_in_place_copy_ctor_exception_for_strong_guarantee(v, in);
+    }
+    check_new_delete_called();
+
+    {
+      int a[] = {1, 2, 3, 4, 5};
+      std::vector<int> in(a, a + sizeof(a) / sizeof(a[0]));
+      std::vector<throwing_data<int>, min_allocator<throwing_data<int> > > v;
+      test_in_place_copy_ctor_exception_for_strong_guarantee(v, in);
+    }
+    check_new_delete_called();
+
+    {
+      std::vector<int> in(10, 42);
+      std::vector<throwing_data<int>, safe_allocator<throwing_data<int> > > v;
+      test_in_place_copy_ctor_exception_for_strong_guarantee(v, in);
+    }
+    check_new_delete_called();
+
+    {
+      std::vector<int> in(10, 42);
+      std::vector<throwing_data<int>, test_allocator<throwing_data<int> > > v;
+      test_in_place_copy_ctor_exception_for_strong_guarantee(v, in);
+    }
+    check_new_delete_called();
+
+    {
+      std::vector<int> in(10, 42);
+      std::vector<throwing_data<int>, limited_allocator<throwing_data<int>, 100> > v;
+      test_in_place_copy_ctor_exception_for_strong_guarantee(v, in);
+    }
+    check_new_delete_called();
+
+#if TEST_STD_VER >= 23
+    {
+      std::vector<int> in{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+      std::vector<throwing_data<int>, increasing_allocator<throwing_data<int>>> v;
+      test_in_place_copy_ctor_exception_for_strong_guarantee(v, in);
+    }
+    check_new_delete_called();
+#endif
+  }
 }
 
 int main(int, char**) {
   test_allocation_exceptions();
   test_copy_ctor_exceptions();
+  return 0;
 }
diff --git a/libcxx/test/std/containers/sequences/vector/vector.cons/assign_size_value.pass.cpp b/libcxx/test/std/containers/sequences/vector/vector.cons/assign_size_value.pass.cpp
index 8526cf587ed3f..239a0bd8ee37b 100644
--- a/libcxx/test/std/containers/sequences/vector/vector.cons/assign_size_value.pass.cpp
+++ b/libcxx/test/std/containers/sequences/vector/vector.cons/assign_size_value.pass.cpp
@@ -17,6 +17,8 @@
 #include "test_macros.h"
 #include "min_allocator.h"
 #include "asan_testing.h"
+#include "../common.h"
+#include "count_new.h"
 
 TEST_CONSTEXPR bool is6(int x) { return x == 6; }
 
@@ -62,5 +64,37 @@ int main(int, char**) {
 #if TEST_STD_VER > 17
   static_assert(tests());
 #endif
+
+  // Verify that assign(n, x) destroys partially-constructed elements when a
+  // later element's copy constructor throws.
+#if !defined(TEST_HAS_NO_EXCEPTIONS)
+  {
+    // Without reallocation (note the reserve call)
+    int throw_after = 4; // x consumes 1, then 3 successful copies, 4th throws
+    std::vector<throwing_t> v;
+    v.reserve(10);
+    try {
+      throwing_t x(throw_after);
+      v.assign(5, x);
+      assert(false);
+    } catch (int) {
+      LIBCPP_ASSERT(v.size() == 0);
+    }
+  }
+  check_new_delete_called();
+  {
+    // With reallocation
+    int throw_after = 4; // x consumes 1, then 3 successful copies, 4th throws
+    std::vector<throwing_t> v;
+    try {
+      throwing_t x(throw_after);
+      v.assign(5, x);
+      assert(false);
+    } catch (int) {
+      LIBCPP_ASSERT(v.size() == 0);
+    }
+  }
+  check_new_delete_called();
+#endif
   return 0;
 }
diff --git a/libcxx/test/std/containers/sequences/vector/vector.modifiers/insert_iter_size_value.pass.cpp b/libcxx/test/std/containers/sequences/vector/vector.modifiers/insert_iter_size_value.pass.cpp
index 8173285d4996b..8da4aef1c8d4f 100644
--- a/libcxx/test/std/containers/sequences/vector/vector.modifiers/insert_iter_size_value.pass.cpp
+++ b/libcxx/test/std/containers/sequences/vector/vector.modifiers/insert_iter_size_value.pass.cpp
@@ -18,6 +18,8 @@
 #include "test_allocator.h"
 #include "min_allocator.h"
 #include "asan_testing.h"
+#include "../common.h"
+#include "count_new.h"
 
 TEST_CONSTEXPR_CXX20 bool tests() {
   {
@@ -120,5 +122,37 @@ int main(int, char**) {
 #if TEST_STD_VER > 17
   static_assert(tests());
 #endif
+
+  // Verify that insert(pos, n, x) destroys partially-constructed elements
+  // when a later element's copy constructor throws.
+#if !defined(TEST_HAS_NO_EXCEPTIONS)
+  {
+    // Without reallocation (note the reserve call)
+    int throw_after = 4; // x consumes 1, then 3 successful copies, 4th throws
+    std::vector<throwing_t> v;
+    v.reserve(10);
+    try {
+      throwing_t x(throw_after);
+      v.insert(v.begin(), 5, x);
+      assert(false);
+    } catch (int) {
+      LIBCPP_ASSERT(v.size() == 0);
+    }
+  }
+  check_new_delete_called();
+  {
+    // With reallocation
+    int throw_after = 4; // x consumes 1, then 3 successful copies, 4th throws
+    std::vector<throwing_t> v;
+    try {
+      throwing_t x(throw_after);
+      v.insert(v.begin(), 5, x);
+      assert(false);
+    } catch (int) {
+      LIBCPP_ASSERT(v.size() == 0);
+    }
+  }
+  check_new_delete_called();
+#endif
   return 0;
 }

>From e96195ea0545ad03caf218e6d4afb0ca8ed7c60e Mon Sep 17 00:00:00 2001
From: Louis Dionne <ldionne.2 at gmail.com>
Date: Mon, 22 Jun 2026 23:05:41 -0400
Subject: [PATCH 3/3] Fix CI failures in tests

---
 libcxx/test/std/containers/sequences/vector/common.h            | 2 ++
 .../vector/vector.capacity/resize_size_exceptions.pass.cpp      | 2 ++
 .../vector.capacity/resize_size_value_exceptions.pass.cpp       | 2 ++
 .../sequences/vector/vector.cons/assign_size_value.pass.cpp     | 2 ++
 .../vector/vector.modifiers/insert_iter_size_value.pass.cpp     | 2 ++
 5 files changed, 10 insertions(+)

diff --git a/libcxx/test/std/containers/sequences/vector/common.h b/libcxx/test/std/containers/sequences/vector/common.h
index 768d7f03fd703..71dff000c93a2 100644
--- a/libcxx/test/std/containers/sequences/vector/common.h
+++ b/libcxx/test/std/containers/sequences/vector/common.h
@@ -22,6 +22,7 @@
 #include "count_new.h"
 #include "test_macros.h"
 
+#if !defined(TEST_HAS_NO_EXCEPTIONS)
 struct throwing_t {
   int* throw_after_n_ = nullptr;
   throwing_t() { throw 0; }
@@ -53,6 +54,7 @@ struct throwing_t {
     return lhs.throw_after_n_ != rhs.throw_after_n_;
   }
 };
+#endif // !defined(TEST_HAS_NO_EXCEPTIONS)
 
 #if TEST_STD_VER >= 11
 
diff --git a/libcxx/test/std/containers/sequences/vector/vector.capacity/resize_size_exceptions.pass.cpp b/libcxx/test/std/containers/sequences/vector/vector.capacity/resize_size_exceptions.pass.cpp
index 33b8ace5ec5cc..1ef8507f2ef45 100644
--- a/libcxx/test/std/containers/sequences/vector/vector.capacity/resize_size_exceptions.pass.cpp
+++ b/libcxx/test/std/containers/sequences/vector/vector.capacity/resize_size_exceptions.pass.cpp
@@ -8,6 +8,8 @@
 
 // UNSUPPORTED: no-exceptions
 
+// XFAIL: FROZEN-CXX03-HEADERS-FIXME
+
 // This test file validates that std::vector<T>::resize(size_type) provides the strong exception guarantee
 // if no exception is thrown by the move constructor of T during the resize call. It also checks that if
 // T's move constructor is not noexcept, resize provides only the basic exception guarantee.
diff --git a/libcxx/test/std/containers/sequences/vector/vector.capacity/resize_size_value_exceptions.pass.cpp b/libcxx/test/std/containers/sequences/vector/vector.capacity/resize_size_value_exceptions.pass.cpp
index 4180155ee9de5..f948ab3b5635e 100644
--- a/libcxx/test/std/containers/sequences/vector/vector.capacity/resize_size_value_exceptions.pass.cpp
+++ b/libcxx/test/std/containers/sequences/vector/vector.capacity/resize_size_value_exceptions.pass.cpp
@@ -8,6 +8,8 @@
 
 // UNSUPPORTED: no-exceptions
 
+// XFAIL: FROZEN-CXX03-HEADERS-FIXME
+
 // Check that std::vector<T>::resize(size_type sz, const value_type& x) provides the strong exception guarantee
 // if T is Cpp17CopyInsertable.
 
diff --git a/libcxx/test/std/containers/sequences/vector/vector.cons/assign_size_value.pass.cpp b/libcxx/test/std/containers/sequences/vector/vector.cons/assign_size_value.pass.cpp
index 239a0bd8ee37b..b335d01db9c74 100644
--- a/libcxx/test/std/containers/sequences/vector/vector.cons/assign_size_value.pass.cpp
+++ b/libcxx/test/std/containers/sequences/vector/vector.cons/assign_size_value.pass.cpp
@@ -10,6 +10,8 @@
 
 // void assign(size_type n, const_reference v);
 
+// XFAIL: FROZEN-CXX03-HEADERS-FIXME
+
 #include <vector>
 #include <algorithm>
 #include <cassert>
diff --git a/libcxx/test/std/containers/sequences/vector/vector.modifiers/insert_iter_size_value.pass.cpp b/libcxx/test/std/containers/sequences/vector/vector.modifiers/insert_iter_size_value.pass.cpp
index 8da4aef1c8d4f..da8d880a0ee35 100644
--- a/libcxx/test/std/containers/sequences/vector/vector.modifiers/insert_iter_size_value.pass.cpp
+++ b/libcxx/test/std/containers/sequences/vector/vector.modifiers/insert_iter_size_value.pass.cpp
@@ -10,6 +10,8 @@
 
 // iterator insert(const_iterator position, size_type n, const value_type& x);
 
+// XFAIL: FROZEN-CXX03-HEADERS-FIXME
+
 #include <vector>
 #include <cassert>
 #include <cstddef>



More information about the libcxx-commits mailing list