Правильный способ обработки вектора указателей на производные классы? - PullRequest
1 голос
/ 23 апреля 2020

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

#include <iostream>
#include <vector>
using namespace std;

struct base {};

struct derived : public base
{
  derived() {}
};

struct Layer
{
  vector<base*> effects;

  Layer() {}

  ~Layer()
  {
    for(int ii = 0; ii < effects.size(); ii++)
    {
      cout << "called effect deleter" << endl;
      delete effects[ii];
    }
  }
};

int main()
{
  vector<Layer> layers;
  for(int i = 0; i < 10; i++)
  {
    layers.push_back(Layer());
    layers[i].effects.push_back(new derived());
    cout << i << endl;
  }
}

Когда я компилирую и запускаю этот код, я получаю следующий вывод:

0
called effect deleter
1
called effect deleter
called effect deleter

Я запутался. Почему он печатает только 0 и 1 вместо 0 до 9? Если я удалю деструктор Layer, у меня не будет утечки памяти? Как правильно справиться с этой ситуацией?

Ответы [ 2 ]

1 голос
/ 23 апреля 2020

Если base *b, но *b на самом деле derived, тогда delete b; - это мгновенный UB, потому что base::~base() не virtual. По сути, вы пытаетесь удалить base часть *b (указанную типом *b, который равен base), но, поскольку деструктор не является виртуальным, вы забыли уничтожить derived часть первая. Это приводит к ужасным вещам (вероятно, к некоторому повреждению стека?). Исправьте это:

struct base {
    virtual ~base() = default;
};

Кроме того, Layer::Layer(Layer const&) (неявно определенный конструктор копирования) не работает, поскольку он дублирует указатели base* из аргумента. Этот конструктор копирования вызывается, когда std::vector<Layer> необходимо изменить размер своего хранилища, что влечет за собой выделение нового блока непрерывной памяти и создание новых Layer из старых, а затем уничтожение старых. За исключением а) ​​Layer не имеет конструктора перемещения (объявленный пользователем деструктор предотвращает его генерацию), поэтому «перемещение» Layer s просто копирует их, а б) копирование Layer s концептуально некорректно, так как когда один Layer будет уничтожен, он будет delete всех своих base с, а затем другой Layer попытается удалить их позже. Отключите Layer копирование и запишите его ход.

struct Layer {
    std::vector<base*> effects;
    Layer() = default;
    Layer(Layer const&) = delete;
    Layer(Layer&&) = default;
    Layer &operator=(Layer const&) = delete;
    Layer &operator=(Layer &&other) {
        std::swap(this->effects, other.effects);
        // what used to be this->effects will be deleted when other is destroyed
        return *this;
    }

    ~Layer() {
        for(int ii = 0; ii < effects.size(); ii++) {
            std::cout << "called effect deleter\n"; // endl is usually unnecessary, and "\n" is portable
            delete effects[ii];
        }
    }
};

Мораль такова: используйте умные указатели:).

struct effect_deleter {
    void operator()(base *b) {
        std::cout << "called effect deleter\n";
        delete b;
    }
};
struct Layer {
    std::vector<std::unique_ptr<base, effect_deleter>> effects;
    // 3 constructors, 2 operator=s, and 1 destructor
    // all doing the right thing, "for free"
    // "Rule of 5" becomes "Rule of 0"
};

int main() {
    std::vector<Layer> layers;
    for(int i = 0; i < 10; i++) {
        layers.emplace_back(); // using push_back(Layer()) constructs a temporary Layer, then constructs the actual Layer in the vector by moving from the temporary; emplace_back just passes the arguments (here nothing) on to the constructor of Layer
        layers[i].effects.emplace_back(new derived()); // similar
        std::cout << i << "\n";
    }
}
0 голосов
/ 23 апреля 2020

Вы не можете сделать это как прямой вектор в c ++ - разные производные классы имеют разные размеры, и, как таковые, вы получаете нарезку, когда конструкторы копирования помещают элементы в массив (classi c Object Slicing).

Наименее болезненный способ сделать это правильно - использовать вектор shared_ptr в базовом классе. Можно также использовать массив из unique_ptr, но вы должны быть очень осторожными с этим, поскольку такие вещи, как циклы for на основе диапазона, могут переносить владение вашими данными во временную переменная итерации.

...