[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
This commit is contained in:
varconst 2023-07-12 10:11:09 -07:00
parent 345f8699c7
commit d0b51657c2
10 changed files with 554 additions and 146 deletions

View File

@ -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_;

View File

@ -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
}

View File

@ -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 difference_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;
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 different 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

View File

@ -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 different 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 {

View File

@ -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;
}

View File

@ -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
}

View File

@ -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
}