Подтвердить что ты не робот

Почему поведение 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?

4b9b3361

Ответ 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 вызывает деструктор в правильном порядке.