[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
Fri Jun 19 12:23:21 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/2] [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/2] [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;
}
More information about the libcxx-commits
mailing list