Почему это неопределенное поведение, чтобы удалить [] массив производных объектов через базовый указатель? - PullRequest
34 голосов
/ 30 мая 2011

Я нашел следующий фрагмент в стандарте C ++ 03 под 5.3.5 [expr.delete] p3:

В первом альтернативном варианте ( удалить объект ), если статический тип удаляемого объекта отличается от его динамического типа, статический тип должен быть базовым классом динамического типа операнда и Статический тип должен иметь виртуальный деструктор или поведение не определено. Во втором альтернативном варианте ( delete array ), если динамический тип удаляемого объекта отличается от его статического типа, поведение не определено.


Быстрый обзор статических и динамических типов:

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.»]


Теперь, если снова взглянуть на цитату сверху, это будет означать, что следующий код вызывает неопределенное поведение, если я правильно понял, независимо от наличия деструктора virtual:

struct B{ virtual ~B(){} };
struct D : B{};

B* p = new D[20];
delete [] p; // undefined behaviour here

Я как-то неправильно понял формулировку в стандарте? Я что-то упустил? Почему стандарт определяет это как неопределенное поведение?

Ответы [ 5 ]

32 голосов
/ 30 мая 2011

Base* p = new Base[n] создает массив размером n из Base элементов, из которых p затем указывает на первый элемент.Однако Base* p = new Derived[n] создает массив размером n с Derived элементами.p затем указывает на подобъект Base первого элемента .p не не однако ссылается на первый элемент массива, что и требуется для правильного выражения delete[] p.

Конечно, это было бы возможнопоручить (и затем реализовать), что delete [] p делает правильные вещи ™ в этом случае.Но что это займет?Реализация должна позаботиться о том, чтобы каким-то образом получить тип элемента массива, а затем морально dynamic_cast p к этому типу.Тогда нужно просто сделать delete[], как мы уже это делаем.

Проблема в том, что для этого потребуется каждый раз массив типа полиморфных элементов, независимо от того,полиморфизм используется на не.На мой взгляд, это не соответствует философии C ++ - не платить за то, что вы не используете.Но что еще хуже: delete[] p с поддержкой полиморфизма просто бесполезен, потому что p почти бесполезен в вашем вопросе.p - указатель на подобъект элемента и не более;в остальном он совершенно не связан с массивом.Вы, конечно, не можете сделать p[i] (для i > 0) с ним.Поэтому не исключено, что delete[] p не работает.

Подводя итог:

  • массивы уже имеют множество законных применений.Не позволяя массивам вести себя полиморфно (либо в целом, либо только для delete[]), это означает, что массивы с полиморфным типом элемента не наказываются за это законное использование, что соответствует философии C ++.

  • если, с другой стороны, нужен массив с полиморфным поведением, его можно реализовать с точки зрения того, что у нас уже есть.

12 голосов
/ 30 мая 2011

Неправильно относиться к производному массиву как к базовому массиву, а не только при удалении элементов. Например, даже простой доступ к элементам обычно приводит к катастрофе:

B *b = new D[10];
b[5].foo();

b[5] будет использовать размер B, чтобы вычислить, к какой ячейке памяти обращаться, и если B и D имеют разные размеры, это не приведет к ожидаемым результатам.

Так же, как std::vector<D> не может быть преобразован в std::vector<B>, указатель на D[] не должен быть конвертируемым в B*, но по историческим причинам он компилируется в любом случае. Если вместо этого будет использоваться std::vector, это приведет к ошибке времени компиляции.

Это также объясняется в ответе C ++ FAQ Lite по этой теме .

Так что delete вызывает неопределенное поведение в этом случае, потому что уже неправильно обрабатывать массив таким образом, даже если система типов не может перехватить ошибку.

1 голос
/ 16 октября 2017

Просто добавьте к превосходному ответу sth - я написал короткий пример, чтобы проиллюстрировать эту проблему с различными смещениями.

Обратите внимание, что если вы закомментируете член m_c класса Derived, операция удаления будет работать хорошо.

Приветствия

Guy.

#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;
}
0 голосов
/ 30 мая 2011

ИМХО, это связано с ограничением массивов для работы с конструктором / деструктором .Обратите внимание, что когда вызывается new[], компилятор заставляет создавать только конструктор по умолчанию .Таким же образом, когда вызывается delete[], компилятор может искать только деструктор вызова статического типа указателя.

Теперь в случае деструктора virtual деструктор производного класса долженбыть вызванным первым, а затем базовым классом.Поскольку для массивов компилятор может видеть статический тип вызывающего типа объекта (здесь Base), он может в конечном итоге вызвать только деструктор Base;это UB.

Сказав это, это не обязательно UB для всех компиляторов;скажем например, gcc вызывает деструктор в правильном порядке.

0 голосов
/ 30 мая 2011

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

...