ItsNear's blog

By ItsNear, 7 years ago, In Russian,

Давече на ВКонтакте Николай Дуров в одном из обсуждений посоветовал книжку Inside C++ Object Model.

Я ее купил, и, читая ее, неоднократно прозрел что я нуб в С++. Каждый день показывал знакомым примеры из книги и спрашивал, как они думают, как поведет себя компилятор, чтобы убедиться, что я такой нуб не один.

Вот сегодня я показал такой пример:

#include <stdio.h>

class Point
{
public:
    int a;
    Point() : a(5) { };
    virtual ~Point() { printf("%d\n", a); }
};

class Point3D : public Point
{
public:
    int b;
    Point3D() : b(7) { };
    virtual ~Point3D() { printf("M\n"); }
};

int main()
{
    Point* p = new Point3D[5];
    delete [] p;
    return 0;
}

И спросил их, что они ожидают увидеть.

Потратьте 5 минут чтобы предложить свой вариант ответа.

На самом деле для меня (и для тех кому я задал вопрос) кажется очевидным, что 5 раз выведется M5 (сначала вызовется виртуальный деструктор Point3D, затем виртуальный деструктор Point).

Согласно книге это не так. delete[] ничего не знает о полиморфизме, и вызовет деструктор Point. Более того, delete[] ничего не знает о том, что реальный размер объектов в массиве больше размера Point, а потому удалив первый элемент он сдвинется только на размер Point, а не на размер Point3D, тем самым вызвав второй декструктор на совершенно неверном участке памяти. Ребята мне не поверили О.О

Мы запустили код, и он вывел M5 пять раз.

Как же так, книга врет? И в тоже время -- я могу поверить, что компилятор догадался взять деструктор из виртуальной таблицы, но откуда он узнал реальный размер объекта?

Не веря в происходящее, я установил Clang, и собрал этот код clang'ом. Код вывел пять пятерок. Кажется, что в этом коде вывести пять пятерок просто невозможно -- это значит, что компилятор не догадался вызывать деструктор из виртуальной таблицы, но догадался сдвигаться на правильный размер?

Я думаю, что все опытные С++ гуру уже поняли что здесь происходит.

Проблема в том, что код запускался на 64-ех битной машине, и компилятор догадался поместить a и b в один восьмибайтный блок, таким образом размер класса Point и Point3D был одинаковый (16 байт -- еще 8 на указатель на виртуальную таблицу).

Теперь давайте поменяем int в определении обоих классов на long long. Каково будет поведение? Опять же, потратьте пять минут, чтобы придумать свой ответ, прежде чем читать дальше.

Чуда не происходит. Коду неоткуда узнать реальный размер объектов. Оба компилятора продолжают вести себя так же как и раньше (g++ вызывает виртуальный деструктор, а clang вызывает деструктор у Point), но только теперь размер Point не равен размеру Point3D. Таким образом clang выводит мусор в виде пятерок, семерок, и численного представления указателя на виртуальную таблицу, а g++ просто сразу падает по Segmentation Fault, не успев вывести ничего. Это странно, потому что виртуальная таблица лежит в самом начале класса, и не понятно, что именно не позволяет вызвать виртуальный декструктор хотя бы для первого элемента.

В любом случае, мораль простая -- массивы не работают с полиморфизмом, и, видимо, приведенный код -- это undefined behavior. Интересно, как этот же код поведет себя в Visual Studio и MigGW?

Вот код для запуска с отладочным выводом:

#include <stdio.h>

class Point
{
public:
    long long a;
    Point() : a(5) { };
    virtual ~Point() { printf("%lld\n", a); }
};

class Point3D : public Point
{
public:
    long long b;
    Point3D() : b(7) { };
    virtual ~Point3D() { printf("M\n"); }
};

int main()
{
    printf("%d\n", (int)sizeof(Point));
    printf("%d\n", (int)sizeof(Point3D));
    // это поможет понять где лежит указатель на виртуальную таблицу
    printf("%llu\n", &Point::a);
    printf("%llu\n", &Point3D::a);
    printf("%llu\n", &Point3D::b);

    Point* p = new Point3D[5];
    delete [] p;
    return 0;
}

Вывод для G++:

16
24
8
8
16
Segmentation fault

Вывод для clang:

16
24
8
8
16
4197968
5
7
4197968
5
 
 
 
 
  • Vote: I like it  
  • +74
  • Vote: I do not like it