我正在根据2018年后的圣地亚哥草案(N4791)实现自己的向量,并对实现强异常安全性有一些疑问。
以下是一些代码:
$("#amount").keyup(function () {
var value = $(this).val();
$("#amountValue").text(value);
});
$("#vatAmt").keyup(function () {
var valueVat = $(this).val();
var sum = value + valueVat;
$("#totalAmount").text(sum);
}).keyup();
我看到此代码有2个问题。我尝试遵循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;
}
逻辑,但是如果元素不可移动但std::move_if_noexcept
会在自定义分配器中的某些日志记录代码中引发异常,该怎么办?然后,我的allocator_traits::construct
调用将引发并仅产生基本保证。这是标准的缺陷吗? MoveAll
上的措词应该更严格吗?
还有Allocator::construct
中的另一个。只有在移动的元素没有移动可分配的情况下,它才真正产生强有力的保证。否则,只有基本保证。这是应该的样子吗?
答案 0 :(得分:4)
基于范围的std::move/copy
函数不能提供强大的异常保证。如果发生异常,则需要一个对成功复制/移动的最后一个元素进行迭代的迭代器,以便可以正确地撤消操作。您必须手动执行复制/移动(或编写专门的功能来执行此操作)。
关于您的问题的详细信息,该标准并未真正解决construct
发出的异常(该异常不是从正在构造的对象的构造函数中引发的异常)应该发生的情况。该标准的目的(出于我将在下面解释的原因)可能是这种情况永远都不会发生。但我尚未在标准中找到与此相关的任何陈述。因此,让我们暂时假设这是可能的。
为了使能识别分配器的容器能够提供强异常保证,construct
至少不得在构造对象后抛出 。毕竟,您不知道抛出了什么异常,因此否则您将无法判断对象是否已成功构建。这将使实现标准的必需行为成为不可能。因此,让我们假设用户没有做一些无法实现的事情。
在这种情况下,可以假设construct
发出的任何异常表示未构造该对象,可以编写代码。如果construct
发出了调用noexcept
构造函数的参数后仍发出异常,则您假设从未调用过该构造函数。然后您相应地编写代码。
在复制的情况下,您只需要删除所有已经复制的元素(当然是相反的顺序)。搬家案有些棘手,但仍然可行。您必须将每个成功移动的对象重新分配回其原始位置。
有问题吗? vector<T>::*_back
不需要T
是MoveAssignable。它仅要求T
为Move Insertable :也就是说,您可以使用分配器在未初始化的内存中构造它们。但是,您并没有将其移至未初始化的内存中。您需要将其移至已从T
移动的位置。因此,为了保留此要求,您需要销毁所有已成功移出的T
,然后将它们移回原位。
但是,由于MoveInsertion需要使用construct
,因此先前建立的方法可能会抛出... 糟糕 。确实,这正是为什么 vector
的重新分配函数不会移动 的原因,除非该类型不可移动或不可复制(如果它是在后一种情况下,您不获得强异常保证。
因此,对于我来说,很显然,标准期望任何分配器的construct
方法仅在所选构造函数抛出时抛出。没有其他方法可以在vector
中实现所需的行为。但是鉴于没有关于此要求的显式陈述,我要说这是标准的缺陷。这并不是一个新缺陷,因为我浏览了C ++ 17标准而不是工作论文。
显然,这是LWG issue since 2014的主题,解决方案却很麻烦。