【问题标题】:Implementing std::vector::push_back strong exception safety实现 std::vector::push_back 强异常安全
【发布时间】:2019-01-05 11:04:30
【问题描述】:

我正在根据 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。只有当被移动的元素不能被移动分配时,它才会真正产生强有力的保证。否则,再次,只有基本保证。这是应该的吗?

【问题讨论】:

  • 论文说: 注意:这是初稿。众所周知,它是不完整和不正确的,并且有很多错误的格式
  • 所以也许我们应该在 C++20 发布之前修复它?
  • 是的,审查文档的人越多越好。这个论坛是突出这些问题的好地方。
  • @Damian 是的,这是 C++ 标准的工作草案。
  • 先构造值,再移动元素!

标签: c++ containers c++20 exception-safety


【解决方案1】:

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

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

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

在这种情况下,您可以编写代码,假设construct 发出的任何异常都表示对象未构造。如果construct 发出异常,尽管给定的参数会调用noexcept 构造函数,那么您假设构造函数从未被调用。然后你相​​应地编写你的代码。

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

问题? vector&lt;T&gt;::*_back 不要求 T 是 MoveAssignable。它只要求T 是MoveInsertable:也就是说,您可以使用分配器在未初始化的内存中构造它们。但是您不会将其移入未初始化的内存中;您需要将其移动到已存在已移动的T 的位置。因此,要保留此要求,您需要销毁所有已成功移出的 Ts,然后将它们 MoveInsert 移回原位。

但由于 MoveInsertion 需要使用 construct,正如之前建立的那样,它可能会抛出...糟糕。确实,这正是为什么vector 的重新分配函数不会移动除非该类型不可移动或不可复制(如果是后者在这种情况下,您不会获得强异常保证)。

所以在我看来,任何分配器的construct 方法都被标准期望只在所选构造函数抛出时才抛出。在vector 中没有其他方法可以实现所需的行为。但鉴于没有明确声明此要求,我会说这是标准中的缺陷。而且这不是一个新缺陷,因为我查看的是 C++17 标准而不是工作文件。

显然这是LWG issue since 2014 的主题,解决它的方法...麻烦。

【讨论】:

  • 我使用std::back_inserter,它调用push_back,它调用Construct,它调用allocator_traits::construct。分配器被复制到 CreateNewBuffer 中,所以一切都是分配器感知的。
  • @Lyberta:已修复
  • 作用域分配器相当系统地打破了这种期望。这是 LWG 2461。
  • @T.C.:所有这一切都表明我们的分配器模型是多么糟糕。分配器不是我们构造对象的手段;他们应该一直为他们分配存储空间。
  • 具有讽刺意味的是,似乎没有人关心花哨的分配器。看看 5 年前报告的 this libstdc++ bug 仍然没有修复,它基本上禁止任何花哨的 pointer 分配器。更不用说rebind 的语法有多丑了。
猜你喜欢
  • 2015-03-24
  • 2014-08-27
  • 1970-01-01
  • 2017-02-01
  • 2021-06-02
  • 2017-09-15
  • 1970-01-01
  • 2013-10-15
  • 2021-06-18
相关资源
最近更新 更多