[libcxx-commits] [libcxx] d0b5165 - [libc++] Fix an exception safety issue in `forward_list` and add tests.

via libcxx-commits libcxx-commits at lists.llvm.org
Wed Jul 12 10:11:41 PDT 2023


Author: varconst
Date: 2023-07-12T10:11:26-07:00
New Revision: d0b51657c259365750b3aada3bae23f19c96fdbc

URL: https://github.com/llvm/llvm-project/commit/d0b51657c259365750b3aada3bae23f19c96fdbc
DIFF: https://github.com/llvm/llvm-project/commit/d0b51657c259365750b3aada3bae23f19c96fdbc.diff

LOG: [libc++] Fix an exception safety issue in `forward_list` and add tests.

When inserting nodes into a forward list, each new node is allocated but
not constructed. The constructor was being called explicitly on the node
`value_` but the `next_` pointer remained uninitialized rather than
being set to null. This bug is only triggered in the cleanup code if an
exception is thrown -- upon successful creation of new nodes, the last
incorrect "next" value is overwritten to a correct pointer.

This issue was found due to new tests added in
https://reviews.llvm.org/D149830.

Differential Revision: https://reviews.llvm.org/D152327

Added: 
    libcxx/test/libcxx/memory/allocation_guard.pass.cpp
    libcxx/test/std/containers/exception_safety_helpers.h
    libcxx/test/std/containers/sequences/forwardlist/exception_safety.pass.cpp

Modified: 
    libcxx/include/__memory/allocation_guard.h
    libcxx/include/forward_list
    libcxx/test/std/containers/container.adaptors/from_range_container_adaptors.h
    libcxx/test/std/containers/container.adaptors/push_range_container_adaptors.h
    libcxx/test/std/containers/from_range_helpers.h
    libcxx/test/std/containers/sequences/from_range_sequence_containers.h
    libcxx/test/std/containers/sequences/insert_range_sequence_containers.h

Removed: 
    


################################################################################
diff  --git a/libcxx/include/__memory/allocation_guard.h b/libcxx/include/__memory/allocation_guard.h
index 1481f57717ef4b..f63b17430ed1b1 100644
--- a/libcxx/include/__memory/allocation_guard.h
+++ b/libcxx/include/__memory/allocation_guard.h
@@ -11,6 +11,7 @@
 #define _LIBCPP___MEMORY_ALLOCATION_GUARD_H
 
 #include <__config>
+#include <__memory/addressof.h>
 #include <__memory/allocator_traits.h>
 #include <__utility/move.h>
 #include <cstddef>
@@ -58,9 +59,29 @@ struct __allocation_guard {
 
     _LIBCPP_HIDE_FROM_ABI
     ~__allocation_guard() _NOEXCEPT {
-        if (__ptr_ != nullptr) {
-            allocator_traits<_Alloc>::deallocate(__alloc_, __ptr_, __n_);
+        __destroy();
+    }
+
+    _LIBCPP_HIDE_FROM_ABI __allocation_guard(const __allocation_guard&) = delete;
+    _LIBCPP_HIDE_FROM_ABI __allocation_guard(__allocation_guard&& __other) _NOEXCEPT
+        : __alloc_(std::move(__other.__alloc_))
+        , __n_(__other.__n_)
+        , __ptr_(__other.__ptr_) {
+      __other.__ptr_ = nullptr;
+    }
+
+    _LIBCPP_HIDE_FROM_ABI __allocation_guard& operator=(const __allocation_guard& __other) = delete;
+    _LIBCPP_HIDE_FROM_ABI __allocation_guard& operator=(__allocation_guard&& __other) _NOEXCEPT {
+        if (std::addressof(__other) != this) {
+            __destroy();
+
+            __alloc_ = std::move(__other.__alloc_);
+            __n_ = __other.__n_;
+            __ptr_ = __other.__ptr_;
+            __other.__ptr_ = nullptr;
         }
+
+        return *this;
     }
 
     _LIBCPP_HIDE_FROM_ABI
@@ -76,6 +97,13 @@ struct __allocation_guard {
     }
 
 private:
+    _LIBCPP_HIDE_FROM_ABI
+    void __destroy() _NOEXCEPT {
+        if (__ptr_ != nullptr) {
+            allocator_traits<_Alloc>::deallocate(__alloc_, __ptr_, __n_);
+        }
+    }
+
     _Alloc __alloc_;
     _Size __n_;
     _Pointer __ptr_;

diff  --git a/libcxx/include/forward_list b/libcxx/include/forward_list
index 41829636a2a2bd..aac9d548ac935e 100644
--- a/libcxx/include/forward_list
+++ b/libcxx/include/forward_list
@@ -195,6 +195,7 @@ template <class T, class Allocator, class Predicate>
 #include <__iterator/move_iterator.h>
 #include <__iterator/next.h>
 #include <__memory/addressof.h>
+#include <__memory/allocation_guard.h>
 #include <__memory/allocator.h>
 #include <__memory/allocator_destructor.h>
 #include <__memory/allocator_traits.h>
@@ -292,6 +293,7 @@ struct __forward_begin_node
     pointer __next_;
 
     _LIBCPP_INLINE_VISIBILITY __forward_begin_node() : __next_(nullptr) {}
+    _LIBCPP_INLINE_VISIBILITY explicit __forward_begin_node(pointer __n) : __next_(__n) {}
 
     _LIBCPP_INLINE_VISIBILITY
     __begin_node_pointer __next_as_begin() const {
@@ -307,8 +309,13 @@ struct _LIBCPP_STANDALONE_DEBUG __forward_list_node
     : public __begin_node_of<_Tp, _VoidPtr>
 {
     typedef _Tp value_type;
+    typedef __begin_node_of<_Tp, _VoidPtr> _Base;
+    typedef typename _Base::pointer _NodePtr;
 
     value_type __value_;
+
+    _LIBCPP_HIDE_FROM_ABI __forward_list_node() = default;
+    _LIBCPP_HIDE_FROM_ABI __forward_list_node(const value_type& __v, _NodePtr __next) : _Base(__next), __value_(__v) {}
 };
 
 
@@ -1247,14 +1254,20 @@ typename forward_list<_Tp, _Alloc>::iterator
 forward_list<_Tp, _Alloc>::insert_after(const_iterator __p, size_type __n,
                                         const value_type& __v)
 {
+    using _Guard = __allocation_guard<__node_allocator>;
+
     __begin_node_pointer __r = __p.__get_begin();
     if (__n > 0)
     {
         __node_allocator& __a = base::__alloc();
-        typedef __allocator_destructor<__node_allocator> _Dp;
-        unique_ptr<__node, _Dp> __h(__node_traits::allocate(__a, 1), _Dp(__a, 1));
-        __node_traits::construct(__a, _VSTD::addressof(__h->__value_), __v);
-        __node_pointer __first = __h.release();
+
+        __node_pointer __first = nullptr;
+        {
+          _Guard __h(__a, 1);
+          __node_traits::construct(__a, std::addressof(__h.__get()->__value_), __v);
+          __h.__get()->__next_ = nullptr;
+          __first = __h.__release_ptr();
+        }
         __node_pointer __last = __first;
 #ifndef _LIBCPP_HAS_NO_EXCEPTIONS
         try
@@ -1262,9 +1275,10 @@ forward_list<_Tp, _Alloc>::insert_after(const_iterator __p, size_type __n,
 #endif // _LIBCPP_HAS_NO_EXCEPTIONS
             for (--__n; __n != 0; --__n, __last = __last->__next_)
             {
-                __h.reset(__node_traits::allocate(__a, 1));
-                __node_traits::construct(__a, _VSTD::addressof(__h->__value_), __v);
-                __last->__next_ = __h.release();
+                _Guard __h(__a, 1);
+                __node_traits::construct(__a, std::addressof(__h.__get()->__value_), __v);
+                __h.__get()->__next_ = nullptr;
+                __last->__next_ = __h.__release_ptr();
             }
 #ifndef _LIBCPP_HAS_NO_EXCEPTIONS
         }
@@ -1293,14 +1307,19 @@ __enable_if_t<__has_input_iterator_category<_InputIterator>::value, typename for
 forward_list<_Tp, _Alloc>::insert_after(const_iterator __p,
                                         _InputIterator __f, _InputIterator __l)
 {
+    using _Guard = __allocation_guard<__node_allocator>;
+
     __begin_node_pointer __r = __p.__get_begin();
     if (__f != __l)
     {
         __node_allocator& __a = base::__alloc();
-        typedef __allocator_destructor<__node_allocator> _Dp;
-        unique_ptr<__node, _Dp> __h(__node_traits::allocate(__a, 1), _Dp(__a, 1));
-        __node_traits::construct(__a, _VSTD::addressof(__h->__value_), *__f);
-        __node_pointer __first = __h.release();
+        __node_pointer __first = nullptr;
+        {
+          _Guard __h(__a, 1);
+          __node_traits::construct(__a, std::addressof(__h.__get()->__value_), *__f);
+          __h.__get()->__next_ = nullptr;
+          __first = __h.__release_ptr();
+        }
         __node_pointer __last = __first;
 #ifndef _LIBCPP_HAS_NO_EXCEPTIONS
         try
@@ -1308,9 +1327,10 @@ forward_list<_Tp, _Alloc>::insert_after(const_iterator __p,
 #endif // _LIBCPP_HAS_NO_EXCEPTIONS
             for (++__f; __f != __l; ++__f, ((void)(__last = __last->__next_)))
             {
-                __h.reset(__node_traits::allocate(__a, 1));
-                __node_traits::construct(__a, _VSTD::addressof(__h->__value_), *__f);
-                __last->__next_ = __h.release();
+                _Guard __h(__a, 1);
+                __node_traits::construct(__a, std::addressof(__h.__get()->__value_), *__f);
+                __h.__get()->__next_ = nullptr;
+                __last->__next_ = __h.__release_ptr();
             }
 #ifndef _LIBCPP_HAS_NO_EXCEPTIONS
         }

diff  --git a/libcxx/test/libcxx/memory/allocation_guard.pass.cpp b/libcxx/test/libcxx/memory/allocation_guard.pass.cpp
new file mode 100644
index 00000000000000..4b2f7abe9159b7
--- /dev/null
+++ b/libcxx/test/libcxx/memory/allocation_guard.pass.cpp
@@ -0,0 +1,192 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+
+// <memory>
+
+// To allow checking that self-move works correctly.
+// ADDITIONAL_COMPILE_FLAGS: -Wno-self-move
+
+// template<class _Alloc>
+// struct __allocation_guard;
+
+#include <cassert>
+#include <memory>
+#include <type_traits>
+#include <utility>
+
+#include "test_allocator.h"
+
+using A = test_allocator<int>;
+
+// A trimmed-down version of `test_allocator` that is copy-assignable (in general allocators don't have to support copy
+// assignment).
+template <class T>
+struct AssignableAllocator {
+  using size_type = unsigned;
+  using 
diff erence_type = int;
+  using value_type = T;
+  using pointer = value_type*;
+  using const_pointer = const value_type*;
+  using reference = typename std::add_lvalue_reference<value_type>::type;
+  using const_reference = typename std::add_lvalue_reference<const value_type>::type;
+
+  template <class U>
+  struct rebind {
+    using other = test_allocator<U>;
+  };
+
+  test_allocator_statistics* stats_ = nullptr;
+
+  explicit AssignableAllocator(test_allocator_statistics& stats) : stats_(&stats) {
+    ++stats_->count;
+  }
+
+  TEST_CONSTEXPR_CXX14 AssignableAllocator(const AssignableAllocator& rhs) TEST_NOEXCEPT
+      : stats_(rhs.stats_) {
+    if (stats_ != nullptr) {
+      ++stats_->count;
+      ++stats_->copied;
+    }
+  }
+
+  TEST_CONSTEXPR_CXX14 AssignableAllocator& operator=(const AssignableAllocator& rhs) TEST_NOEXCEPT {
+    stats_ = rhs.stats_;
+    if (stats_ != nullptr) {
+      ++stats_->count;
+      ++stats_->copied;
+    }
+
+    return *this;
+  }
+
+  TEST_CONSTEXPR_CXX14 pointer allocate(size_type n, const void* = nullptr) {
+    if (stats_ != nullptr) {
+      ++stats_->alloc_count;
+    }
+    return std::allocator<value_type>().allocate(n);
+  }
+
+  TEST_CONSTEXPR_CXX14 void deallocate(pointer p, size_type s) {
+    if (stats_ != nullptr) {
+      --stats_->alloc_count;
+    }
+    std::allocator<value_type>().deallocate(p, s);
+  }
+
+  TEST_CONSTEXPR size_type max_size() const TEST_NOEXCEPT { return UINT_MAX / sizeof(T); }
+
+  template <class U>
+  TEST_CONSTEXPR_CXX20 void construct(pointer p, U&& val) {
+    if (stats_ != nullptr)
+      ++stats_->construct_count;
+#if TEST_STD_VER > 17
+    std::construct_at(std::to_address(p), std::forward<U>(val));
+#else
+    ::new (static_cast<void*>(p)) T(std::forward<U>(val));
+#endif
+  }
+
+  TEST_CONSTEXPR_CXX14 void destroy(pointer p) {
+    if (stats_ != nullptr) {
+      ++stats_->destroy_count;
+    }
+    p->~T();
+  }
+};
+
+// Move-only.
+static_assert(!std::is_copy_constructible<std::__allocation_guard<A> >::value, "");
+static_assert(std::is_move_constructible<std::__allocation_guard<A> >::value, "");
+static_assert(!std::is_copy_assignable<std::__allocation_guard<A> >::value, "");
+static_assert(std::is_move_assignable<std::__allocation_guard<A> >::value, "");
+
+int main(int, char**) {
+  const int size = 42;
+
+  { // The constructor allocates using the given allocator.
+    test_allocator_statistics stats;
+    std::__allocation_guard<A> guard(A(&stats), size);
+    assert(stats.alloc_count == 1);
+    assert(guard.__get() != nullptr);
+  }
+
+  { // The destructor deallocates using the given allocator.
+    test_allocator_statistics stats;
+    {
+      std::__allocation_guard<A> guard(A(&stats), size);
+      assert(stats.alloc_count == 1);
+    }
+    assert(stats.alloc_count == 0);
+  }
+
+  { // `__release_ptr` prevents deallocation.
+    test_allocator_statistics stats;
+    A alloc(&stats);
+    int* ptr = nullptr;
+    {
+      std::__allocation_guard<A> guard(alloc, size);
+      assert(stats.alloc_count == 1);
+      ptr = guard.__release_ptr();
+    }
+    assert(stats.alloc_count == 1);
+    alloc.deallocate(ptr, size);
+  }
+
+  { // Using the move constructor doesn't lead to double deletion.
+    test_allocator_statistics stats;
+    {
+      std::__allocation_guard<A> guard1(A(&stats), size);
+      assert(stats.alloc_count == 1);
+      auto* ptr1 = guard1.__get();
+
+      std::__allocation_guard<A> guard2 = std::move(guard1);
+      assert(stats.alloc_count == 1);
+      assert(guard1.__get() == nullptr);
+      assert(guard2.__get() == ptr1);
+    }
+    assert(stats.alloc_count == 0);
+  }
+
+  { // Using the move assignment operator doesn't lead to double deletion.
+    using A2 = AssignableAllocator<int>;
+
+    test_allocator_statistics stats;
+    {
+      std::__allocation_guard<A2> guard1(A2(stats), size);
+      assert(stats.alloc_count == 1);
+      std::__allocation_guard<A2> guard2(A2(stats), size);
+      assert(stats.alloc_count == 2);
+      auto* ptr1 = guard1.__get();
+
+      guard2 = std::move(guard1);
+      assert(stats.alloc_count == 1);
+      assert(guard1.__get() == nullptr);
+      assert(guard2.__get() == ptr1);
+    }
+    assert(stats.alloc_count == 0);
+  }
+
+  { // Self-assignment is a no-op.
+    using A2 = AssignableAllocator<int>;
+
+    test_allocator_statistics stats;
+    {
+      std::__allocation_guard<A2> guard(A2(stats), size);
+      assert(stats.alloc_count == 1);
+      auto* ptr = guard.__get();
+
+      guard = std::move(guard);
+      assert(stats.alloc_count == 1);
+      assert(guard.__get() == ptr);
+    }
+    assert(stats.alloc_count == 0);
+  }
+
+  return 0;
+}

diff  --git a/libcxx/test/std/containers/container.adaptors/from_range_container_adaptors.h b/libcxx/test/std/containers/container.adaptors/from_range_container_adaptors.h
index 0ad0b18f575c0d..8bd3ac632cfc95 100644
--- a/libcxx/test/std/containers/container.adaptors/from_range_container_adaptors.h
+++ b/libcxx/test/std/containers/container.adaptors/from_range_container_adaptors.h
@@ -17,6 +17,7 @@
 #include <utility>
 #include <vector>
 
+#include "../exception_safety_helpers.h"
 #include "../from_range_helpers.h"
 #include "MoveOnly.h"
 #include "almost_satisfies_types.h"
@@ -192,18 +193,11 @@ constexpr void test_container_adaptor_move_only() {
 template <template <class ...> class Adaptor>
 void test_exception_safety_throwing_copy() {
 #if !defined(TEST_HAS_NO_EXCEPTIONS)
-  using T = ThrowingCopy<3>;
-  T::reset();
-  T in[5];
-
-  try {
-    Adaptor<T, std::vector<T>> c(std::from_range, in);
-    assert(false); // The constructor call above should throw.
-
-  } catch (int) {
-    assert(T::created_by_copying == 3);
-    assert(T::destroyed == 2); // No destructor call for the partially-constructed element.
-  }
+  constexpr int ThrowOn = 3;
+  using T = ThrowingCopy<ThrowOn>;
+  test_exception_safety_throwing_copy<ThrowOn, /*Size=*/5>([](T* from, T* to) {
+    [[maybe_unused]] Adaptor<T, std::vector<T>> c(std::from_range, std::ranges::subrange(from, to));
+  });
 #endif
 }
 

diff  --git a/libcxx/test/std/containers/container.adaptors/push_range_container_adaptors.h b/libcxx/test/std/containers/container.adaptors/push_range_container_adaptors.h
index 37b166b7df2899..19e63d23884b27 100644
--- a/libcxx/test/std/containers/container.adaptors/push_range_container_adaptors.h
+++ b/libcxx/test/std/containers/container.adaptors/push_range_container_adaptors.h
@@ -18,6 +18,7 @@
 #include <type_traits>
 #include <vector>
 
+#include "../exception_safety_helpers.h"
 #include "../from_range_helpers.h"
 #include "../insert_range_helpers.h"
 #include "MoveOnly.h"
@@ -263,19 +264,12 @@ void test_push_range_inserter_choice(bool is_result_heapified = false) {
 template <template <class ...> class Container>
 void test_push_range_exception_safety_throwing_copy() {
 #if !defined(TEST_HAS_NO_EXCEPTIONS)
-  using T = ThrowingCopy<3>;
-  T::reset();
-  T in[5];
-
-  try {
+  constexpr int ThrowOn = 3;
+  using T = ThrowingCopy<ThrowOn>;
+  test_exception_safety_throwing_copy<ThrowOn, /*Size=*/5>([](auto* from, auto* to) {
     Container<T> c;
-    c.push_range(in);
-    assert(false); // The function call above should throw.
-
-  } catch (int) {
-    assert(T::created_by_copying == 3);
-    assert(T::destroyed == 2); // No destructor call for the partially-constructed element.
-  }
+    c.push_range(std::ranges::subrange(from, to));
+  });
 #endif
 }
 

diff  --git a/libcxx/test/std/containers/exception_safety_helpers.h b/libcxx/test/std/containers/exception_safety_helpers.h
new file mode 100644
index 00000000000000..08884160d19c9c
--- /dev/null
+++ b/libcxx/test/std/containers/exception_safety_helpers.h
@@ -0,0 +1,106 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef SUPPORT_EXCEPTION_SAFETY_HELPERS_H
+#define SUPPORT_EXCEPTION_SAFETY_HELPERS_H
+
+#include <cassert>
+#include <cstddef>
+#include <functional>
+#include <utility>
+#include "test_macros.h"
+
+#if !defined(TEST_HAS_NO_EXCEPTIONS)
+template <int N>
+struct ThrowingCopy {
+  static bool throwing_enabled;
+  static int created_by_copying;
+  static int destroyed;
+  int x = 0; // Allows distinguishing between 
diff erent instances.
+
+  ThrowingCopy() = default;
+  ThrowingCopy(int value) : x(value) {}
+  ~ThrowingCopy() {
+    ++destroyed;
+  }
+
+  ThrowingCopy(const ThrowingCopy& other) : x(other.x) {
+    ++created_by_copying;
+    if (throwing_enabled && created_by_copying == N) {
+      throw -1;
+    }
+  }
+
+  // Defined to silence GCC warnings. For test purposes, only copy construction is considered `created_by_copying`.
+  ThrowingCopy& operator=(const ThrowingCopy& other) {
+    x = other.x;
+    return *this;
+  }
+
+  friend bool operator==(const ThrowingCopy& lhs, const ThrowingCopy& rhs) { return lhs.x == rhs.x; }
+  friend bool operator<(const ThrowingCopy& lhs, const ThrowingCopy& rhs) { return lhs.x < rhs.x; }
+
+  static void reset() {
+    created_by_copying = destroyed = 0;
+  }
+};
+
+template <int N>
+bool ThrowingCopy<N>::throwing_enabled = true;
+template <int N>
+int ThrowingCopy<N>::created_by_copying = 0;
+template <int N>
+int ThrowingCopy<N>::destroyed = 0;
+
+template <int N>
+struct std::hash<ThrowingCopy<N>> {
+  std::size_t operator()(const ThrowingCopy<N>& value) const {
+    return value.x;
+  }
+};
+
+template <int ThrowOn, int Size, class Func>
+void test_exception_safety_throwing_copy(Func&& func) {
+  using T = ThrowingCopy<ThrowOn>;
+  T::reset();
+  T in[Size];
+
+  try {
+    func(in, in + Size);
+    assert(false); // The function call above should throw.
+
+  } catch (int) {
+    assert(T::created_by_copying == ThrowOn);
+    assert(T::destroyed == ThrowOn - 1); // No destructor call for the partially-constructed element.
+  }
+}
+
+// Destroys the container outside the user callback to avoid destroying extra elements upon throwing (which would
+// complicate asserting that the expected number of elements was destroyed).
+template <class Container, int ThrowOn, int Size, class Func>
+void test_exception_safety_throwing_copy_container(Func&& func) {
+  using T = ThrowingCopy<ThrowOn>;
+  T::throwing_enabled = false;
+  T in[Size];
+  Container c(in, in + Size);
+  T::throwing_enabled = true;
+  T::reset();
+
+  try {
+    func(std::move(c));
+    assert(false); // The function call above should throw.
+
+  } catch (int) {
+    assert(T::created_by_copying == ThrowOn);
+    assert(T::destroyed == ThrowOn - 1); // No destructor call for the partially-constructed element.
+  }
+}
+
+#endif // !defined(TEST_HAS_NO_EXCEPTIONS)
+
+#endif // SUPPORT_EXCEPTION_SAFETY_HELPERS_H

diff  --git a/libcxx/test/std/containers/from_range_helpers.h b/libcxx/test/std/containers/from_range_helpers.h
index 9bd72acfad09b3..7fff99da1e15e3 100644
--- a/libcxx/test/std/containers/from_range_helpers.h
+++ b/libcxx/test/std/containers/from_range_helpers.h
@@ -57,52 +57,6 @@ struct std::hash<KeyValue> {
 };
 
 #if !defined(TEST_HAS_NO_EXCEPTIONS)
-template <int N>
-struct ThrowingCopy {
-  static bool throwing_enabled;
-  static int created_by_copying;
-  static int destroyed;
-  int x = 0; // Allows distinguishing between 
diff erent instances.
-
-  ThrowingCopy() = default;
-  ThrowingCopy(int value) : x(value) {}
-  ~ThrowingCopy() {
-    ++destroyed;
-  }
-
-  ThrowingCopy(const ThrowingCopy& other) : x(other.x) {
-    ++created_by_copying;
-    if (throwing_enabled && created_by_copying == N) {
-      throw -1;
-    }
-  }
-
-  // Defined to silence GCC warnings. For test purposes, only copy construction is considered `created_by_copying`.
-  ThrowingCopy& operator=(const ThrowingCopy& other) {
-    x = other.x;
-    return *this;
-  }
-
-  friend auto operator<=>(const ThrowingCopy&, const ThrowingCopy&) = default;
-
-  static void reset() {
-    created_by_copying = destroyed = 0;
-  }
-};
-
-template <int N>
-struct std::hash<ThrowingCopy<N>> {
-  std::size_t operator()(const ThrowingCopy<N>& value) const {
-    return value.x;
-  }
-};
-
-template <int N>
-bool ThrowingCopy<N>::throwing_enabled = true;
-template <int N>
-int ThrowingCopy<N>::created_by_copying = 0;
-template <int N>
-int ThrowingCopy<N>::destroyed = 0;
 
 template <class T>
 struct ThrowingAllocator {

diff  --git a/libcxx/test/std/containers/sequences/forwardlist/exception_safety.pass.cpp b/libcxx/test/std/containers/sequences/forwardlist/exception_safety.pass.cpp
new file mode 100644
index 00000000000000..2fe35df2cd412a
--- /dev/null
+++ b/libcxx/test/std/containers/sequences/forwardlist/exception_safety.pass.cpp
@@ -0,0 +1,153 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// <forward_list>
+
+// UNSUPPORTED: c++03, no-exceptions
+
+// TODO:
+// - throwing upon moving;
+// - initializer lists;
+// - throwing when constructing the element in place.
+
+// forward_list(size_type n, const value_type& v);
+// forward_list(size_type n, const value_type& v, const allocator_type& a);
+// template <class InputIterator>
+//     forward_list(InputIterator first, InputIterator last);
+// template <class InputIterator>
+//     forward_list(InputIterator first, InputIterator last, const allocator_type& a);
+// forward_list(const forward_list& x);
+// forward_list(const forward_list& x, const allocator_type& a);
+//
+// forward_list& operator=(const forward_list& x);
+//
+// template <class InputIterator>
+//     void assign(InputIterator first, InputIterator last);
+// void assign(size_type n, const value_type& v);
+//
+// void push_front(const value_type& v);
+//
+// iterator insert_after(const_iterator p, const value_type& v);
+// iterator insert_after(const_iterator p, size_type n, const value_type& v);
+// template <class InputIterator>
+//     iterator insert_after(const_iterator p,
+//                           InputIterator first, InputIterator last);
+//
+// void resize(size_type n, const value_type& v);
+
+#include <forward_list>
+
+#include <cassert>
+#include "../../exception_safety_helpers.h"
+
+int main(int, char**) {
+  {
+    constexpr int ThrowOn = 1;
+    constexpr int Size = 1;
+    using T = ThrowingCopy<ThrowOn>;
+
+    // void push_front(const value_type& v);
+    test_exception_safety_throwing_copy<ThrowOn, Size>([](T* from, T*){
+      std::forward_list<T> c;
+      c.push_front(*from);
+    });
+
+    // iterator insert_after(const_iterator p, const value_type& v);
+    test_exception_safety_throwing_copy</*ThrowOn=*/1, Size>([](T* from, T*){
+      std::forward_list<T> c;
+      c.insert_after(c.before_begin(), *from);
+    });
+  }
+
+  {
+    constexpr int ThrowOn = 3;
+    constexpr int Size = 5;
+    using T = ThrowingCopy<ThrowOn>;
+    using C = std::forward_list<T>;
+    using Alloc = std::allocator<T>;
+
+    // forward_list(size_type n, const value_type& v);
+    test_exception_safety_throwing_copy<ThrowOn, Size>([](T* from, T*){
+      std::forward_list<T> c(Size, *from);
+      (void)c;
+    });
+
+    // forward_list(size_type n, const value_type& v, const allocator_type& a);
+    test_exception_safety_throwing_copy<ThrowOn, Size>([](T* from, T*){
+      std::forward_list<T> c(Size, *from, Alloc());
+      (void)c;
+    });
+
+    // template <class InputIterator>
+    //     forward_list(InputIterator first, InputIterator last);
+    test_exception_safety_throwing_copy<ThrowOn, Size>([](T* from, T* to){
+      std::forward_list<T> c(from, to);
+      (void)c;
+    });
+
+    // template <class InputIterator>
+    //     forward_list(InputIterator first, InputIterator last, const allocator_type& a);
+    test_exception_safety_throwing_copy<ThrowOn, Size>([](T* from, T* to){
+      std::forward_list<T> c(from, to, Alloc());
+      (void)c;
+    });
+
+    // forward_list(const forward_list& x);
+    test_exception_safety_throwing_copy_container<C, ThrowOn, Size>([](C&& in) {
+      std::forward_list<T> c(in);
+      (void)c;
+    });
+
+    // forward_list(const forward_list& x, const allocator_type& a);
+    test_exception_safety_throwing_copy_container<C, ThrowOn, Size>([](C&& in) {
+      std::forward_list<T> c(in, Alloc());
+      (void)c;
+    });
+
+    // forward_list& operator=(const forward_list& x);
+    test_exception_safety_throwing_copy_container<C, ThrowOn, Size>([](C&& in) {
+      std::forward_list<T> c;
+      c = in;
+    });
+
+    // template <class InputIterator>
+    //     void assign(InputIterator first, InputIterator last);
+    test_exception_safety_throwing_copy<ThrowOn, Size>([](T* from, T* to) {
+      std::forward_list<T> c;
+      c.assign(from, to);
+    });
+
+    // void assign(size_type n, const value_type& v);
+    test_exception_safety_throwing_copy<ThrowOn, Size>([](T* from, T*) {
+      std::forward_list<T> c;
+      c.assign(Size, *from);
+    });
+
+    // iterator insert_after(const_iterator p, size_type n, const value_type& v);
+    test_exception_safety_throwing_copy<ThrowOn, Size>([](T* from, T*) {
+      std::forward_list<T> c;
+      c.insert_after(c.before_begin(), Size, *from);
+    });
+
+    // template <class InputIterator>
+    //     iterator insert_after(const_iterator p,
+    //                           InputIterator first, InputIterator last);
+    test_exception_safety_throwing_copy<ThrowOn, Size>([](T* from, T* to) {
+      std::forward_list<T> c;
+      c.insert_after(c.before_begin(), from, to);
+    });
+
+    // void resize(size_type n, const value_type& v);
+    test_exception_safety_throwing_copy<ThrowOn, Size>([](T* from, T*) {
+      std::forward_list<T> c;
+      c.resize(Size, *from);
+    });
+  }
+
+  return 0;
+}

diff  --git a/libcxx/test/std/containers/sequences/from_range_sequence_containers.h b/libcxx/test/std/containers/sequences/from_range_sequence_containers.h
index 7106cb8077332a..859b9c535f7ba3 100644
--- a/libcxx/test/std/containers/sequences/from_range_sequence_containers.h
+++ b/libcxx/test/std/containers/sequences/from_range_sequence_containers.h
@@ -17,6 +17,7 @@
 #include <ranges>
 #include <utility>
 
+#include "../exception_safety_helpers.h"
 #include "../from_range_helpers.h"
 #include "MoveOnly.h"
 #include "almost_satisfies_types.h"
@@ -125,18 +126,11 @@ constexpr void test_vector_bool(ValidateFunc validate) {
 template <template <class ...> class Container>
 void test_exception_safety_throwing_copy() {
 #if !defined(TEST_HAS_NO_EXCEPTIONS)
-  using T = ThrowingCopy<3>;
-  T::reset();
-  T in[5];
-
-  try {
-    Container<T> c(std::from_range, in);
-    assert(false); // The constructor call above should throw.
-
-  } catch (int) {
-    assert(T::created_by_copying == 3);
-    assert(T::destroyed == 2); // No destructor call for the partially-constructed element.
-  }
+  constexpr int ThrowOn = 3;
+  using T = ThrowingCopy<ThrowOn>;
+  test_exception_safety_throwing_copy<ThrowOn, /*Size=*/5>([](T* from, T* to) {
+    [[maybe_unused]] Container<T> c(std::from_range, std::ranges::subrange(from, to));
+  });
 #endif
 }
 

diff  --git a/libcxx/test/std/containers/sequences/insert_range_sequence_containers.h b/libcxx/test/std/containers/sequences/insert_range_sequence_containers.h
index 10fadca10eb85e..b77b2510eae230 100644
--- a/libcxx/test/std/containers/sequences/insert_range_sequence_containers.h
+++ b/libcxx/test/std/containers/sequences/insert_range_sequence_containers.h
@@ -18,6 +18,7 @@
 #include <type_traits>
 #include <vector>
 
+#include "../exception_safety_helpers.h"
 #include "../from_range_helpers.h"
 #include "../insert_range_helpers.h"
 #include "MoveOnly.h"
@@ -675,19 +676,12 @@ constexpr void test_sequence_assign_range_move_only() {
 template <template <class ...> class Container>
 void test_insert_range_exception_safety_throwing_copy() {
 #if !defined(TEST_HAS_NO_EXCEPTIONS)
-  using T = ThrowingCopy<3>;
-  T::reset();
-  T in[5];
-
-  try {
+  constexpr int ThrowOn = 3;
+  using T = ThrowingCopy<ThrowOn>;
+  test_exception_safety_throwing_copy<ThrowOn, /*Size=*/5>([](T* from, T* to) {
     Container<T> c;
-    c.insert_range(c.end(), in);
-    assert(false); // The function call above should throw.
-
-  } catch (int) {
-    assert(T::created_by_copying == 3);
-    assert(T::destroyed == 2); // No destructor call for the partially-constructed element.
-  }
+    c.insert_range(c.end(), std::ranges::subrange(from, to));
+  });
 #endif
 }
 
@@ -713,19 +707,12 @@ void test_insert_range_exception_safety_throwing_allocator() {
 template <template <class ...> class Container>
 void test_prepend_range_exception_safety_throwing_copy() {
 #if !defined(TEST_HAS_NO_EXCEPTIONS)
-  using T = ThrowingCopy<3>;
-  T::reset();
-  T in[5];
-
-  try {
+  constexpr int ThrowOn = 3;
+  using T = ThrowingCopy<ThrowOn>;
+  test_exception_safety_throwing_copy<ThrowOn, /*Size=*/5>([](T* from, T* to) {
     Container<T> c;
-    c.prepend_range(in);
-    assert(false); // The function call above should throw.
-
-  } catch (int) {
-    assert(T::created_by_copying == 3);
-    assert(T::destroyed == 2); // No destructor call for the partially-constructed element.
-  }
+    c.prepend_range(std::ranges::subrange(from, to));
+  });
 #endif
 }
 
@@ -751,19 +738,12 @@ void test_prepend_range_exception_safety_throwing_allocator() {
 template <template <class ...> class Container>
 void test_append_range_exception_safety_throwing_copy() {
 #if !defined(TEST_HAS_NO_EXCEPTIONS)
-  using T = ThrowingCopy<3>;
-  T::reset();
-  T in[5];
-
-  try {
+  constexpr int ThrowOn = 3;
+  using T = ThrowingCopy<ThrowOn>;
+  test_exception_safety_throwing_copy<ThrowOn, /*Size=*/5>([](T* from, T* to) {
     Container<T> c;
-    c.append_range(in);
-    assert(false); // The function call above should throw.
-
-  } catch (int) {
-    assert(T::created_by_copying == 3);
-    assert(T::destroyed == 2); // No destructor call for the partially-constructed element.
-  }
+    c.append_range(std::ranges::subrange(from, to));
+  });
 #endif
 }
 
@@ -789,19 +769,12 @@ void test_append_range_exception_safety_throwing_allocator() {
 template <template <class ...> class Container>
 void test_assign_range_exception_safety_throwing_copy() {
 #if !defined(TEST_HAS_NO_EXCEPTIONS)
-  using T = ThrowingCopy<3>;
-  T::reset();
-  T in[5];
-
-  try {
+  constexpr int ThrowOn = 3;
+  using T = ThrowingCopy<ThrowOn>;
+  test_exception_safety_throwing_copy<ThrowOn, /*Size=*/5>([](T* from, T* to) {
     Container<T> c;
-    c.assign_range(in);
-    assert(false); // The function call above should throw.
-
-  } catch (int) {
-    assert(T::created_by_copying == 3);
-    assert(T::destroyed == 2); // No destructor call for the partially-constructed element.
-  }
+    c.assign_range(std::ranges::subrange(from, to));
+  });
 #endif
 }
 


        


More information about the libcxx-commits mailing list