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

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

16
задан Community 23 May 2017 в 10:29
поделиться

16 ответов

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

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

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

3
ответ дан 30 November 2019 в 16:24
поделиться

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

0
ответ дан 30 November 2019 в 16:24
поделиться

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

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

    int* x;
};

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

3
ответ дан 30 November 2019 в 16:24
поделиться

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

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

1
ответ дан 30 November 2019 в 16:24
поделиться

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

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


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

8
ответ дан 30 November 2019 в 16:24
поделиться

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

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

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

13
ответ дан 30 November 2019 в 16:24
поделиться

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

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

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

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

-1
ответ дан 30 November 2019 в 16:24
поделиться

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

0
ответ дан 30 November 2019 в 16:24
поделиться

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

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

0
ответ дан 30 November 2019 в 16:24
поделиться

Standard 12.4/14

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

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

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

2
ответ дан 30 November 2019 в 16:24
поделиться

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

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

.
1
ответ дан 30 November 2019 в 16:24
поделиться

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

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

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

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

-1
ответ дан 30 November 2019 в 16:24
поделиться

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

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

0
ответ дан 30 November 2019 в 16:24
поделиться

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

Ваш пример работает, но его сложно, что обычно работает

0
ответ дан 30 November 2019 в 16:24
поделиться

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

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

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

7
ответ дан 30 November 2019 в 16:24
поделиться

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

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

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

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

4
ответ дан 30 November 2019 в 16:24
поделиться
Другие вопросы по тегам:

Похожие вопросы: