实现 std::vector::push_back 强异常安全

Implementing std::vector::push_back strong exception safety(实现 std::vector::push_back 强异常安全)
本文介绍了实现 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::*_back 不要求 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 Ts 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.

这篇关于实现 std::vector::push_back 强异常安全的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持编程学习网!

本站部分内容来源互联网,如果有图片或者内容侵犯您的权益请联系我们删除!

相关文档推荐

What is the proper function for comparing two C-style strings?(比较两个 C 风格字符串的正确函数是什么?)
Image Capture with OpenCV - Select Timeout Error(使用 OpenCV 捕获图像 - 选择超时错误)
SHA256 HMAC using OpenSSL 1.1 not compiling(使用 OpenSSL 1.1 的 SHA256 HMAC 未编译)
How to make a Debian package depend on multiple versions of libboost(如何制作一个Debian包依赖于多个版本的libboost)
Why does strcpy_s not exist anywhere on my system?(为什么我系统上的任何地方都不存在 strcpy_s?)
Simplest way to get current time in current timezone using boost::date_time?(使用 boost::date_time 在当前时区获取当前时间的最简单方法?)