Почему поведение undefined удаляет [] массив производных объектов с помощью базового указателя?
Я нашел следующий сниппет в стандарте С++ 03 в разделе 5.3.5 [expr.delete] p3
:
В первом альтернативе (объект удаления), если статический тип объекта, который нужно удалить, отличается от его динамического типа, статический тип должен быть базовым классом динамического типа операндов, а статический тип должен иметь виртуальный деструктор или поведение undefined. Во втором альтернативе (удалить массив), если динамический тип подлежащего удалению объекта отличается от его статического типа, поведение undefined.
Быстрый просмотр статических и динамических типов:
struct B{ virtual ~B(){} };
struct D : B{};
B* p = new D();
Статический тип p
равен B*
, тогда как динамический тип *p
равен D
, 1.3.7 [defns.dynamic.type]
:
[Пример: если указатель p
, статический тип которого является "указателем на class B
", указывает на объект class D
, полученный из B
, динамический тип выражения *p
D
. "]
Теперь, глядя на цитату вверху, это будет означать, что следующий код вызывает поведение undefined, если я получил это право, независимо от наличия деструктора virtual
:
struct B{ virtual ~B(){} };
struct D : B{};
B* p = new D[20];
delete [] p; // undefined behaviour here
Я неправильно понял формулировку в стандарте? Я что-то пропустил? Почему стандарт определяет это как поведение undefined?
Ответы
Ответ 1
Base* p = new Base[n]
создает массив n
-разделенных элементов Base
, из которых p
затем указывает на первый элемент. Тем не менее, Base* p = new Derived[n]
создает массив n
-разделенных элементов Derived
. p
затем указывает на подобъект Base
первого элемента. p
имеет не, но относится к первому элементу массива, что и требует действительное выражение delete[] p
.
Конечно, можно было бы поручить (а затем реализовать), что delete [] p
Does Right Thing ™ в этом случае. Но что это будет? Реализация должна была бы позаботиться о том, чтобы каким-то образом получить тип элемента массива, а затем морально dynamic_cast
p
к этому типу. Тогда это вопрос простой delete[]
, как мы уже это делаем.
Проблема заключается в том, что это будет необходимо каждый раз, когда массив типов полиморфных элементов, независимо от того, используется ли полиморфизм на нет. На мой взгляд, это не соответствует философии С++ о том, что вы не платите за то, что не используете. Но хуже: полиморфный delete[] p
просто бесполезен, потому что p
почти бесполезен в вашем вопросе. p
- указатель на подобъект элемента и не более; он иначе полностью не связан с массивом. Вы, конечно, не можете сделать с ним p[i]
(для i > 0
). Поэтому небезопасно, что delete[] p
не работает.
Подводя итог:
-
Массивы
-
уже имеют множество законных целей. Не позволяя массивам вести себя полиморфно (как в целом, так и только для delete[]
), это означает, что массивы с типом полиморфного элемента не наказываются за эти законные применения, что соответствует философии С++.
-
если, с другой стороны, необходим массив с полиморфным поведением, возможно реализовать его с помощью того, что у нас уже есть.
Ответ 2
Неправильно обрабатывать массив из производного как массив базы, а не только при удалении элементов. Например, даже просто доступ к элементам обычно вызывает катастрофу:
B *b = new D[10];
b[5].foo();
b[5]
будет использовать размер B
, чтобы вычислить, к какой ячейке памяти нужно получить доступ, и если B
и D
имеют разные размеры, это не приведет к предполагаемым результатам.
Точно так же, как std::vector<D>
не может быть преобразован в std::vector<B>
, указатель на D[]
не должен быть конвертирован в B*
, но по историческим причинам он компилируется в любом случае. Если вместо этого использовать std::vector
, это приведет к ошибке времени компиляции.
Это также объясняется в С++ FAQ Lite ответ в этом разделе.
Итак, delete
вызывает поведение undefined в этом случае, потому что уже неправильно обрабатывать массив таким образом, даже если система типов не может поймать ошибку.
Ответ 3
Чтобы добавить отличный ответ sth - я написал короткий пример, чтобы проиллюстрировать эту проблему различными смещениями.
Обратите внимание, что если вы закомментируете член m_c класса Derived, операция удаления будет работать хорошо.
Приветствия,
Гай.
#include <iostream>
using namespace std;
class Base
{
public:
Base(int a, int b)
: m_a(a)
, m_b(b)
{
cout << "Base::Base - setting m_a:" << m_a << " m_b:" << m_b << endl;
}
virtual ~Base()
{
cout << "Base::~Base" << endl;
}
protected:
int m_a;
int m_b;
};
class Derived : public Base
{
public:
Derived()
: Base(1, 2) , m_c(3)
{
}
virtual ~Derived()
{
cout << "Derived::Derived" << endl;
}
private:
int m_c;
};
int main(int argc, char** argv)
{
// create an array of Derived object and point them with a Base pointer
Base* pArr = new Derived [3];
// now go ahead and delete the array using the "usual" delete notation for an array
delete [] pArr;
return 0;
}
Ответ 4
Я думаю, что все сводится к принципу нулевой надбавки. т.е. язык не позволяет хранить информацию о динамическом типе элементов массива.
Ответ 5
IMHO это связано с ограничением массивов для работы с конструктором/деструктором. Обратите внимание, что при вызове new[]
компилятор вынуждает создавать экземпляр только конструктора по умолчанию. Точно так же, когда вызывается delete[]
, компилятор может искать только деструктор статического типа указателя вызова.
Теперь, в случае деструктора virtual
, сначала должен вызываться деструктор класса Derived, за которым следует класс Base. Поскольку для массива компилятор может видеть тип статического типа вызывающего объекта (здесь Base), он может в итоге вызвать только базовый деструктор; который является UB.
Сказав это, это не обязательно UB для всех компиляторов; например gcc вызывает деструктор в правильном порядке.