实现 std::vector::push_back 强异常安全
我正在根据 2018 年后圣地亚哥草案 (N4791) 实施我自己的向量,并有一些有关实施强异常安全的问题.
I'm implementing my own vector based on post-2018 San Diego draft (N4791) and have some questions regarding implementing strong exception safety.
这里有一些代码:
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
是否应该有更严格的措辞?
I see 2 problems with this code. I've tried to follow the std::move_if_noexcept
logic, but what if the element is nothrow move constructible but allocator_traits::construct
throws exception in, say, some logging code inside custom allocator? Then my MoveAll
call will throw and produce only basic guarantee. Is this a defect in the standard? Should there be more strict wording on Allocator::construct
?
Rollback
中的另一个.只有当被移动的元素不能被移动分配时,它才会真正产生强有力的保证.否则,再次,只有基本保证.这是应该的吗?
And another one in Rollback
. It really produces strong guarantee only if the moved elements are nothrow move assignable. Otherwise, again, only basic guarantee. Is this how it is supposed to be?
推荐答案
基于范围的 std::move/copy
函数无法提供强大的异常保证.如果发生异常,您需要一个指向成功复制/移动的最后一个元素的迭代器,以便您可以正确撤消操作.您必须手动进行复制/移动(或编写专门的函数来执行此操作).
The range-based std::move/copy
functions are not capable of providing a strong exception guarantee. In the event of an exception, you need an iterator to the last element that was successfully copied/moved, so that you can undo things properly. You have to do the copy/move manually (or write a specialized function to do so).
至于您的问题的细节,该标准并没有真正解决如果 construct
发出一个不是从正在构造的对象的构造函数中抛出的异常应该发生的情况.该标准的意图(出于我将在下面解释的原因)可能是这种情况永远不应该发生.但我还没有在标准中找到任何关于此的声明.因此,让我们暂时假设这是可能的.
As for the particulars of your question, the standard does not really address what should happen if construct
emits an exception which is not thrown from within the constructor of the object being construction. The intent of the standard (for reasons I will explain below) is probably that this circumstance should never happen. But I have yet to find any statement in the standard about this. So let's assume for a moment that this is intended to be possible.
为了让分配器感知容器能够提供强异常保证,construct
至少不能在构造对象之后抛出 .毕竟,你不知道抛出了什么异常,否则你将无法判断对象是否构造成功.这将使实施标准要求的行为变得不可能.因此,让我们假设用户没有做任何无法实现的事情.
In order for allocator-aware containers to be able to offer the strong-exception guarantee, construct
at the very least must not throw after constructing the object. After all, you don't know what exception was thrown, so otherwise you would not be able to tell if the object was successfully constructed or not. That would make implementing the standard required behavior impossible. So let us assume that the user has not done something that makes implementation impossible.
鉴于这种情况,您可以编写代码,假设 construct
发出的任何异常都意味着对象没有被构造.如果 construct
发出异常,尽管给定了会调用 noexcept
构造函数的参数,那么您假定构造函数从未被调用.然后你相??应地编写你的代码.
Given this circumstance, you can write your code assuming that any exception emitted by construct
means that the object was not constructed. If construct
emits an exception despite being given arguments that would invoke a noexcept
constructor, then you assume that the constructor was never called. And you write your code accordingly.
在复制的情况下,您只需要删除任何已经复制的元素(当然是相反的顺序).移动案例有点棘手,但仍然很可行.您必须将每个成功移动的对象移动分配回其原始位置.
In the case of copying, you only need to delete any already-copied elements (in reverse order of course). The move case is a bit trickier, but still quite doable. You have to move-assign each successfully-moved object back into its original position.
问题?vector
不要求 T
是 MoveAssignable.它只要求 T
是 MoveInsertable:也就是说,您可以使用分配器在未初始化的内存中构造它们.但是您不会将其移入未初始化的内存中;您需要将其移动到已存在已移动的 T
的位置.因此,要保留此要求,您需要销毁所有成功移出的 T
,然后将它们 MoveInsert 放回原处.
The problem? vector<T>::*_back
does not require that T
be MoveAssignable. It only requires that T
be MoveInsertable: that is, you can use an allocator to construct them in uninitialized memory. But you're not moving it into uninitialized memory; you need to move it to where a moved-from T
already exists. So to preserve this requirement, you would need to destroy all of the T
s that were successfully moved-from and then MoveInsert them back into place.
但由于 MoveInsertion 需要使用 construct
,如之前建立的那样,它可能会抛出... oops.确实,这正是 为什么 vector
的重新分配函数不会移动 除非 类型不可移动或不可复制(如果是后一种情况,您不会获得强异常保证).
But since MoveInsertion requires using construct
, which as previously established might throw... oops. Indeed, this very thing is preciesely why vector
's reallocation functions do not move unless the type is nothrow-moveable or is non-copyable (and if it's the latter case, you don't get the strong-exception guarantee).
所以对我来说似乎很清楚,标准期望任何分配器的 construct
方法仅在所选构造函数抛出时才抛出.在 vector
中没有其他方法可以实现所需的行为.但鉴于没有明确声明此要求,我会说这是标准中的缺陷.而且这不是一个新缺陷,因为我查看的是 C++17 标准而不是工作文件.
So it seems pretty clear to me that any allocator's construct
method is expected by the standard to only throw if the selected constructor throws. There's no other way to implement the required behavior in vector
. But given that there is no explicit statement of this requirement, I would say that this is a defect in the standard. And it's not a new defect, since I looked through the C++17 standard rather than the working paper.
显然,这一直是 自 2014 年以来的 LWG 问题的主题,其解决方案是...麻烦.
Apparently this has been the subject of an LWG issue since 2014, with solutions to it being... troublesome.
相关文章