Обертка C++ вокруг необработанного массива - управление памятью

Обертка C++ вокруг необработанного массива - управление памятью
Обертка C++ вокруг необработанного массива - управление памятью - knobelman @ Unsplash

У меня есть простой класс-обертка вокруг массива в стиле C. Я не хочу использовать std::vector, так как хочу иметь только один массив, даже если я скопирую struct. При использовании std::vector вектор также копируется.

struct RawDataArray {
    double* data;
    size_t size;
    
    static RawDataArray CreateNew(size_t size){
        return RawDataArray(new double[size], size);
    }

    static RawDataArray CreateNew(size_t size, double defaultValue){
       RawDataArray rd = RawDataArray::CreateNew(size);
       std::fill_n(rd.data, size, defaultValue);
       return rd;
    }

    static RawDataArray CreateCopy(double* data, size_t size) {
       RawDataArray rd = RawDataArray::CreateNew(size);
       std::copy(data, data + size, rd.data);
       return rd;
    } 

    RawDataArray(double* data, int size) :
        data(data),
        size(size)
    {}

    RawDataArray(const RawDataArray& other) :
        data(other.data),
        size(other.size)
    {}

    RawDataArray(RawDataArray&& other) noexcept :
        data(std::exchange(other.data, nullptr)),
        size(std::exchange(other.size, 0))
    {}

    RawDataArray& operator=(const RawDataArray& other){
        return *this = RawDataArray(other);
    }

    RawDataArray& operator=(RawDataArray&& other) noexcept {
        std::swap(data, other.data);
        std::swap(size, other.size);    
        return *this;
    }
}

Однако проблема заключается в управлении памятью. Если я перетасую класс-обертку, я не знаю, смогу ли я безопасно освободить данные.

Я думал использовать std::shared_ptr<double[]>, который доступен с C++17 для массивов. Есть ли другой способ или идиома, которую можно использовать?

По сути, вы пишете свой собственный умный указатель

В настоящее время в вашей реализации происходит утечка памяти, поскольку массив никогда не будет освобожден. Это безопасно, но неоптимально. Чтобы узнать, когда общий массив можно безопасно освободить, вам нужно будет отслеживать, сколько объектов совместно используют этот массив — вам нужен счетчик ссылок. Вы увеличиваете счетчик ссылок в конструкторе копирования и уменьшаете его в деструкторе. Если деструктор видит, что счетчик ссылок падает до нуля, общие данные удаляются.

Безусловно, самый простой способ сделать это — использовать std::shared_ptr<double[]> вместо необработанного указателя double*. Все будет Просто Работать™, и вам не придется думать о копирующих и перемещающих конструкторах или деструкторах (сравните правило нуля).

Могут быть веские причины не использовать shared_ptr. Например, shared_ptr должен использовать атомарный подсчет ссылок, чтобы быть потокобезопасным, даже если ваш код никогда не будет совместно использовать массив между потоками. Это может быть немного медленнее, в зависимости от платформы. А для shared_ptr обычно требуется два выделения: одно для контрольного блока со счетчиком ссылок и средством удаления, другое для фактических данных. Хотя shared_ptr это необходимо для поддержки эффективных слабых ссылок и из-за деталей стандарта C++, ваша реализация языка C++ может позволить вам использовать более компактное представление с использованием гибкого члена массива в стиле C. Тогда примерно:

struct Inner {
  size_t refcount;
  size_t capacity;
  double data[];  // !!! not ISO C++ !!!
};

struct SharedDataArray {
  Inner* inner;

  SharedDataArray() : inner(nullptr) {}

  explicit SharedDataArray(size_t capacity) : SharedDataArray() {
    if (!capacity) return;

    inner = reinterpret_cast<Inner*>(std::malloc(
      sizeof(Inner) + capacity * sizeof(double)
    ));
    if (!inner) throw std::bad_alloc();
    inner->refcount = 1;
    inner->capacity = capacity;
    std::fill(inner->data, inner->data + capacity, 0);
  }

  SharedDataArray(SharedDataArray& other) : inner(other.inner) {
    if (!inner) return;

    ++(inner->refcount);
  }

  ~SharedDataArray() {
    if (!inner) return;

    --(inner->refcount);
    if (inner->refcount > 0) return;
    std::free(inner);
    inner = nullptr; // not strictly necessary
  }
};

Но это сложно сделать правильно, и вам действительно стоит подумать об использовании вместо этого shared_ptr. Предпочтительно shared_ptr<vector<double>>, поскольку он управляет жизненным циклом массива за вас за счет дополнительной косвенности указателя.


LetsCodeIt, 9 апреля 2023 г., 15:38