最終的にデストラクタと概念的な違いは何ですか?


12

まず、C ++に「最終的に」コンストラクトがない理由をよく知っていますか?しかし、別の質問に関する長期にわたるコメントの議論は、別の質問を正当化するようです。

それを除けば問題から、finallyC#やJavaで基本的に一度だけ存在することができます(== 1)の範囲および単一のスコープあたりが(== n)を複数持つことができますC ++デストラクタ、私は、彼らは基本的に同じものだと思います。(技術的な違いがいくつかあります。)

しかし、別のユーザー次のように主張しました

...私は、dtorは本質的に(リリースセマティクス)のツールであり、最終的には本質的に(コミットセマンティクス)のツールであると言っていました。理由がわからない場合は、finallyブロックで例外を互いの上にスローするのが正当である理由と、デストラクタが例外でない理由を検討してください。(ある意味、それはデータとコントロールの関係です。デストラクタはデータを解放するためのものであり、最終的にはコントロールを解放するためのものです。それらは異なります。C++がそれらを結び付けるのは残念です。)

誰かがこれを解決できますか?

回答:


6
  • トランザクション(try
  • エラー出力/応答(catch
  • 外部エラー(throw
  • プログラマーエラー(assert
  • ロールバック(最も近いものは、ネイティブにサポートする言語のスコープガードかもしれません)
  • リソースの解放(デストラクタ)
  • その他のトランザクションに依存しない制御フロー(finally

finallyその他のトランザクションに依存しない制御フローよりも良い説明を思い付くことができません。特にデストラクタとの両方を含む理論的言語では、トランザクションとエラー回復の考え方のコンテキストで、必ずしも高レベルの概念に直接マップするわけではありませんfinally

私に最も本質的に欠けているのは、外部副作用をロールバックする概念を直接表す言語機能です。Dのような言語のスコープガードは、その概念を表現することに近い、私が考えることができる最も近いものです。制御フローの観点から、特定の関数のスコープでのロールバックでは、例外的なパスを通常のパスと区別する必要があります。同時に、トランザクションが失敗した場合に関数によって引き起こされる副作用の暗黙的なロールバックを自動化する必要がありますが、トランザクションが成功したときではありません。たとえば、succeededデストラクタでのロールバックロジックを防ぐために、tryブロックの最後にブール値をtrueのような値に設定すると、デストラクタで簡単に実行できます。しかし、これはこれを行うためのかなり遠回りの方法です。

それはそれほど節約しないように思えるかもしれませんが、副作用の逆転は正しいことを行うのが最も難しいことの1つです(例:例外に対して安全なジェネリックコンテナを書くことを非常に難しくするもの)。


4

ある意味では、フェラーリとトランジットの両方を使用して、さまざまな用途向けに設計されているにもかかわらず、1パイントのミルクを求めて店を挟むことができます。

try / finallyコンストラクトをすべてのスコープに配置し、finallyブロック内のすべてのスコープ定義変数をクリーンアップして、C ++デストラクタをエミュレートできます。これは、概念的にはC ++が行うことです。変数がスコープから外れると(つまり、スコープブロックの最後に)、コンパイラは自動的にデストラクタを呼び出します。ただし、各スコープでtryが最初で最後に最後になるように、try / finallyを調整する必要があります。また、各オブジェクトの標準を定義して、finallyブロックで呼び出す状態をクリーンアップするために使用する特定の名前のメソッドを持たせる必要がありますが、言語が提供する通常のメモリ管理はそのままにしておくことができます好きなときに空になったオブジェクトをクリーンアップします。

ただし、これを行うのはきれいではありません。また、.NETはIDisposeを手動で管理するデストラクタとして導入し、ブロックを使用して手動での管理をやや簡単にしようとしましたが、それでも実際にはやりたくないことです。


4

私の観点からの主な違いは、c ++のデストラクタが、 割り当てられたリソースを解放するための暗黙的なメカニズム(自動的に呼び出される)である一方で、try ... 最終的にそれを行う明示的なメカニズムとして使用できることです。

C ++プログラムでは、プログラマーは割り当てられたリソースを解放する責任があります。これは通常、クラスのデストラクタで実装され、変数がスコープ外に出たとき、または削除が呼び出されたときにすぐに実行されます。

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
}

C ++では、例外の場合にデストラクタがスタックオブジェクトでのみ自動的に呼び出され、ヒープオブジェクトではなく自動的に呼び出されるのはなぜですか?
ジョルジオ

@Giorgioヒープリソースは、呼び出しスタックに直接結び付けられていないメモリ空間に存在するためです。例えば、2つのスレッドとマルチスレッド・アプリケーションを想像し、AそしてB。1つのスレッドがスローした場合、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を使用することは理にかなっています。

言い換えると、どちらかをシミュレートするためにどちらかを使用できるからといって、両方が1つの同じ問題であること、または両方が両方の問題の適切な解決策であることを意味しません。


ありがとう。私は反対する、ちょっと:-)私は...私は次の日には徹底した反対側のビューの答えを追加することができますだと思う
マーティンのBa

2
finally(メモリ以外の)リソースを解放するために主に使用される事実は、どのようにこれに影響しますか?
バートヴァンインゲンシェナウ

1
@BartvanIngenSchenau:現在存在している言語には、私が説明したものと一致する哲学または実装があると主張したことはありません。人々はまだ存在する可能性のあるすべてのものを発明し終えていません。私は、2つの概念が異なるアイデアであり、異なるユースケースを持っているので、2つの概念を分離することに価値があると主張しただけです。あなたの好奇心を満たすために、Dには両方があると思います。おそらく他の言語もあります。しかし、私はそれを関連性があるとは考えません。そして、なぜJavaがを好むのかなど、それほど気にすることはできませんでしたfinally
user541686

1
JavaScriptで私が遭遇した実際的な例は、長時間の操作(例外をスローする可能性がある)の間にマウスポインターを砂時計に一時的に変更し、次にfinally句で通常に戻す関数です。C ++ワールドビューでは、擬似グローバル変数への割り当てのこの「リソース」を管理するクラスを導入します。それはどのような概念的な意味を持っていますか?ただし、デストラクタは、必要なブロックの終わりのコード実行のためのC ++のハンマーです。
dan04

1
@ dan04:どうもありがとう、これは完璧な例です。私は、RAIIが意味をなさない多くの状況に出くわすと誓うことができましたが、それらについて考えるのは非常に苦労しました。
user541686

1

k3bの答えは本当にうまく表現しています:

c ++のデストラクタは、割り当てられたリソースを解放するための暗黙的なメカニズム(自動的に呼び出されます)であり、try ...は最終的にそれを行う明示的なメカニズムとして使用できます。

「リソース」については、Jon Kalb:RAIIはResponsibility Acquisition Is Initializationを意味します。

とにかく、暗黙的vs明示的に関しては、これは本当にそれのようです:

  • d'torは、オブジェクトの有効期間が終了する(暗黙的に-スコープの終わりと一致することが多い)ときに、どの操作が行われるかを定義するツールです。
  • 最終ブロックは、明示的に-スコープの終わりにどの操作を行うかを定義するツールです。
  • さらに、技術的には、常に最終的にスローすることが許可されていますが、以下を参照してください。

概念的な部分については以上だと思います...


...現在、いくつかの興味深い詳細があります。

また、デストラクタでコードを実行する責任を除いて、c'tor / d'torが概念的に何かを「取得」または「作成」する必要があるとは思いません。これが最終的に行うことでもあります。コードを実行します。

そして、finallyブロック内のコードは確かに例外をスローする可能性がありますが、明示的なものと暗黙的なものとでは概念的に異なると言うのに十分な違いではありません。

(さらに、「良い」コードが最終的にスローされるべきだとはまったく確信していません。おそらくそれは、それ自体に対する別の全体的な質問でしょう。)


私のJavascriptの例についてどう思いますか?
user541686

あなたの他の議論に関して:「私たちは本当に関係なく同じことを記録したいのですか?」はい、それは単なる例であり、あなたはポイントを逃しているようなものです、そして、はい、各ケースのより具体的な詳細を記録することを誰も禁止していません。ここでのポイントは、両方に共通する何かをログに記録したい状況は決してないと主張できないことです。一部のログエントリは汎用であり、一部は特定です。あなたは両方が欲しい。繰り返しになりますが、ロギングに焦点を当てることで、ポイントを完全に失います。10行の例の動機付けは難しい。ポイントを見逃さないようにしてください。
user541686

あなたは...これらを扱ったことがない
user541686

@Mehrdad-あなたのjavascriptの例については触れませんでした。なぜなら、私が考えていることを議論するために別のページが必要になるからです。(試してみましたが、首尾一貫した何かを表現するのに非常に時間がかかったので、スキップしました:
マーティンBa

@Mehrdad-他の点に関しては-同意しないといけないようです。私はあなたが違いを目指しているところを見ていますが、それらが概念的に異なるものであると確信しているわけではありません:主に私は主に最終的に投げることが本当に悪い考えだと思うキャンプにいるからです(:私もあなたのobserver例では、そこに投げることは本当に悪い考えだと思います。)これについてさらに議論したい場合は、チャットを開いてください。あなたの議論について考えるのは確かに楽しかったです。乾杯。
マーティンBa
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.