Реализация std :: vector :: push_back безопасности сильных исключений - PullRequest
0 голосов
/ 05 января 2019

Я реализую свой собственный вектор, основанный на черновом варианте Сан-Диего после 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, но что, если элемент nothrow move constructible, но allocator_traits::construct выдает исключение, скажем, в некотором коде регистрации внутри собственного распределителя? Тогда мой MoveAll вызов сработает и выдаст только основную гарантию. Это дефект в стандарте? Должна ли быть более строгая формулировка Allocator::construct?

И еще один в Rollback. Это действительно дает сильную гарантию, только если перемещенные элементы не могут быть назначены на ход. Иначе опять же только базовая гарантия. Это так и должно быть?

1 Ответ

0 голосов
/ 05 января 2019

Функции 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, а не рабочий документ.

Очевидно, что с 2014 года этот вопрос стал проблемой LWG, и решение этой проблемы было ... хлопотным.

...