C ++に「最終的に」コンストラクトがないのはなぜですか?


57

C ++での例外処理は、try / throw / catchに制限されています。Object Pascal、Java、C#、Pythonとは異なり、C ++ 11でもfinallyコンストラクトは実装されていません。

「例外の安全なコード」について議論しているC ++の文献がたくさんあります。リップマンは、例外セーフコードは重要だが高度で難しいトピックであり、彼の入門書の範囲を超えていると書いています。これは、セーフコードがC ++の基本ではないことを暗示しているようです。ハーブサッターは、例外的なC ++のトピックに10章を費やしています!

しかし、「例外セーフコード」を記述しようとするときに遭遇する問題の多くは、finally構造が実装されていれば非常によく解決でき、プログラマは例外が発生した場合でもプログラムを復元できるようになりますリソース、潜在的に問題のあるコードの割り当てのポイントに近い、安全で安定した、漏れのない状態に。私は非常に経験豊富なDelphiおよびC#プログラマーとしてtry ..を使用します。これらの言語のほとんどのプログラマーと同様に、最終的にコード内でかなり広範囲にブロックします。

C ++ 11に実装されているすべての「付加機能」を考慮すると、「最終的に」まだ存在していないことに驚いた。

それでは、なぜfinallyコンストラクトがC ++で実装されていないのでしょうか?把握するのはそれほど難しくも高度な概念でもないので、プログラマーが「例外安全なコード」を書くのを支援するのに大いに役立ちます。


25
なんでついに?オブジェクト(またはスマートポインター)がスコープを離れると自動的に起動するデストラクタで物事を解放するためです。デストラクターは、ワークフローをクリーンアップロジックから分離するため、finally {}よりも優れています。free()の呼び出しが、ガベージコレクションされた言語でワークフローを乱雑にしたくないのと同じように。
mike30


8
「なぜfinallyC ++にはないのか、その代わりにどのような例外処理のテクニックが使用されているのか」という質問をします。このサイトのトピックで有効です。既存の答えはこれをうまくカバーしていると思います。finally「価値のないものを含めたC ++デザイナーの理由は?」に関する議論に変わります。「finallyC ++に追加する必要がありますか?」質問へのコメントを超えて議論を続けると、すべての回答がこのQ&Aサイトのモデルに適合しません。
ジョシュケリー

2
最終的には、懸念の分離が既にあります。メインコードブロックはここにあり、クリーンアップの懸念はここで処理されます。
カズ

2
@カズ。違いは、暗黙的なクリーンアップと明示的なクリーンアップです。デストラクタは、スタックからポップするときに単純な古いプリミティブがクリーンアップされる方法に似た自動クリーンアップを提供します。明示的なクリーンアップコールを行う必要はなく、コアロジックに集中できます。try / finallyでスタックに割り当てられたプリミティブをクリーンアップしなければならなかった場合の複雑さを想像してください。暗黙のクリーンアップが優れています。クラス構文と匿名関数の比較は関係ありません。ただし、ハンドルを解放する関数に最初のクラスの関数を渡すことにより、手動でのクリーンアップを一元化できます。
mike30

回答:


57

@Nemanja の答えに関する追加のコメントとして(これはStroustrupを引用しているので、あなたが得ることができるのと同じくらい本当に良い答えです):

C ++の哲学とイディオムを理解するだけです。永続クラスでデータベース接続を開き、例外がスローされた場合にその接続を確実に閉じる必要がある操作の例を取り上げます。これは例外の安全性の問題であり、例外のあるすべての言語(C ++、C#、Delphi ...)に適用されます。

try/ を使用する言語ではfinally、コードは次のようになります。

database.Open();
try {
    database.DoRiskyOperation();
} finally {
    database.Close();
}

シンプルでわかりやすい。ただし、いくつかの欠点があります。

  • 言語に決定論的なデストラクタがない場合は、常にfinallyブロックを記述する必要があります。そうしないと、リソースがリークします。
  • if DoRiskyOperationが単一のメソッド呼び出しを超えている場合- tryブロック内で何らかの処理を行う場合- Close操作は、Open操作からかなり離れたところにある可能性があります。買収の直後にクリーンアップを書くことはできません。
  • 取得する必要のある複数のリソースがあり、例外に対して安全な方法で解放されている場合、try/ finallyblockの深さのいくつかのレイヤーになります。

C ++のアプローチは次のようになります。

ScopedDatabaseConnection scoped_connection(database);
database.DoRiskyOperation();

これにより、finallyアプローチのすべての欠点が完全に解決されます。これにはいくつかの欠点がありますが、比較的マイナーです。

  • ScopedDatabaseConnection自分でクラスを作成する必要がある可能性は十分にあります。ただし、非常に単純な実装であり、コードは4行または5行のみです。
  • 「混乱をきれいにするためにデストラクタに依存するクラスを常に作成および破棄する」というコメントに基づいて、あなたは明らかにファンではない余分なローカル変数を作成する必要がありますが、優れたコンパイラは最適化されます余分なローカル変数が関与する余分な作業を排除します。優れたC ++設計は、この種の最適化に大きく依存しています。

個人的には、これらの長所と短所を考慮すると、RAIIはに比べてはるかに望ましい手法finallyです。あなたのマイレージは異なる場合があります。

最後に、RAIIはC ++で非常に確立されたイディオムであり、多数のScoped...クラスを記述する負担を開発者に軽減するために、ScopeGuardBoost.ScopeExitのようなこの種の決定論的なクリーンアップを容易にするライブラリがあります。


8
C#にはusingIDisposableインターフェイスを実装するオブジェクトを自動的にクリーンアップするステートメントがあります。そのため、誤解する可能性はありますが、正しく理解するのは非常に簡単です。
ロバートハーヴェイ

18
try/finallyコンパイラがtry/finallyコンストラクトを公開せず、アクセスするための唯一の方法がクラスベースを介しているため、コンストラクトを使用してコンパイラによって実装される設計イディオムを使用して、一時的な状態変更の反転を処理するために完全に新しいクラスを記述する必要がありますデザインのイディオムは「利点」ではありません。それはまさに抽象化反転の定義です。
メイソンウィーラー

15
@MasonWheeler-うーん、新しいクラスを書く必要があるとは言わなかった。私はそれが不利だと言った。バランス上、しかし、私はRAII を使用しなければならないよりもRAIIを好むfinally。私が言ったように、あなたの走行距離は異なる場合があります。
ジョシュケリー

7
@JoshKelley:「優れたC ++設計は、この種の最適化に大きく依存しています。」無関係なコードのゴブを作成し、コンパイラの最適化に依存するのはグッドデザインですか?!IMOそれは良いデザインのアンチテーゼです。優れたデザインの基本には、簡潔で読みやすいコードがあります。デバッグ、保守などが少なくなります。コードの塊を書いて、コンパイラに頼ってそれをすべて消滅させるべきではありません -IMOはまったく意味がありません!
ベクトル

14
@Mikey:コードベース全体でクリーンアップコードを複製する(またはクリーンアップを実行する必要があるという事実)のは、「簡潔」で「読みやすい」ということですか?RAIIを使用すると、このようなコードを1回記述するだけで、あらゆる場所に自動的に適用されます。
マンカルス

55

なぜC ++「ついに」構築を提供していませんか?ビャーネ・ストロヴストルップのC ++スタイルとテクニックよくある質問

C ++は、ほぼ常に優れた代替手段をサポートしているため、「リソースの取得は初期化」手法(TC ++ PL3セクション14.4)です。基本的な考え方は、ローカルオブジェクトによってリソースを表現し、ローカルオブジェクトのデストラクタがリソースを解放することです。そうすれば、プログラマはリソースを解放することを忘れることができません。


5
しかし、C ++固有の手法については何もありませんか?オブジェクト、コンストラクタ、デストラクタを使用して、任意の言語でRAIIを実行できます。これは素晴らしいテクニックですが、RAIIが単に存在するということは、Strousupが言っていることfinallyもかかわらず、コンストラクトがいつまでも役に立たないことを意味するものではありません。「例外安全コード」を書くことがC ++で大したことであるという単なる事実は、その証拠です。ちなみに、C#にはデストラクタとの両方finallyあり、どちらも使用されます。
タクロイ

28
@Tacroy:C ++は、決定論的なデストラクタを備えた数少ないメインストリーム言語の1つです。C#の「デストラクタ」はこの目的には使用できません。RAIIを使用するには、「使用中」ブロックを手動で記述する必要があります。
ネマンジャトリフノヴィッチ

15
@Mikeyには、「C ++が「最終的に」コンストラクトを提供しないのはなぜですか」という答えがあります。そこからStroustrup自身から直接。さらに何を求めることができますか?それ理由です。

5
あなたが特定のリソースを漏洩しない、うまく動作してあなたのコードを心配している場合、例外はそれで投げているとき@Mikey、あなたがされている例外安全なコードを書くしようとしている/例外安全性を心配します。単にそれを呼び出しているのではなく、さまざまなツールが利用可能であるため、それを別々に実装しています。しかし、例外の安全性について議論するときは、まさにC ++の人々が語っています。

19
@Kaz:デストラクタでのクリーンアップは1回だけ行う必要があり、それ以降はオブジェクトを使用するだけです。割り当てる操作を使用するたびに、finallyブロックでクリーンアップを行うことを忘れないでください。
-deworde

19

C ++に含まれていない理由は、C ++ではfinally必要ないためです。 finally例外が発生したかどうかに関係なく、コードを実行するために使用されます。これはほとんどの場合、何らかのクリーンアップコードです。C ++では、このクリーンアップコードは関連するクラスのデストラクタ内にある必要があり、デストラクタは常にfinallyブロックのように呼び出されます。クリーンアップにデストラクタを使用するイディオムはRAIIと呼ばれます。

C ++コミュニティ内では、「例外に対して安全な」コードについてより多くの話があるかもしれませんが、例外がある他の言語でもほぼ同様に重要です。「例外セーフ」コードのポイントは、呼び出す関数/メソッドのいずれかで例外が発生した場合、コードがどの状態のままになるかを考えることです。
C ++では、例外により孤立したオブジェクトを処理する自動ガベージコレクションがC ++にはないため、「例外セーフ」コードの方がわずかに重要です。

例外の安全性がC ++コミュニティで詳細に議論されている理由は、おそらく、C ++ではデフォルトのセーフティネットが少ないので、何が間違っているのかをよりよく認識しなければならないという事実に起因します。


2
注:C ++には決定論的なデストラクタがあると主張しないでください。また、Object Pascal / Delphiには確定的なデストラクタがありますが、以下の最初のコメントで説明した非常に良い理由により、「最終的に」もサポートしています。
ベクトル

13
@Mikey:finallyC ++標準に追加する提案がこれまでになかったことを考えると、C ++コミュニティがthe absence of finally問題とみなさないと結論付けるのは安全だと思います。持っているほとんどの言語にはfinally、C ++が持つ一貫した決定論的な破壊がありません。Delphiには両方がありますが、どちらが最初に存在したかを知るのに十分なほど歴史がわかりません。
バートヴァンインゲンシェナウ

3
Dephiはスタックベースのオブジェクトをサポートしていません-スタックベースのオブジェクトとスタック上のオブジェクト参照のみです。したがって、適切なときにデストラクタなどを明示的に呼び出すには「最終的に」必要です。
ベクトル

2
C ++には多くの問題があり、それは間違いなく必要ではないので、これは正しい答えにはなりません。
カズ

15
20年以上、私はこの言語を使用し、その言語を使用した他の人々と仕事をしてきましたfinally。簡単にアクセスできたはずのタスクを思い出せません。
ロボットをゲット

12

他の人は、RAIIをソリューションとして検討しています。それは完全に良い解決策です。しかし、それは彼らがfinally広く望まれているものであるので彼らが同様に追加しなかった理由に実際に対処しません。それに対する答えは、C ++の設計と開発にとってより基本的なものです。C++の開発を通じて、関係者は、特に導入が必要な場合に、他の機能を使用して大騒ぎせずに達成できる設計機能の導入に強く抵抗しました古いコードの互換性を失わせる可能性のある新しいキーワード。RAIIは非常に機能的な代替手段を提供し、とにかくC ++ 11でfinally実際にロールバックできるのでfinally、ほとんど必要ありませんでした。

行う必要がFinallyあるのは、デストラクタのコンストラクタに渡された関数を呼び出すクラスを作成することだけです。次に、これを行うことができます:

try
{
    Finally atEnd([&] () { database.close(); });

    database.doRisky();
}

ただし、ほとんどのネイティブC ++プログラマーは、一般に、きれいに設計されたRAIIオブジェクトを好みます。


3
ラムダの参照キャプチャがありません。しなければならないFinally atEnd([&] () { database.close(); });。また、私は次のことを想像する方がよい:{ Finally atEnd(...); try {...} catch(e) {...} }(。それはcatchブロックの後に実行されるように、私が試し-ブロックのうち、ファイナライザを持ち上げる)
トーマスEding

2

try / catchブロックを使用したくない場合でも、「トラップ」パターンを使用できます。

単純なオブジェクトを必要なスコープに入れます。このオブジェクトのデストラクタに「最終的な」ロジックを配置します。スタックが解かれると、オブジェクトのデストラクタが呼び出され、お菓子がもらえます。


1
これは、質問に答えると、単にことを証明しない最後に ...結局、このような悪い考えではありません
ベクトル

2

さて、finallyLambdasを使用して、自分でロールオーバーすることができます。これにより、次のコードをうまくコンパイルできます(もちろん、素晴らしいコードではなくRAIIなしの例を使用します)。

{
    FILE *file = fopen("test","w");

    finally close_the_file([&]{
        cout << "We're closing the file in a pseudo-finally clause." << endl;
        fclose(file);
    });
}

こちらの記事をご覧ください。


-2

ここでRAIIがのスーパーセットであるという主張に同意するかどうかはわかりませんfinally。RAIIのアキレス腱は単純です。例外です。RAIIはデストラクタを使用して実装されており、C ++ではデストラクタから破棄することは常に間違っています。つまり、クリーンアップコードをスローする必要がある場合は、RAIIを使用できません。finally一方、実装された場合、finallyブロックからスローすることは合法ではないと信じる理由はありません。

次のようなコードパスを考えます。

void foo() {
    try {
        ... stuff ...
        complex_cleanup();
    } catch (A& a) {
        handle_a(a);
        complex_cleanup();
        throw;
    } catch (B& b) {
        handle_b(b);
        complex_cleanup();
        throw;
    } catch (...) {
        handle_generic();
        complex_cleanup();
        throw;
    }
}

もしあれば、次のfinallyように書くことができます。

void foo() {
    try {
        ... stuff ...
    } catch (A& a) {
        handle_a(a);
        throw;
    } catch (B& b) {
        handle_b(b);
        throw;
    } catch (...) {
        handle_generic();
        throw;
    } finally {
        complex_cleanup();
    }
}

しかし、RAIIを使用して同等の動作を取得する方法はありません。

誰かがC ++でこれを行う方法を知っているなら、私はその答えにとても興味があります。たとえば、いくつかの特別な機能などを備えた単一のクラスからすべての例外を継承するように強制するなど、信頼できるものでさえ満足です。


1
2番目の例では、if complex_cleanupがスローできる場合、RAII /デストラクタの場合と同様に、キャッチされていない2つの例外が同時に飛行している場合があり、C ++はこれを許可しません。元の例外を表示するcomplex_cleanup場合は、RAII /デストラクタの場合と同様に、例外を防止する必要があります。complex_cleanupの例外を表示したい場合、ネストされたtry / catchブロックを使用できると思います-これは接線であり、コメントに収めるのが難しいので、別の質問に値します。
ジョシュケリー

RAIIを使用して、最初の例と同じ動作をより安全に取得したいと思います。推定finallyブロックのスローは、catchブロックWRTの実行中の例外のスローと同じように機能しますstd::terminate。質問は「なぜfinallyC ++にないの?」です。そして、答えはすべて「あなたはそれを必要としません... RAII FTW!」と言います。私のポイントは、はい、RAIIはメモリ管理のような単純なケースには適していますが、例外の問題が解決されるまで、汎用ソリューションであるためには考えすぎ、オーバーヘッド、懸念、再設計が必要です。
MadScientist

3
私はあなたの主張を理解しています-デストラクタに投げるかもしれないいくつかの正当な問題があります-しかし、それらはまれです。RAII +例外には未解決の問題があるとか、RAIIが汎用ソリューションではないと言うと、ほとんどのC ++開発者の経験とは一致しません。
ジョシュケリー

1
デストラクタで例外を発生させる必要があることに気付いた場合は、何か間違ったことをしていることになります。おそらく、必要のないときに他の場所でポインタを使用しています。
ベクトル

1
これはコメントにはあまりにも複雑です。それについての質問投稿:あなたはRAIIモデルを使用してC ++でこのシナリオを扱うでしょうどのように... ...動作していないよう 次のことを行う必要があり、再度コメントを指示:タイプ@メンバーの、名前は、あなたが話していますコメントの冒頭に。コメントが自分の投稿にある場合、すべてが通知されますが、他の人はコメントを指示しない限り通知されません。
ベクトル
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.