C++ 实现std::vector::push_-back强异常安全

C++ 实现std::vector::push_-back强异常安全,c++,containers,c++20,exception-safety,C++,Containers,C++20,Exception Safety,我正在基于2018年后的圣地亚哥草案()实现我自己的向量,并且对实现强异常安全性有一些疑问 下面是一些代码: template <typename T, typename Allocator> void Vector<T, Allocator>::push_back(const T& value) { if (buffer_capacity == 0) { this->Allocate(this->GetSufficie

我正在基于2018年后的圣地亚哥草案()实现我自己的向量,并且对实现强异常安全性有一些疑问

下面是一些代码:

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;
}
模板
无效向量::推回(常量T和值)
{
如果(缓冲区容量==0)
{
此->分配(此->获取足够容量(1));
}
if(缓冲区大小<缓冲区容量)
{
这个->构造(值);
返回;
}
自动新建缓冲区=CreateNewBuffer(此->获取足够的容量(
缓冲区(U大小+1),分配器);
此->移动所有(新的\u缓冲区);
尝试
{
新的缓冲区构造(值);
}
捕获(…)
{
这->回滚(新建缓冲区,std::end(新建缓冲区));
投掷;
}
这个->提交(std::move(new_buffer));
}
模板
无效向量::分配(大小类型新容量)
{
elements=std::allocator\u traits::allocate(分配器,
新产能);
缓冲区容量=新容量;
}
模板
void Vector::Construct(Args&&…Args)
{
//TODO:std::to\U地址
std::allocator\u traits::construct(分配器,
元素+缓冲区大小,标准::转发(args);
++缓冲区大小;
}
模板
矢量::CreateNewBuffer(
大小\类型新\容量、常量分配器和新\分配器)
{
向量new_缓冲区{new_分配器};
新的缓冲区分配(新的缓冲区容量);
返回新的缓冲区;
}
模板
void Vector::Move(迭代器优先、迭代器最后、向量和缓冲区)
{
if(std::是否为非行移动可构造||
!std::是否可复制(可构造)
{
std::move(第一个、最后一个std::back_插入器(缓冲区));
}
其他的
{
std::copy(第一个、最后一个std::back_插入器(缓冲区));
}
}
模板释放();
元素=其他元素;
缓冲区容量=其他缓冲区容量;
缓冲区大小=其他缓冲区大小;
分配器=other.allocator;
other.elements=nullptr;
其他缓冲区容量=0;
其他缓冲区大小=0;
}
我发现这个代码有两个问题。我试图遵循
std::move\if\u noexcept
逻辑,但是如果元素不是row move可构造的,而是
allocator\u traits::construct
在自定义分配器中的一些日志代码中抛出异常,该怎么办?然后,我的
MoveAll
调用将抛出并仅产生基本保证。这是标准中的缺陷吗?关于
分配器::construct
是否应该有更严格的措辞


回滚
中还有一个。只有当移动的元素不可分配时,它才会产生强大的保证。否则,再次强调,只有基本保证。应该是这样吗?

基于范围的
std::move/copy
功能无法提供强大的异常保证。在发生异常的情况下,需要对成功复制/移动的最后一个元素使用迭代器,以便可以正确撤消操作。您必须手动执行复制/移动(或编写专门的函数)

至于你问题的细节,标准并没有真正解决如果
construct
发出异常,而该异常不是从正在构造的对象的构造函数中抛出的,那么应该发生什么。该标准的意图(出于我将在下文解释的原因)可能是这种情况永远不会发生。但我还没有在标准中找到关于这一点的任何声明。让我们暂时假设这是可能的

为了使分配器感知容器能够提供强异常保证,
construct
至少不能在构造对象后抛出。毕竟,您不知道抛出了什么异常,否则您将无法判断对象是否成功构造。这将使实现标准要求的行为变得不可能。因此,让我们假设用户没有做一些使实现变得不可能的事情

在这种情况下,您可以编写代码,假设
construct
发出的任何异常都意味着未构造对象。如果
construct
发出异常,尽管给定的参数将调用
noexcept
构造函数,那么您假定从未调用过该构造函数。然后编写相应的代码

在复制的情况下,您只需要删除任何已复制的元素(当然顺序相反)。移动的情况有点棘手,但仍然是可行的。必须将每个成功移动的对象移回其原始位置

问题出在哪里<代码>向量::*_back不要求
T
是可移动可分配的。它只要求
T
是可移动插入的:也就是说,您可以使用分配器在未初始化的内存中构造它们。但你不会把它移到未初始化的内存中;您需要将其移动到已存在移动自
T
的位置。因此,为了保留此需求,您需要销毁所有成功移出的
T
s,然后将它们重新插入到位

但是由于MoveInsertion需要使用
构造
,正如前面建立的那样,它可能会抛出oops。事实上,这正是
vector
的重新分配函数不移动的原因,除非类型是不可移动的或不可复制的(如果是后一种情况,则无法获得强异常保证)

因此,我很清楚,标准要求任何分配器的
构造
方法只有在所选构造函数抛出时才会抛出。在
vector
中,没有其他方法可以实现所需的行为。但鉴于没有明确说明这一要求,我认为这是标准中的一个缺陷。这并不是一个新的缺陷,因为我查看了C++17标准,而不是工作环境