std::vector::erase. Что-то здесь не так

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Редкая задача в программировании решается без контейнеров. В C++ наиболее часто используемый контейнер - std::vector (возможно кто-то не согласится и скажет: "Зачем std::vector, когда есть boost", но это дела не меняет).

При работе с std::vector часто возникает задача - удалить из него элемент (или несколько элементов). На помощь приходит метод erase. И это работает! Но иногда мы можем столкнуться с ситуацией, когда что-то идёт не так.

Что же может пойти не так?

Рассмотрим небольшой пример. Исходный код лежит в репозитории.

С чего начиналось

Есть класс, который в деструкторе производит некоторую очистку (например удаляет запись о себе из базы данных).

Object.hpp
class Object {
public:
    explicit Object(int id);
    ~Object();

private:
    int m_id;

    template 
    friend T &operator<<(T &stream, const Object &object) {
        stream << object.m_id;
        return stream;
    }
};

Object.cpp
#include "Object.hpp"
#include <iostream>

Object::Object(int id)
    : m_id(id) {
    std::cout << "Create: " << m_id << std::endl;
}

Object::~Object() {
    std::cout << "Delete: " << m_id << std::endl;
}

При создании и удалении объекта выводим отладочную информацию, для понимания что где вызывается.

Создадим тестовую программу, в которой создадим std::vector объектов нашего класса, удалим один элемент и посмотрим на результат.

main.cpp
#include <iostream>
#include <vector>
#include "Object.hpp"

using Objects = std::vector;

void print(const Objects &objects) {
    std::cout << "Objects: ";
    for (const auto &object : objects) {
        std::cout << object << " ";
    }
    std::cout << std::endl;
}

int main() {
    constexpr int SIZE = 6;

    Objects objects;
    objects.reserve(SIZE);
    for (int i = 0; i < SIZE; ++i) {
        objects.emplace_back(i);
    }

    print(objects);

    std::cout << "~~~ EraseBegin ~~~" << std::endl;
    objects.erase(objects.begin() + 3);
    std::cout << "~~~ EraseEnd ~~~" << std::endl;

    print(objects);

    return 0;
}

Соберём этот пример. Запустим и посмотрим вывод.

Вывод приложения
Create: 0
Create: 1
Create: 2
Create: 3
Create: 4
Create: 5
Objects: 0 1 2 3 4 5
~~~ EraseBegin ~~~
Delete: 5
~~~ EraseEnd ~~~
Objects: 0 1 2 4 5
Delete: 0
Delete: 1
Delete: 2
Delete: 4
Delete: 5

Что мы видим?

При попытке удалить элемент 3 вызовом erase удаляется элемент 5.

Что-то напутали?

Нет. После вызова erase в контейнере действительно исчез элемент 3.

Что в итоге получается?

Элемент 3 не удаляется, а элемент 5 удаляется дважды.

Окунемся в метод erase

Что же делает метод erase на самом деле?

Метод erase для std::vector выполняет два шага:

  1. Перемещает элементы вектора, которые следуют за удаляемой частью, на место удаляемых элементов.

  2. Удаляет последние элементы, освободившиеся после перемещения.

# Исходный вектор #
[   0   ][   1   ][   2   ][   3   ][   4   ][   5   ]

# Шаг 1 #
[   0   ][   1   ][   2   ][   3   ][   4   ][   5   ]
                               <- move <- 
[   0   ][   1   ][   2   ][   4   ][   4   ][   5   ]
                                        <- move <- 
[   0   ][   1   ][   2   ][   4   ][   5   ][   5   ]

# Шаг 2 #
[   0   ][   1   ][   2   ][   4   ][   5   ][   5   ]
                                      resize(size - 1) 
[   0   ][   1   ][   2   ][   4   ][   5   ]

Теперь понятно, почему у нас удаляется элемент 5 два раза. Но что же с этим делать?

Решение (одно из нескольких возможных)

Поскольку на шаге 1 вызывается перемещающее присваивание, переопределим соответствующий оператор. Также определим перемещающий конструктор. Копирование из класса удалим для упрощения, поскольку это не играет роли в рассматриваемой ситуации.

Рассмотрим модификацию класса Object.

Object.hpp
class Object {
public:
    explicit Object(int id);

    Object(Object &other) = delete;
    Object &operator=(Object &other) = delete;

    Object(Object &&other);
    Object &operator=(Object &&other);

    ~Object();

    void clear();

private:
    int m_id;

    template 
    friend T &operator<<(T &stream, const Object &object) {
        stream << object.m_id;
        return stream;
    }
};

Object.cpp
#include "Object.hpp"

#include 

constexpr int INVALID_ID = -1;

Object::Object(int id)
    : m_id(id) {
    std::cout << "Create: " << m_id << std::endl;
}

Object::Object(Object &&other)
    : m_id(other.m_id) {
        other.m_id = INVALID_ID;
}

Object &Object::operator=(Object &&other) {
    clear();
    m_id = other.m_id;
    other.m_id = INVALID_ID;
    return *this;
}

Object::~Object() {
    std::cout << "Destroy: " << m_id << std::endl;
    clear();
}

void Object::clear() {
    if (m_id != INVALID_ID) {
        std::cout << "Delete: " << m_id << std::endl;
    }
}

Что мы видим?

Добавилась проверка на валидность идентификатора при очистке. Очистка вызывается в деструкторе и при перемещении.

Добавим дополнительное сообщение в деструктор, чтобы понимать, когда мы в него заходим.

Тестовая программа не изменилась.

Соберём, запустим и посмотрим вывод.

Вывод приложения
Create: 0
Create: 1
Create: 2
Create: 3
Create: 4
Create: 5
Objects: 0 1 2 3 4 5
~~~ EraseBegin ~~~
Delete: 3
Destroy: -1
~~~ EraseEnd ~~~
Objects: 0 1 2 4 5
Destroy: 0
Delete: 0
Destroy: 1
Delete: 1
Destroy: 2
Delete: 2
Destroy: 4
Delete: 4
Destroy: 5
Delete: 5

Как видим, при вызове erase происходит удаление нужного элемента 3, а также вызывается деструктор с невалидным идентификатором (это освобождается место от перемещённого элемента 5).

Спасибо за внимание!

Источник: https://habr.com/ru/articles/880026/


Интересные статьи

Интересные статьи

По информации СМИ, Роскомнадзор в течение следующих пяти лет планирует потратить почти 59 млрд рублей на обновление системы блокировки интернет-ресурсов в Рунете, включая значительную доработку компле...
При внедрении ИТ-решений в промышленности я периодически наблюдаю ситуацию, сильно усложняющую жизнь команде внедрения. Заказчик имеет яркий образ результата в будущем, но зачастую не имеет точного по...
Приветствую вас (лично вас, а не всех кто это читает)! Сегодня мы: Создадим приложение (навык) Алисы с использованием нового (октябрь 2019) сервиса Yandex Cloud Functions. Настроим н...
В 2019 году люди знакомятся с брендом, выбирают и, что самое главное, ПОКУПАЮТ через интернет. Сегодня практически у любого бизнеса есть свой сайт — от личных блогов, зарабатывающих на рекламе, до инт...
Тема статьи навеяна результатами наблюдений за методикой создания шаблонов различными разработчиками, чьи проекты попадали мне на поддержку. Порой разобраться в, казалось бы, такой простой сущности ка...