В чем концептуальное различие между окончательно и деструктором?

12

Во-первых, я хорошо понимаю, почему в C ++ нет конструкции «наконец»? но продолжительное обсуждение комментариев по другому вопросу, кажется, требует отдельного вопроса.

Помимо проблемы, finallyзаключающейся в том, что в C # и Java в принципе может существовать только один раз (== 1) на область действия, и в одной области может быть несколько (== n) деструкторов C ++, я думаю, что это по сути одно и то же. (С некоторыми техническими отличиями.)

Однако другой пользователь утверждал :

... Я пытался сказать, что dtor по своей сути является инструментом для (Release sematics) и, наконец, по сути, инструментом для (Commit семантики). Если вы не понимаете, почему: подумайте, почему допустимо бросать исключения друг на друга в блоках finally, и почему это не относится к деструкторам. (В некотором смысле, это данные против контроля. Деструкторы предназначены для освобождения данных, наконец, для освобождения контроля. Они разные; к сожалению, C ++ связывает их вместе.)

Может кто-нибудь прояснить это?

Мартин Ба
источник

Ответы:

6
  • Транзакция ( try)
  • Вывод ошибок / Ответ ( catch)
  • Внешняя ошибка ( throw)
  • Ошибка программиста ( assert)
  • Откат (самая близкая вещь может быть защитой границ в языках, которые поддерживают их изначально)
  • Выпуск ресурсов (деструкторы)
  • Независимый от транзакции поток управления ( finally)

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

Чего мне по сути не хватает, так это языковой функции, которая непосредственно представляет концепцию отката внешних побочных эффектов. Охрана области видимости в таких языках, как D, является самой близкой вещью, о которой я могу подумать, которая приближается к представлению этой концепции. С точки зрения потока управления, откат в области действия определенной функции должен будет отличать исключительный путь от обычного, одновременно неявно автоматизируя откат любых побочных эффектов, вызванных функцией в случае сбоя транзакции, но не тогда, когда транзакция завершается успешно. , Это достаточно легко сделать с деструкторами, если мы, скажем, установим логическое значение равным succeededtrue в конце нашего блока try, чтобы предотвратить откат логики в деструкторе. Но это довольно окольный способ сделать это.

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


источник
4

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

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

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

gbjbaanb
источник
4

С моей точки зрения, главное отличие состоит в том, что деструктор в c ++ является неявным механизмом (автоматически вызывается) для освобождения выделенных ресурсов, в то время как try ... finally может использоваться в качестве явного механизма для этого.

В программах на c ++ программист отвечает за освобождение выделенных ресурсов. Обычно это реализуется в деструкторе класса и выполняется сразу же, когда переменная выходит из области видимости или когда вызывается delete.

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

// c++
void test() {
    MyClass myClass(someParameter);
    // if there is an exception the destructor of MyClass is called automatically
    // this does not work with
    // MyClass* pMyClass = new MyClass(someParameter);

} // on test() exit the destructor of myClass is implicitly called

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

// c#
void test() {
    MyClass myClass = new MyClass(someParameter);
    // if there is an exception myClass is NOT destroyed so there may be memory/resource leakes

    myClass.destroy(); // this is never called
}

Не существует неявного механизма для этого, поэтому вы должны программировать это явно, используя try finally

// c#
void test() {
    MyClass myClass = null;

    try {
        myClass = new MyClass(someParameter);
        ...
    } finally {
        // explicit memory management
        // even if there is an exception myClass resources are freed
        myClass.destroy();
    }

    myClass.destroy(); // this is never called
}
k3b
источник
В C ++, почему деструктор вызывается автоматически только с объектом стека, а не с объектом кучи в случае исключения?
Джорджио
@Giorgio Потому что ресурсы кучи находятся в пространстве памяти, которое напрямую не связано со стеком вызовов. Например, представьте себе многопоточное приложение с двумя потоками Aи B. Если один поток выбрасывает, откат A'sтранзакции не должен уничтожать ресурсы, выделенные B, например, - состояния потока не зависят друг от друга, а постоянная память, живущая в куче, не зависит от обоих. Однако, как правило, в C ++ память кучи все еще привязана к объектам в стеке.
@Giorgio Например, std::vectorобъект может жить в стеке, но указывать на память в куче - и векторный объект (в стеке), и его содержимое (в куче) будут освобождены во время разматывания стека в этом случае, так как уничтожение вектора в стеке вызовет деструктор, который освобождает связанную память в куче (и аналогично уничтожает эти элементы кучи). Как правило, для обеспечения безопасности исключений большинство объектов C ++ живут в стеке, даже если они являются только ручками, указывающими на память в куче, автоматизируя процесс освобождения памяти кучи и стека при разматывании стека.
4

Рад, что вы опубликовали это как вопрос. :)

Я пытался сказать, что деструкторы и finallyконцептуально разные:

  • Деструкторы предназначены для высвобождения ресурсов ( данных )
  • finallyдля возврата звонящему ( контроль )

Рассмотрим, скажем, этот гипотетический псевдокод:

try {
    bar();
} finally {
    logfile.print("bar has exited...");
}

finallyздесь полностью решается проблема управления, а не проблема управления ресурсами.
Не имеет смысла делать это в деструкторе по разным причинам:

  • Нет вещь не быть «приобрела» или «создал»
  • Невозможность печати в файл журнала не приведет к утечке ресурсов, повреждению данных и т. Д. (При условии, что файл журнала здесь не возвращается в программу в другом месте)
  • Законно logfile.printтерпеть неудачу, тогда как разрушение (концептуально) не может потерпеть неудачу

Вот еще один пример, на этот раз как в Javascript:

var mo_document = document, mo;
function observe(mutations) {
    mo.disconnect();  // stop observing changes to prevent re-entrance
    try {
        /* modify stuff */
    } finally {
        mo.observe(mo_document);  // continue observing (conceptually, this can fail)
    }
}
mo = new MutationObserver(observe);
return observe();

В приведенном выше примере, опять же, нет ресурсов для освобождения.
На самом деле, finallyблок получение внутренних ресурсов для достижения своей цели, которая потенциально может потерпеть неудачу. Следовательно, не имеет смысла использовать деструктор (если он был у Javascript).

С другой стороны, в этом примере:

b = get_data();
try {
    a.write(b);
} finally {
    free(b);
}

finallyразрушает ресурс b. Это проблема с данными. Проблема не в том, чтобы вернуть контроль вызывающей стороне, а в том, чтобы избежать утечек ресурсов.
Неудача не вариант, и никогда не должно происходить (концептуально).
Каждый выпуск bобязательно сопряжен с приобретением, и имеет смысл использовать RAII.

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

user541686
источник
Благодарю. Я не согласен, но эй :-) Я думаю, что смогу добавить подробный ответ противоположного взгляда в ближайшие дни ...
Мартин Ба
2
Как факт, который finallyв основном используется для высвобождения ресурсов (не связанных с памятью), влияет на это?
Барт ван Инген Шенау
1
@BartvanIngenSchenau: я никогда не утверждал, что у любого языка, существующего в настоящее время, есть философия или реализация, которая соответствует тому, что я описал. Люди еще не закончили изобретать все, что могло бы существовать. Я только утверждал, что было бы полезно разделить эти два понятия, поскольку они представляют собой разные идеи и имеют разные варианты использования. Чтобы удовлетворить ваше любопытство, я верю, что у D есть и то, и другое. Возможно, есть и другие языки. Хотя я не считаю это уместным, и мне было все равно, почему, например, Java поддерживает finally.
user541686
1
Практическим примером, с которым я столкнулся в JavaScript, являются функции, которые временно изменяют указатель мыши на песочные часы во время некоторой длительной операции (которая может вызвать исключение), а затем возвращают его в нормальное состояние в finallyпредложении. Мировоззрение C ++ представило бы класс, который управляет этим «ресурсом» присваивания псевдоглобальной переменной. Какой концептуальный смысл это имеет? Но деструкторы - это молоток C ++ для требуемого выполнения кода конца блока.
dan04
1
@ dan04: Большое спасибо, это идеальный пример для этого. Я мог поклясться, что сталкивался с таким количеством ситуаций, когда RAII не имело смысла, но мне было так трудно думать о них.
user541686
1

Ответ K3B действительно хорошо формулирует это:

деструктор в c ++ - это неявный механизм (автоматически вызывается) для освобождения выделенных ресурсов, в то время как try ... finally может использоваться как явный механизм для этого.

Что касается «ресурсов», я хотел бы сослаться на Джона Калба: RAII должен означать «Ответственность за приобретение - это инициализация» .

В любом случае, что касается неявного и явного, это действительно так:

  • D'Tor - это инструмент для определения того, какие операции должны произойти - неявно - когда заканчивается время жизни объекта (что часто совпадает с окончанием области видимости)
  • Блок finally - это инструмент, который явно определяет, какие операции должны выполняться в конце области видимости.
  • Плюс, технически, вы всегда можете бросить в конце концов, но смотрите ниже.

Я думаю, что это концептуальная часть ...


... теперь есть ИМХО некоторые интересные детали:

Я также не думаю, что c'tor / d'tor нужно концептуально «приобретать» или «создавать» что-либо, кроме ответственности за запуск некоторого кода в деструкторе. Что, в конце концов, и делает: запустить некоторый код.

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

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

Мартин Ба
источник
Что вы думаете о моем примере Javascript?
user541686
Относительно ваших других аргументов: «Хотели бы мы регистрировать одно и то же независимо?» Да, это всего лишь пример, и вы как бы упускаете суть, и да, никто никогда не запрещал регистрировать более конкретные детали для каждого случая. Дело в том, что вы, конечно, не можете утверждать, что никогда не будет ситуации, в которой вы захотите записать что-то общее для обоих. Некоторые записи журнала являются общими, некоторые - конкретными; Вы хотите оба. И снова, вы как бы упускаете из виду смысл, сосредотачиваясь на регистрации. Мотивировать примеры из 10 строк сложно; пожалуйста, постарайтесь не упустить момент.
user541686
Вы никогда не обращались к этим ...
user541686
@ Mehrdad - я не рассмотрел ваш пример javascript, потому что мне потребовалась бы другая страница, чтобы обсудить, что я об этом думаю. (Я пытался, но мне потребовалось так много времени, чтобы сформулировать что-то связное, что я пропустил это :-)
Martin Ba
@ Mehrdad - что касается других ваших вопросов - похоже, мы должны согласиться не соглашаться. Я вижу, к чему вы стремитесь, с разницей, но я просто не уверен, что они являются чем-то концептуально иным: главным образом потому, что я в основном в лагере, который думает, что бросать из-за - действительно плохая идея ( примечание : я также подумайте в своем observerпримере, что бросать там было бы действительно плохой идеей.) Не стесняйтесь открывать чат, если вы хотите обсудить это дальше. Конечно, было весело думать о ваших аргументах. Приветствия.
Мартин Ба