Почему полиморфизм не применяется к массивам на С++?

#include <iostream>

using namespace std;

struct Base
{
    virtual ~Base()
    {
        cout << "~Base(): " << b << endl;
    }

    int b = 1;
};

struct Derived : Base
{
    ~Derived() override
    {
        cout << "~Derived(): " << d << endl;
    }

    int d = 2;
};

int main()
{
    Base* p = new Derived[4];
    delete[] p;
}

Вывод выглядит следующим образом: (Visual Studio 2015 с Clang 3.8)

~Base(): 1
~Base(): 2
~Base(): -2071674928
~Base(): 1

Почему полиморфизм не применяется к массивам в С++?

Ответы

Ответ 1

Вы получаете поведение undefined, потому что оператор delete[] понятия не имеет, какой объект хранится в массиве, поэтому он доверяет статическому типу, чтобы принять решение о смещениях отдельных объектов. В стандарте говорится следующее:

Во втором альтернативе (удалить массив), если динамический тип подлежащего удалению объекта отличается от его статического типа, поведение undefined.

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

Derived* p = new Derived[4]; // Obviously, this works

Этот Q & A более подробно описывает причину, по которой стандарт имеет это требование.

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

Ответ 2

Учитывая,

Base* p = Derived[4];

стандарт С++ 11 делает

delete [] p;

будет undefined.

5.3.5 Удаление

...

2... Во втором альтернативе (удалить массив), если динамический тип подлежащего удалению объекта отличается от его статического типа, поведение undefined.

С точки зрения макета памяти также имеет смысл, почему delete [] p; приведет к поведению undefined.

Если sizeof(Derived) - N, new Derived[4] выделяет память, которая будет выглядеть примерно так:

+--------+--------+--------+--------+
|   N    |   N    |   N    |   N    |
+--------+--------+--------+--------+

В общем случае sizeof(Base) <= sizeof(Derived). В вашем случае sizeof(Base) < sizeof(Derived), поскольку Derived имеет дополнительную переменную-член.

При использовании:

Base* p = new Derived[4];

у вас есть:

p
|
V
+--------+--------+--------+--------+
|   N    |   N    |   N    |   N    |
+--------+--------+--------+--------+

p+1 указывает на какое-то место в середине первого объекта, так как sizeof(Base) < sizeof(Derived).

       p+1
       |
       V
+--------+--------+--------+--------+
|   N    |   N    |   N    |   N    |
+--------+--------+--------+--------+

Когда деструктор вызывается на p+1, указатель не указывает на начало объекта. Следовательно, программа проявляет симптомы поведения undefined.


Связанная проблема

Из-за различий в размерах Base и Derived вы не можете перебирать элементы динамически распределенного массива с помощью p.

for ( int i = 0; i < 4; ++i )
{
   // Do something with p[i]
   // will not work since p+i does not necessary point to an object
   // boundary.
}

Ответ 3

Это связано с концепциями ковариация и контравариантность. Я приведу пример, который, надеюсь, прояснит ситуацию.

Начнем с простой иерархии. Предположим, мы имеем дело с созданием и уничтожением объектов в реальном мире. У нас есть 3D-объекты (например, деревянные блоки) и 2D-объекты (например, листы бумаги). 2D-объект можно рассматривать как трехмерный объект, за исключением незначительной высоты. Это правильное направление подкласса, поскольку обратное неверно; 2D-объекты могут быть уложены плоскими друг на друга, что очень сложно в лучшем случае сделать с любыми 3D-объектами.

Что-то, что создает объекты, такие как принтер, ковариант. Предположим, ваш друг хочет заимствовать 3D-принтер, взять десять объектов, которые он производит, и вставить их в коробку. Вы можете дать им 2D-принтер; он будет печатать десять страниц, а ваш друг будет вставлять их в коробку. Однако, если этот друг захотел взять десять 2D-объектов и вставить их в папку, вы не можете дать им 3D-принтер.

Что-то, что потребляет объекты, например, измельчитель, контравариантно. У вашего друга есть папка со страницами, которые она печатала, но она не продавалась, поэтому он хочет ее испортить. Вы можете дать им 3D-измельчитель, например, промышленные, используемые для измельчения таких вещей, как автомобили, и он будет работать нормально; кормление страниц не вызывает никаких трудностей. С другой стороны, если бы он хотел уничтожить коробку предметов, которые он получил раньше, вы не могли бы дать ему 2D-измельчитель, поскольку объекты могут не помещаться в слот.

Массивы инвариант; они оба потребляют объекты (с назначением) и производят объекты (с доступом к массиву). В качестве примера такого отношения возьмите какой-нибудь факсимильный аппарат. Если вашему другу нужен факсимильный аппарат, ему нужен точный вид, который они запрашивают; они не могут иметь супер- или подкласс, поскольку вы не можете вставлять 3D-объекты в слот для 2D-бумаги, и вы не можете привязывать объекты, созданные 3D-факсимильным аппаратом, в книгу.

Ответ 4

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

Полиморфизм может работать только, когда мы говорим о соотношении класса и подкласса. Поэтому, когда у нас есть ситуация, так как вы закодированы, мы имеем:

введите описание изображения здесь

Мы говорим: "Все экземпляры Derived также являются экземплярами Base". Обратите внимание, что этот должен удерживаться или мы даже не можем говорить о полиморфизме. В этом случае он выполняется и, следовательно, мы можем использовать указатель на Derived, где код ожидает указатель на Base.

Но тогда вы пытаетесь сделать что-то другое:

введите описание изображения здесь

И здесь у нас есть проблема. Хотя в теории множеств можно сказать, что набор подкласса также является набором суперкласса, в программировании это неверно. Проблема несколько возрастает из-за того, что разница - "всего два символа". Но массив элементов в некотором роде совершенно другой, чем любой из этих элементов.

Возможно, если вы переработали код с использованием шаблона std::array, который станет более понятным:

#include <iostream>
#include <array>

struct Base
{   
    virtual ~Base()
    {
        std::cout << "~Base()" << std::endl;
    }
};

struct Derived : Base
{   
    ~Derived() override
    {
        std::cout << "~Derived()" << std::endl;
    }
};

int main()
{
    std::array<Base, 4>* p = new std::array<Derived, 4>;
    delete[] p;
}

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

Надеюсь, это поможет некоторым людям, которые хотят более интуитивно понятное понимание того, что происходит, а не техническое объяснение.