Что делают Clang и GCC, когда `delete`ing базовые классы с не виртуальными деструкторами?

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

.... но это на самом деле не очень полезно; зная, что каждый компилятор может вести себя по-другому, не говорит нам ничего о поведении какого-либо конкретного компилятора. Итак, что делают Clang и g++ в этом случае? Я бы предположил, что они просто вызовут деструктор базового класса, а затем освободят память (для всего производного класса). Это тот случай?

Или, если это невозможно определить для всех версий GCC и Clang, как насчет GCC 4.9 и 5.1 и Clang 3.5 до 3.7?

Ответ 1

Во-первых, стандартный отказ от ответственности: это поведение undefined, поэтому даже с одним конкретным компилятором изменение флагов компилятора, дня недели или способа просмотра компьютера может изменить поведение.

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

В простом случае (одиночное наследование) вы обычно получаете что-то примерно эквивалентное статической привязке, то есть если вы уничтожаете производный объект с помощью указателя на базовый объект, вызывается только базовый конструктор, поэтому объект isn ' t уничтожен должным образом.

Если вы используете множественное наследование и уничтожаете объект производного класса через "первый" базовый класс, он обычно будет примерно таким же, как если бы вы использовали одиночное наследование - будет вызван деструктор базового класса, но деструктор производного класса не будет.

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

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

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

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

С не виртуальным деструктором, что обычно произойдет, так это то, что код будет в основном принимать этот адрес этого первого объекта базового класса, примерно примерно эквивалент reinterpret_cast на нем и попытаться использовать эту память как если это был объект базового класса, указанный указателем (например, base2). Например, пусть предположим, что base2 имеет указатель со смещением 14, а dest2-объект base2 пытается удалить блок памяти, на который он указывает. С не виртуальным деструктором он, вероятно, получит указатель на объект base1, но он все равно будет смотреть на смещение 14 и попытаться рассматривать это как указатель и передать его в delete. Возможно, что base1 содержит указатель на это смещение и фактически указывает на некоторую динамически распределенную память, и в этом случае это может показаться успешным. Опять же, также может быть, что это нечто совсем другое, и программа умирает с сообщением об ошибке (например), пытающимся освободить недействительный указатель.

Также возможно, что base1 меньше размера 14 байтов, поэтому это фактически приводит к смещению (скажем) смещения 4 в base2.

Итог: для случая, подобного этому, все становится очень уродливым в спешке. Самое лучшее, на что вы можете надеяться, это то, что программа быстро и громко умирает.

Просто для пинков, быстрый демонстрационный код:

#include <iostream>
#include <string>
#include <vector>

class base{ 
    char *data;
    std::string s;
    std::vector<int> v;
public:
    base() { data = new char;  v.push_back(1); s.push_back('a'); }
    ~base() { std::cout << "~base\n"; delete data; }
};

class base2 {
    char *data2;
public:
    base2() : data2(new char) {}
    ~base2() { std::cout << "~base2\n"; delete data2; }
};

class derived : public base, public base2 { 
    char *more_data;

public:
    derived() : more_data(new char) {}
    ~derived() { std::cout << "~derived\n"; delete more_data; }
};

int main() {
    base2 *b = new derived;
    delete b;
}

g++/Linux: ошибка сегментации
clang/Linux: ошибка сегментации
VС++/Windows: всплывающее окно: "foo.exe перестает работать" "Проблема привела к тому, что программа перестала работать правильно. Закройте программу."

Если мы изменим указатель на base вместо base2, мы получим ~base от всех компиляторов (и если мы получим только один базовый класс и будем использовать указатель на этот базовый класс, мы получим то же: работает только деструктор базового класса.

Ответ 2

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

Если вы не используете основной базовый класс для удаления объекта, это не так, поэтому компилятор вызовет operator delete с неправильным адресом.

Конечно, компилятор не будет вызывать деструктор производного класса или operator delete производного класса (если он есть).