实施 std::vector::push_back 强异常安全
Implementing std::vector::push_back strong exception safety
我正在根据 post-2018 年圣地亚哥草案 (N4791) 实施我自己的向量,并且有一些关于实施强异常安全性的问题。
这是一些代码:
template <typename T, typename Allocator>
void Vector<T, Allocator>::push_back(const T& value)
{
if (buffer_capacity == 0)
{
this->Allocate(this->GetSufficientCapacity(1));
}
if (buffer_size < buffer_capacity)
{
this->Construct(value);
return;
}
auto new_buffer = CreateNewBuffer(this->GetSufficientCapacity(
buffer_size + 1), allocator);
this->MoveAll(new_buffer);
try
{
new_buffer.Construct(value);
}
catch (...)
{
this->Rollback(new_buffer, std::end(new_buffer));
throw;
}
this->Commit(std::move(new_buffer));
}
template <typename T, typename Allocator>
void Vector<T, Allocator>::Allocate(size_type new_capacity)
{
elements = std::allocator_traits<Allocator>::allocate(allocator,
new_capacity);
buffer_capacity = new_capacity;
}
template <typename T, typename Allocator> template <typename... Args>
void Vector<T, Allocator>::Construct(Args&&... args)
{
// TODO: std::to_address
std::allocator_traits<Allocator>::construct(allocator,
elements + buffer_size, std::forward<Args>(args)...);
++buffer_size;
}
template <typename T, typename Allocator>
Vector<T, Allocator> Vector<T, Allocator>::CreateNewBuffer(
size_type new_capacity, const Allocator& new_allocator)
{
Vector new_buffer{new_allocator};
new_buffer.Allocate(new_capacity);
return new_buffer;
}
template <typename T, typename Allocator>
void Vector<T, Allocator>::Move(iterator first, iterator last, Vector& buffer)
{
if (std::is_nothrow_move_constructible_v<T> ||
!std::is_copy_constructible_v<T>)
{
std::move(first, last, std::back_inserter(buffer));
}
else
{
std::copy(first, last, std::back_inserter(buffer));
}
}
template <typename T, typename Allocator
void Vector<T, Allocator>::MoveAll(Vector& buffer)
{
Move(std::begin(*this), std::end(*this), buffer);
}
template <typename T, typename Allocator>
void Vector<T, Allocator>::Rollback(Vector& other, iterator last) noexcept
{
if (!std::is_nothrow_move_constructible_v<T> &&
std::is_copy_constructible_v<T>)
{
return;
}
std::move(std::begin(other), last, std::begin(*this));
}
template <typename T, typename Allocator>
void Vector<T, Allocator>::Commit(Vector&& other) noexcept
{
this->Deallocate();
elements = other.elements;
buffer_capacity = other.buffer_capacity;
buffer_size = other.buffer_size;
allocator = other.allocator;
other.elements = nullptr;
other.buffer_capacity = 0;
other.buffer_size = 0;
}
我发现此代码有 2 个问题。我已经尝试遵循 std::move_if_noexcept
逻辑,但是如果元素不可抛出移动构造但 allocator_traits::construct
抛出异常,比如说,自定义分配器中的一些日志代码?然后我的 MoveAll
调用将抛出并仅产生基本保证。这是标准的缺陷吗? Allocator::construct
是否应该有更严格的措辞?
还有 Rollback
中的另一个。只有当被移动的元素不可抛出移动时,它才会真正产生强有力的保证。否则,再次,只有基本保证。这是应该的样子吗?
基于范围的 std::move/copy
函数无法提供强大的异常保证。如果发生异常,您需要一个指向最后一个成功 copied/moved 元素的迭代器,以便您可以正确撤消操作。您必须手动执行 copy/move(或编写专门的函数来执行此操作)。
至于你的问题的细节,标准并没有真正解决如果 construct
发出不是从正在构造的对象的构造函数中抛出的异常应该发生什么。该标准的意图(出于我将在下面解释的原因)可能是这种情况永远不应该发生。但是我还没有在标准中找到关于此的任何声明。因此,让我们暂时假设这是可能的。
为了让分配器感知容器能够提供强异常保证,construct
至少不能在 构造对象后抛出 。毕竟,你并不知道抛出的是什么异常,否则你无法判断这个对象是否构造成功。这将使实施标准要求的行为变得不可能。因此,让我们假设用户没有做任何使实现变得不可能的事情。
在这种情况下,您可以编写代码,假设 construct
发出的任何异常都意味着未构造对象。如果 construct
发出异常,尽管给出了调用 noexcept
构造函数的参数,那么您假设从未调用过构造函数。然后你相应地编写你的代码。
在复制的情况下,你只需要删除任何已经复制的元素(当然是相反的顺序)。移动案例有点棘手,但仍然很可行。您必须将每个成功移动的对象移动分配回其原始位置。
问题? vector<T>::*_back
不要求 T
是 MoveAssignable。只需要T
是MoveInsertable即可:即可以使用分配器在未初始化的内存中构造它们。但是您并没有将它移动到未初始化的内存中;您需要将其移动到已存在移出 T
的位置。因此,为了保留此要求,您需要销毁所有已成功移动的 T
,然后将它们 MoveInsert 放回原位。
但由于 MoveInsertion 需要使用 construct
,如前所述,这可能会抛出... oops。事实上,这件事很精确 为什么 vector
的重新分配函数不移动 除非 类型是不可移动的或不可移动的-可复制(如果是后一种情况,您不会获得强异常保证)。
所以对我来说似乎很清楚,标准期望任何分配器的 construct
方法仅在所选构造函数抛出时抛出。没有其他方法可以在 vector
中实现所需的行为。但是鉴于没有明确声明这个要求,我会说这是标准中的一个缺陷。这不是一个新缺陷,因为我查看了 C++17 标准而不是工作文件。
显然这是一个 LWG issue since 2014 的主题,解决它的方法……很麻烦。
我正在根据 post-2018 年圣地亚哥草案 (N4791) 实施我自己的向量,并且有一些关于实施强异常安全性的问题。
这是一些代码:
template <typename T, typename Allocator>
void Vector<T, Allocator>::push_back(const T& value)
{
if (buffer_capacity == 0)
{
this->Allocate(this->GetSufficientCapacity(1));
}
if (buffer_size < buffer_capacity)
{
this->Construct(value);
return;
}
auto new_buffer = CreateNewBuffer(this->GetSufficientCapacity(
buffer_size + 1), allocator);
this->MoveAll(new_buffer);
try
{
new_buffer.Construct(value);
}
catch (...)
{
this->Rollback(new_buffer, std::end(new_buffer));
throw;
}
this->Commit(std::move(new_buffer));
}
template <typename T, typename Allocator>
void Vector<T, Allocator>::Allocate(size_type new_capacity)
{
elements = std::allocator_traits<Allocator>::allocate(allocator,
new_capacity);
buffer_capacity = new_capacity;
}
template <typename T, typename Allocator> template <typename... Args>
void Vector<T, Allocator>::Construct(Args&&... args)
{
// TODO: std::to_address
std::allocator_traits<Allocator>::construct(allocator,
elements + buffer_size, std::forward<Args>(args)...);
++buffer_size;
}
template <typename T, typename Allocator>
Vector<T, Allocator> Vector<T, Allocator>::CreateNewBuffer(
size_type new_capacity, const Allocator& new_allocator)
{
Vector new_buffer{new_allocator};
new_buffer.Allocate(new_capacity);
return new_buffer;
}
template <typename T, typename Allocator>
void Vector<T, Allocator>::Move(iterator first, iterator last, Vector& buffer)
{
if (std::is_nothrow_move_constructible_v<T> ||
!std::is_copy_constructible_v<T>)
{
std::move(first, last, std::back_inserter(buffer));
}
else
{
std::copy(first, last, std::back_inserter(buffer));
}
}
template <typename T, typename Allocator
void Vector<T, Allocator>::MoveAll(Vector& buffer)
{
Move(std::begin(*this), std::end(*this), buffer);
}
template <typename T, typename Allocator>
void Vector<T, Allocator>::Rollback(Vector& other, iterator last) noexcept
{
if (!std::is_nothrow_move_constructible_v<T> &&
std::is_copy_constructible_v<T>)
{
return;
}
std::move(std::begin(other), last, std::begin(*this));
}
template <typename T, typename Allocator>
void Vector<T, Allocator>::Commit(Vector&& other) noexcept
{
this->Deallocate();
elements = other.elements;
buffer_capacity = other.buffer_capacity;
buffer_size = other.buffer_size;
allocator = other.allocator;
other.elements = nullptr;
other.buffer_capacity = 0;
other.buffer_size = 0;
}
我发现此代码有 2 个问题。我已经尝试遵循 std::move_if_noexcept
逻辑,但是如果元素不可抛出移动构造但 allocator_traits::construct
抛出异常,比如说,自定义分配器中的一些日志代码?然后我的 MoveAll
调用将抛出并仅产生基本保证。这是标准的缺陷吗? Allocator::construct
是否应该有更严格的措辞?
还有 Rollback
中的另一个。只有当被移动的元素不可抛出移动时,它才会真正产生强有力的保证。否则,再次,只有基本保证。这是应该的样子吗?
基于范围的 std::move/copy
函数无法提供强大的异常保证。如果发生异常,您需要一个指向最后一个成功 copied/moved 元素的迭代器,以便您可以正确撤消操作。您必须手动执行 copy/move(或编写专门的函数来执行此操作)。
至于你的问题的细节,标准并没有真正解决如果 construct
发出不是从正在构造的对象的构造函数中抛出的异常应该发生什么。该标准的意图(出于我将在下面解释的原因)可能是这种情况永远不应该发生。但是我还没有在标准中找到关于此的任何声明。因此,让我们暂时假设这是可能的。
为了让分配器感知容器能够提供强异常保证,construct
至少不能在 构造对象后抛出 。毕竟,你并不知道抛出的是什么异常,否则你无法判断这个对象是否构造成功。这将使实施标准要求的行为变得不可能。因此,让我们假设用户没有做任何使实现变得不可能的事情。
在这种情况下,您可以编写代码,假设 construct
发出的任何异常都意味着未构造对象。如果 construct
发出异常,尽管给出了调用 noexcept
构造函数的参数,那么您假设从未调用过构造函数。然后你相应地编写你的代码。
在复制的情况下,你只需要删除任何已经复制的元素(当然是相反的顺序)。移动案例有点棘手,但仍然很可行。您必须将每个成功移动的对象移动分配回其原始位置。
问题? vector<T>::*_back
不要求 T
是 MoveAssignable。只需要T
是MoveInsertable即可:即可以使用分配器在未初始化的内存中构造它们。但是您并没有将它移动到未初始化的内存中;您需要将其移动到已存在移出 T
的位置。因此,为了保留此要求,您需要销毁所有已成功移动的 T
,然后将它们 MoveInsert 放回原位。
但由于 MoveInsertion 需要使用 construct
,如前所述,这可能会抛出... oops。事实上,这件事很精确 为什么 vector
的重新分配函数不移动 除非 类型是不可移动的或不可移动的-可复制(如果是后一种情况,您不会获得强异常保证)。
所以对我来说似乎很清楚,标准期望任何分配器的 construct
方法仅在所选构造函数抛出时抛出。没有其他方法可以在 vector
中实现所需的行为。但是鉴于没有明确声明这个要求,我会说这是标准中的一个缺陷。这不是一个新缺陷,因为我查看了 C++17 标准而不是工作文件。
显然这是一个 LWG issue since 2014 的主题,解决它的方法……很麻烦。