5

I'm implementing my own vector based on post-2018 San Diego draft (N4791) and have some questions regarding implementing strong exception safety.

Here is some code:

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;
}

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?

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?

4

1 回答 1

3

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

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

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

鉴于这种情况,您可以编写代码,假设任何发出的异常都construct意味着对象没有被构造。如果construct尽管给出了会调用noexcept构造函数的参数,但仍发出异常,那么您假设构造函数从未被调用。然后你相​​应地编写你的代码。

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

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

但是由于 MoveInsertion 需要 using construct,因此之前建立的可能会抛出... oops。事实上,这正是为什么 vector' 的重新分配函数不会移动的原因,除非类型不可移动或不可复制(如果是后一种情况,您得不到强异常保证)。

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

显然,自 2014 年以来,这一直是 LWG 问题的主题,其解决方案......麻烦。

于 2019-01-05T14:51:05.013 回答