Почему именно второй раз вызывает деструктор неопределенное поведение в C ++? - PullRequest
16 голосов
/ 05 мая 2010

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

Например:

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 ]

14 голосов
/ 05 мая 2010

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

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

Так почему же это не вызывает неопределенное поведение?

8 голосов
/ 05 мая 2010

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

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


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

7 голосов
/ 05 мая 2010

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

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

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

4 голосов
/ 05 мая 2010

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

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

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

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

3 голосов
/ 05 мая 2010

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

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

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

3 голосов
/ 05 мая 2010

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

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

    int* x;
};

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

2 голосов
/ 05 мая 2010

Стандарт 12.4 / 14

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

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

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

1 голос
/ 06 мая 2010

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

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

1 голос
/ 05 мая 2010

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

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

0 голосов
/ 05 мая 2010

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...