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

Почему именно вызывает деструктор во второй раз undefined поведение в С++?

Как уже упоминалось в этом ответе просто вызов деструктора во второй раз уже есть undefined поведение 12.4/14 (3.8).

Например:

class Class {
public:
    ~Class() {}
};
// somewhere in code:
{
    Class* object = new Class();
    object->~Class();
    delete object; // UB because at this point the destructor call is attempted again
}

В этом примере класс сконструирован таким образом, что деструктор можно было бы вызвать несколько раз - такие вещи, как двойное удаление, не могут произойти. Память по-прежнему распределяется в точке, где вызывается delete - первый вызов деструктора не вызывает ::operator delete() для освобождения памяти.

Например, в Visual С++ 9 приведенный выше код выглядит работоспособным. Даже С++-определение UB напрямую не запрещает работать, как UB. Таким образом, для вышеприведенного кода необходимо выполнить некоторые требования к реализации и/или платформе.

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

4b9b3361

Ответ 1

Деструкторы не являются регулярными функциями. Вызов одного не вызывает одну функцию, он вызывает много функций. Это волшебство деструкторов. Хотя вы предоставили тривиальный деструктор с единственной целью сделать его трудно показать, как он может сломаться, вы не смогли продемонстрировать, что делают другие функции, которые вызываются. И стандарт тоже. Его в тех функциях, которые вещи могут потенциально развалиться.

Как тривиальный пример, скажем, компилятор вставляет код для отслеживания времени жизни объектов для целей отладки. Конструктор [который также является волшебной функцией, которая делает всевозможные вещи, о которых вы не просили) хранит некоторые данные где-то, где говорится "Вот я". Перед вызовом деструктора он изменяет эти данные, чтобы сказать "Там я иду". После вызова деструктора он избавляется от информации, которую он использовал для поиска этих данных. Поэтому в следующий раз, когда вы вызовете деструктор, вы получите нарушение доступа.

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

Ответ 2

Я думаю, что ваш вопрос направлен на обоснование стандарта. Подумайте об этом по-другому:

  • Определение поведения вызова деструктора дважды создает работу, возможно, большую работу.
  • В вашем примере показано, что в некоторых тривиальных случаях не будет проблемой дважды вызвать деструктор. Это правда, но не очень интересно.
  • Вы не дали убедительного варианта использования (и я сомневаюсь, что вы можете) при вызове деструктора дважды - это хорошая идея/упрощает код/​​делает язык более мощным/очищает семантику/или что-то еще.

Итак, почему снова этот не вызывает поведение undefined?

Ответ 3

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

С другой стороны, для этого поведения theres нет выгоды. На практике вы не можете извлечь из этого выгоду, потому что вы вообще не знаете, подходит ли деструктор класса к вышеуказанным критериям или нет. Никакой код общего назначения не может положиться на это. Было бы очень легко ввести ошибки таким образом. И, наконец, как это помогает? Это просто позволяет писать неаккуратный код, который не отслеживает срок службы его объектов - недописанный код, другими словами. Почему стандарт должен поддерживать это?


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

Ответ 4

Объект больше не существует после вызова деструктора.

Итак, если вы его еще раз вызовете, вы вызываете метод для объекта , который не существует.

Почему это когда-либо будет определено поведение? Компилятор может обнулить память объекта, который был разрушен, для отладки/защиты/по какой-либо причине или переработать свою память с другим объектом в качестве оптимизации или что-то еще. Реализация может по своему усмотрению. Вызов деструктора снова по существу вызывает метод для произвольной необработанной памяти - Bad Idea (tm).

Ответ 5

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

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

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

Достаточно легко объявить правильно названные функции для использования вместо того, чтобы злоупотреблять конструктором и деструктором. Объектно-ориентированный прямой C по-прежнему возможен на С++ и может быть правильным инструментом для какого-то задания... в любом случае деструктор не является правильной конструкцией для каждой задачи, связанной с уничтожением.

Ответ 6

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

class Class {
public:
    Class()
    {
        x = new int;
    }
    ~Class() 
    {
        delete x;
        x = (int*)0xbaadf00d;
    }

    int* x;
};

Я могу представить себе реализацию, когда она сработает с тривиальным деструктором. Например, такая реализация может удалить уничтоженные объекты из физической памяти, и любой доступ к ним приведет к некоторому сбою оборудования. Похоже, что Visual С++ не является одним из таких реализаций, но кто знает.

Ответ 7

Стандарт 12.4/14

Как только деструктор вызывается для объект, объект больше не существует; поведение undefined, если деструктор вызывается для объекта чье время жизни закончилось (3.8).

Я думаю, что этот раздел относится к вызову деструктора с помощью delete. Другими словами: Суть этого параграфа заключается в том, что "удаление объекта дважды - это поведение undefined". Поэтому, почему ваш пример кода работает нормально.

Тем не менее, этот вопрос довольно академичен. Деструкторы предназначены для вызова через delete (кроме исключения объектов, выделенных с помощью размещения-new, которые правильно соблюдаются). Если вы хотите разделить код между деструктором и второй функцией, просто извлеките код в отдельную функцию и вызовите это из вашего деструктора.

Ответ 8

Это undefined, потому что если бы это было не так, каждая реализация должна была бы закладок через некоторые метаданные, будет ли объект еще жив или нет. Вы должны будете заплатить эту стоимость за каждый отдельный объект, который противоречит основным правилам проектирования С++.

Ответ 9

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

Чтобы ваш код "работал" так, как вы намеревались, эта реализация для отладки должна была бы использовать специальный дескриптор do-nothing и пропустить установку этого флага. То есть, это должно было бы предположить, что вы намеренно уничтожаете дважды, потому что (вы думаете) деструктор ничего не делает, в отличие от предположения, что вы случайно уничтожили дважды, но не смогли обнаружить ошибку, потому что деструктор ничего не делает, Либо вы небрежны, либо являетесь повстанцем, и там больше пробега в реализациях отладки, помогающих людям, которые небрежны, чем в потворстве повстанцам; -)

Ответ 10

Один важный пример реализации, который может сломаться:

Соответствующая реализация на С++ может поддерживать сбор мусора. Это была давняя цель дизайна. GC может предположить, что объект может быть GC'ed немедленно, когда его dtor запущен. Таким образом, каждый вызов dtor будет обновлять свою внутреннюю учетную запись GC. Во второй раз, когда dtor вызывается для одного и того же указателя, структуры данных GC могут очень сильно испортиться.

Ответ 11

По определению деструктор "уничтожает" объект и уничтожает объект дважды, не имеет смысла.

Ваш пример работает, но его трудный, который обычно работает

Ответ 12

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

Что касается того, где ваш код мог break;, вы могли бы найти, что ваш код разбивается на отладочные сборки на некоторых компиляторах; многие компиляторы рассматривают UB как "делают то, что не влияет на производительность для хорошо определенного поведения" в режиме деблокирования, и "вставляют проверки для обнаружения плохого поведения" в отладочных сборках.

Ответ 13

В принципе, как уже указывалось, вызов деструктора второй раз не удастся для любого деструктора класса, который выполняет работу.

Ответ 14

Причина в том, что ваш класс может быть, например, посчитанным с помощью умного указателя. Таким образом, деструктор уменьшает счетчик ссылок. Как только этот счетчик достигнет 0, фактический объект должен быть очищен.

Но если вы дважды вызовете деструктор, то счет будет испорчен.

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

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

Ответ 15

Это поведение undefined, потому что стандарт дал понять, для чего используется деструктор, и не решил, что произойдет, если вы используете его неправильно. Поведение undefined не обязательно означает "crashy smashy", это просто означает, что стандарт не определил его, поэтому он остался до реализации.

В то время как я не слишком свободно владею С++, моя кишка говорит мне, что реализация приветствуется либо рассматривать деструктор как просто другую функцию-член, либо фактически уничтожать объект при вызове деструктора. Таким образом, это может сломаться в некоторых реализациях, но, возможно, это не будет в других. Кто знает, это undefined (обратите внимание на то, что демоны вылетают из носа, если вы попытаетесь).

Ответ 16

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

Множество концепций: RAII, интеллектуальные указатели и просто общее распределение/освобождение памяти зависят от этого правила. Количество раз, которое деструктор будет называться (один), является для них существенным. Поэтому документация для таких вещей обычно promises: "Используйте наши классы в соответствии с правилами языка С++, и они будут работать правильно!"

Если бы не было такого правила, оно указывало бы как "Использовать наши классы в соответствии с правилами lanugage С++, и да, не называйте его деструктор дважды, тогда они будут работать правильно". Множество спецификаций будет звучать именно так. Эта концепция слишком важна для языка, чтобы пропустить его в стандартном документе.

Вот почему. Ничего не связано с двоичными внутренними элементами (которые описаны в ответе Potatoswatter).