Std:: unique_ptr <T []> с массивом производных объектов, использование удаленной функции

В моем коде для численной физики мне нужно создать массив объектов Derived с помощью unique_ptr, а их тип будет базовым. Обычно я бы:

// Header file of the Base class
class Particle{
public:
    Particle();             // some constructor
    virtual ~Particle();    // virtual destructor because of polymorphism
    virtual function();     // some random function for demonstration
};

// Header file of the Derived class
class Electron : public Particle{
public:
    Electron();
    // additional things, dynamic_cast<>s, whatever
};

Позже в моем коде, чтобы создать массив объектов Derived с указателем Base, я бы сделал

Particle* electrons = new Electron[count];

Преимущество состоит в том, что я могу использовать массив в очень удобном способе electrons[number].function(), потому что инкрементное значение в [] на самом деле является адресом памяти, указывающим на соответствующий экземпляр объекта Electron в массиве. Однако использование raw-указателей становится беспорядочным, поэтому я решил использовать интеллектуальные указатели.

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

std::unique_ptr<Particle, std::default_delete<Particle[]>> electrons(new Electron[count]);

который создает массив полиморфных электронов и использует даже правильный вызов delete[]. Проблема заключается в способе вызова конкретных объектов массива, поскольку я должен это сделать:

electrons.get()[number].function();

и мне не нравится часть get(), а не немного.

Я мог бы сделать следующее:

std::unique_ptr<Particle[]> particles(new Particle[count]);

и да, вызовите экземпляры типа Particle в массиве с помощью

particles[number].function();

и все будет хорошо и dandy, за исключением той части, что я не использую конкретные детали класса Electron, поэтому код бесполезен.

И теперь для забавной части, допустим еще одну вещь, мы будем?

std::unique_ptr<Particle[]> electrons(new Electron[count]);

BOOM!

use of deleted function ‘std::unique_ptr<_Tp [], _Dp>::unique_ptr(_Up*) [with _Up = Electron; <template-
 parameter-2-2> = void; _Tp = Particle; _Dp = std::default_delete<Particle []>]’

Что происходит?

Ответ 1

Вы можете использовать std::unique_ptr<std::unique_ptr<Particle>[]>, если хотите придерживаться своего текущего кода и в любом случае отслеживать счетчик.

Имейте в виду, что это не приведет вас к дополнительным косвенным действиям, хотя, если можно, использование std::vector<std::unique_ptr<Particle>> и, таким образом, включая длину и разумное использование reserve не должно быть медленнее.

Ответ 2

std::unique_ptr не позволяет стрелять себе в ногу, так как std::default_delete<T[]> вызывает delete[], который имеет поведение, указанное в стандартном

Если выражение удаления начинается с унарного:: оператора, имя функции перераспределения отображается в глобальном масштабе. В противном случае, если выражение удаления используется для освобождения объекта класса, статический тип имеет виртуальный деструктор, функция освобождения является один выбранный в точке определения динамических типов виртуальный деструктор (12.4). 117 В противном случае, если выражение delete используется для deallocate объект класса T или массив, статический и динамические типы объекта должны быть идентичными и освобождение имя функции просматривается в области T.

Другими словами, код выглядит следующим образом:

Base* p = new Derived[50];
delete[] p;

- поведение undefined.

Возможно, он работает над некоторыми реализациями - там вызов delete[] просматривает размер выделенного массива и вызывает деструкторы на элементах, что требует, чтобы элементы имели хорошо известный размер. Поскольку размер производных объектов может отличаться, арифметика указателя идет не так, а деструкторы вызываются с неправильным адресом.

Посмотрите, что вы пробовали:

std::unique_ptr<Particle[]> electrons(new Electron[count]);

существует код в конструкторе std::unique_ptr, который обнаруживает эти нарушения, см. cppreference.

std::unique_ptr<Particle, std::default_delete<Particle[]>> electrons(new Electron[count]);

- это поведение undefined, вы, по сути, говорите компилятору, что delete[] является допустимым способом освобождения ресурсов, которые вы нажимаете на конструктор electrons, что неверно, как упоминалось выше.

... но подождите, есть еще (бесценный комментарий от @T.C.):

Для сложения или вычитания, если выражения P или Q имеют тип "указатель на cv T", где T и тип элемента массива не похожи ([conv.qual]), поведение undefined. [Примечание. В частности, указатель на базовый класс не может использоваться для арифметики указателя, когда массив содержит объекты производного типа класса. - конечная нота]

Это означает, что не только удаление массива - это поведение undefined, но и индексация!

Base* p = new Derived[50]();
p[10].a_function(); // undefined behaviour

Что это значит для вас? Это означает, что вы не должны использовать массивы полиморфно.

Единственным безопасным способом с полиморфизмом является использование std::unique_ptr, указывающего на производные объекты, такие как std::vector<std::unique_ptr<Particle>> (у нас нет полиморфного использования массива там, но массивы с полиморфными объектами есть)

Поскольку вы упоминаете, что производительность критическая, то динамическое распределение каждого Particle будет медленным - в этом случае вы можете:

  • использовать пул объектов
  • использовать мухи
  • реорганизовать его, чтобы избежать наследования
  • используйте std::vector<Electron> или std::unique_ptr<Electron[]> напрямую.

Ответ 3

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

Например, Electron может иметь дополнительные данные, которые нет в Particle. Тогда размер объекта Electron больше не будет такого же размера, как объект Particle. Таким образом, арифметика указателя, которая необходима для доступа к элементам массива, больше не будет работать.

Эта проблема существует для необработанных указателей на массив, а также для unique_ptr для массива. Только сами объекты являются полиморфными. Если вы хотите использовать их без риска slicing, вам понадобится массив указателей на полиморфные объекты.

Если вы ищете дополнительные аргументы, объясняющие, почему этот дизайн следует избегать, вы можете взглянуть на раздел книги Скотта Мейерса "Более эффективный С++" под названием "Пункт 3: никогда не обрабатывать массивы полиморфно".

Альтернатива: измените дизайн

Например, для создания объектов используйте vector реального типа. И используйте вектор для полиморфного указателя Particle для использования этих объектов полиморфно:

vector<Electron>myelectrons(count);   // my real object store 
vector<Particle*>ve(count, nullptr);  // my adaptor for polymorphic access
transform(myelectrons.begin(), myelectrons.end(), ve.begin(), 
                [](Particle&e){return &e;} );  // use algorithm to populate easlily 
for (auto x: ve)  // make plain use of C++11 to forget about container type and size
   x->function(); 

Здесь живая демонстрация:

Ответ 4

Используйте массив std::vector или std:: (если вы знаете, сколько) из std:: unique_ptr. Что-то вроде этого:

#include <vector>
#include <memory>

class A
{
public:

    A() = default;
    virtual ~A() = default;
};

class B : public A
{
public:

    B() = default;
    virtual ~B() = default;
};

int main(void)
{
    auto v = std::vector<std::unique_ptr<A>>();

    v.push_back(std::make_unique<A>());
    v.push_back(std::make_unique<B>());

    return 0;
}

Изменить: с точки зрения скорости я быстро проверил эти 3 метода, и вот что я нашел:

Debug

6.59999430  : std::vector (with reserve, unique_ptr)
5.68793220  : std::array (unique_ptr)
4.85969770  : raw array (new())

Release

4.81274890  : std::vector (with reserve, unique_ptr)
4.42210580  : std::array (unique_ptr)
4.12522340  : raw array (new())

Наконец, я проверил, где я использовал new() для всех трех версий вместо unique_ptr:

4.13924640 : std::vector
4.14430030 : std::array
4.14081580 : raw array

Итак, вы видите, что в сборке релизов действительно нет разницы, при прочих равных условиях.

Ответ 5

Удивительно, но никто еще не предложил использовать полиморфный делетер. Исключением по умолчанию для unique_ptr является только это - значение по умолчанию. Вы можете изменить его, чтобы делать все, что хотите, включая до или вниз.

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

http://coliru.stacked-crooked.com/a/35bd4c3674d7df07

Я бы не советовал делать индексацию указателя с этим. Это все равно будет полностью нарушено.