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

как говорится в ответ просто вызов деструктора во второй раз уже является неопределенным поведением 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 C++ 9 приведенный выше код выглядит работающим. Даже определение UB на C++ напрямую не запрещает работать вещам, квалифицируемым как UB. Поэтому для кода выше, чтобы сломать некоторую реализацию и / или специфику платформы, необходимы.

почему именно вышеуказанный код сломается и при каких условиях?

16 ответов


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

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

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


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

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

Так почему же опять это не привести к неопределенному поведению?


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

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


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


объект уже нет после вызова деструктора.

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

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


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

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

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

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


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

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

    int* x;
};

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


стандарт 12.4 / 14

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

Я думаю, что этот раздел относится к вызову деструктора через удалить. Другими словами: суть этого абзаца заключается в том, что "удаление объекта дважды является неопределенным поведением". Вот почему ваш пример кода отлично работает.

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


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

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


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

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


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

ваш пример работает, но его трудно, что работает в целом


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

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


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


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

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


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


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

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

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

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


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

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

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

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