Java の try/finally を C++ で模倣するための好まれるイディオムはありますか?

StackOverflow https://stackoverflow.com/questions/500244

質問

何年も Java をやっているので、C++ は追跡していません。もっている ついに 言語定義の C++ 例外処理に句が追加されましたか?

Java の try/finally を模倣する好まれるイディオムはありますか?

また、C++ には、Java の Throwable クラスのような、スローされる可能性のあるすべての例外に対応する究極のスーパータイプがないことも気になります。

次のように書くことができます:

try {
  // do something
} catch(...) {
  // alas, can't examine the exception
  // can only do cleanup code and perhaps rethrow, ala:
  throw;
}

追記編集:

最終的には、最大の投票があった答えを受け入れました。つまり、デストラクタを使用してクリーンアップを行いました。もちろん、私自身のコメントから、私がそれに完全に同意しないことは明らかです。ただし、C ++はそれが何であるかを考えるので、私が念頭に置いているアプリケーションの努力では、多かれ少なかれ共通のコミュニティの実践に固執するよう努めています。テンプレートクラスを使用して、クラスデストラクタ(IE、Cライブラリリソース)をまだ持っていないリソースをラップし、デストラクタセマンティクスを授与します。

新しい補遺編集:

うーん、代わりに ついに それでは、閉鎖機能はおそらく?ScopeGuardアプローチと組み合わされた閉鎖(以下の回答の1つを参照)は、任意のアクションとクリーンアップコードの外側スコープコンテキストへのアクセスでクリーンアップを達成する方法です。クリーンアップは、リソースが開かれているときにクリーンアップ ブロックを提供する Ruby プログラミングで見られる慣用的な方法で実行できます。C ++の閉鎖機能は考慮されていませんか?

役に立ちましたか?

解決

デストラクターを効果的に使用することによって。try ブロックで例外がスローされると、その中で作成されたオブジェクトはすべて即座に破棄されます (したがって、そのデストラクターが呼び出されます)。

これは、オブジェクトのファイナライザーがいつ呼び出されるかわからない Java とは異なります。

アップデート:馬の口から真っ直ぐに: C++ はなぜ「finally」構造を提供しないのですか?

他のヒント

私の.02ドル。私は何年も C# や Java などのマネージ言語でプログラミングしてきましたが、速度を高めるために C++ への切り替えを余儀なくされました。最初はメソッドのシグネチャをヘッダー ファイルと cpp ファイルに 2 回書き出す必要があることが信じられませんでした。また、finally ブロックがないこと、ガベージ コレクションがないためあらゆる場所でメモリ リークが追跡されることが気に入らなかったのです。うーん、全然気に入らなかったです!

ただし、前述したように、C++ を使用する必要がありました。それで私はそれを真剣に学ぶことを余儀なくされ、今ではついに RAII のようなプログラミングのイディオムをすべて理解し、言語の微妙な点などもすべて理解しました。しばらく時間がかかりましたが、今では C# や Java と比べてこの言語がいかに違うかがわかりました。

最近では、C++ が最高の言語だと考えています。はい、私が「チャフ」と呼ぶものがもう少しあることは理解できます (書く必要のないことのように見えます)。しかし、実際にこの言語を真剣に使ってみて、それについての考えは完全に変わりました。

以前は常にメモリリークが発生していました。私はコードを分離することが嫌いで、すべてのコードを .h ファイルに書き込んでいました。なぜそんなことをするのか理解できませんでした。そして、私はいつも愚かな循環インクルード依存関係やそれ以上のものを抱えてしまうことがよくありました。私は本当に C# か Java にこだわっていましたが、私にとって C++ は大きな一歩でした。最近ではそれが分かります。メモリ リークはほとんどなく、インターフェイスと実装の分離を楽しんでおり、サイクル依存関係の問題もなくなりました。

そしてfinalブロックも見逃せません。正直に言うと、私の意見では、catch ブロックに繰り返しクリーンアップ アクションを記述するというあなたが話している C++ プログラマーは、ただの下手な C++ プログラマーのようにしか聞こえません。つまり、このスレッドにいる他の C++ プログラマーは、あなたが指摘したような問題を抱えているようには見えません。RAII は実際にfinallyを冗長化し、どちらかといえば作業を軽減します。1 つのデストラクターを作成すれば、最終的には別のデストラクターを作成する必要がなくなります。まあ、少なくともそのタイプにとっては。

敬意を表しますが、私と同じように、あなたも Java に慣れてきただけだと思います。

C++ の答えは RAII です。オブジェクトのデストラクターは、スコープ外に出ると実行されます。返品でも例外でも何でも。別の場所で例外を処理する場合は、呼び出された関数からハンドラーまでのすべてのオブジェクトが、デストラクターを呼び出すことによって適切に破棄されることを確認できます。彼らはあなたのために掃除をしてくれます。

読む http://en.wikipedia.org/wiki/Resource_acquisition_is_initialization

No Final は C++ に追加されておらず、今後も追加される可能性はありません。

C++ がコンストラクター/デストラクターを使用する方法により、最終的には不要になります。
catch(...) を使用してクリーンアップを行っている場合は、C++ を適切に使用していません。クリーンアップ コードはすべてデストラクター内にある必要があります。

これを使用する必要はありませんが、C++ には std::Exception があります。
例外を使用するために特定のクラスから派生することを開発者に強制することは、C++ のシンプルさを維持する哲学に反します。すべてのクラスが Object から派生する必要がないのもこれが理由です。

読む: C++ は「finally」ブロックをサポートしていますか?(そして、私がよく聞く「RAII」とは何ですか?)

Final を使用すると、クリーンアップを行うデストラクタよりもエラーが発生しやすくなります。
これは、クラスの設計者/実装者ではなく、オブジェクトのユーザーにクリーンアップを強制しているためです。

わかりました。別の回答投稿であなたが指摘した点に対する回答を追加する必要があります。(これを元の質問に編集して、一番下にならないようにすると、はるかに便利になります。 下に それに対する答え。

すべてのクリーンアップが常にデストラクタで行われる場合、キャッチブロックにクリーンアップコードを必要とする必要はありませんが、C ++にはクリーンアップアクションが完了するキャッチブロックがあります。確かに、CATCH(...)のブロックがあり、クリーンアップアクションを実行することのみが可能です(まあ、ロギングを実行するために例外情報を取得することはできません)。

catch にはまったく別の目的があるため、Java プログラマーはそのことを認識しておく必要があります。finally 句は、「無条件」クリーンアップ アクション用です。ブロックがどのように終了されるかに関係なく、これを実行する必要があります。Catch は条件付きクリーンアップ用です。このタイプの例外がスローされた場合は、いくつかの追加のアクションを実行する必要があります。

最終的なブロックでのクリーンアップは、例外があるかどうかにかかわらず完了します - これは、クリーンアップコードが存在するときに常に起こりたいことです。

本当に?そうしたいのであれば いつも このタイプでは (つまり、データベース接続が完了したら常にデータベース接続を閉じたいなど)、それを定義しないのはなぜでしょうか。 一度?型自体で?データベース接続を使用するたびに try/finally を実行するのではなく、データベース接続を自動的に閉じるようにしますか?

それがデストラクターのポイントです。これらは、呼び出し元が考えることなく、使用されるたびに各タイプが独自のクリーンアップを処理できることを保証します。

初日からのC ++開発者は、Tryブロックからの出口が成功したときに発生するコードフローのキャッチブロックに表示されるクリーンアップアクションを繰り返す必要があると悩まされています。JavaとC#プログラマーは、最終的にブロックで一度それを行います。

いいえ。C++ プログラマーは、このことに悩まされたことはありません。C プログラマーはそうしています。そして、C++ にクラスがあることに気づき、自分たちを C++ プログラマーと呼んだ C プログラマーは、クラスを持っています。

私は毎日 C++ と C# でプログラミングしていますが、finally 句 (または using block) データベース接続やその他のクリーンアップが必要なものを毎回使用します。

C++ では、「この型の使用が完了したら、必ずこれらのアクションを実行する」ということをきっぱりと指定できます。メモリの解放を忘れる危険はありません。ファイル ハンドル、ソケット、データベース接続を閉じるのを忘れる危険はありません。なぜなら、メモリ、ハンドル、ソケット、データベース接続が自動的に実行するからです。

どうしてそうなるの これまで 型を使用するたびに重複したクリーンアップ コードを記述する必要がある方が望ましいでしょうか?型自体にデストラクターがないために型をラップする必要がある場合は、2 つの簡単なオプションがあります。

  • このデストラクターを提供する適切な C++ ライブラリを探してください (ヒント:ブースト)
  • boost::shared_ptr を使用してそれをラップし、実行時にカスタム ファンクターを指定して実行するクリーンアップを指定します。

Java EEアプリのようなアプリケーションサーバーソフトウェアを作成すると、Glassfish、JBossなどをサーバーすると、床に落ちるのではなく、例外情報をキャッチしてログに記録できるようにする必要があります。または、さらに悪いことに、ランタイムに分類され、アプリケーションサーバーの不名誉な突然の脱出を引き起こします。そのため、考えられる例外のために包括的な基本クラスを持つことが非常に望ましいです。そして、C++ にはまさにそのようなクラスがあります。std::例外。

CFRONTの時代とJava/C#からC ++を行ってきましたが、この10年のほとんど。根本的に類似したものがどのようにアプローチされているかに大きな文化のギャップがあることを見るのは明確です。

いいえ、C++ をやったことがありません。CFront、つまり C とクラスを実行しました。C++ではありません。大きな違いがあります。答えをつまらないと言うのはやめましょう。そうすれば、知っていると思っていた言語について何かを学べるかもしれません。;)

クリーンアップ機能自体はまったく不十分です。これらは、発生時にのみ関連する一連のアクティビティを実行することが期待されるため、凝集性が低くなります。実際に何かを行う関数が変更されると、内部を変更する必要があるため、結合度が高くなります。このため、間違いが発生しやすくなります。

try...finally 構造は、クリーンアップ関数のフレームワークです。これは言語によって推奨される、ひどいコードを記述する方法です。さらに、同じクリーンアップ コードを何度も記述することを奨励するため、DRY 原則が損なわれます。

これらの目的には、C++ の方法がはるかに適しています。リソースのクリーンアップ コードは、デストラクター内に 1 回だけ書き込まれます。これはそのリソースのコードの残りの部分と同じ場所にあるため、良好な一貫性があります。クリーンアップ コードを無関係なモジュールに入れる必要がないため、結合が削減されます。適切に設計されていれば、それは 1 回だけ書き込まれます。

さらに、C++ の方法はより均一です。スマート ポインターが追加された C++ は、あらゆる種類のリソースを同じ方法で処理しますが、Java はメモリを適切に処理し、他のリソースを解放するための不適切な構造を提供します。

C++ には多くの問題がありますが、これはその 1 つではありません。Java が C++ よりも優れている点はいくつかありますが、これはそのうちの 1 つではありません。

Java では、try...finally の代わりに RAII を実装する方法を採用した方がはるかに優れています。

解放可能なリソースごとにラッパー クラスを定義する必要を避けるために、ScopeGuard に興味があるかもしれません (http://www.ddj.com/cpp/184403758) これにより、その場で「クリーナー」を作成できます。

例えば:

FILE* fp = SomeExternalFunction();
// Will automatically call fclose(fp) when going out of scope
ScopeGuard file_guard = MakeGuard(fclose, fp);

最終的に正しく使用することがいかに難しいかを示す例。

2 つのファイルを開いたり閉じたりします。
ファイルが正しく閉じられたことを保証したい場合。
ファイルは再利用される可能性があるため、GC を待つことはできません。

C++の場合

void foo()
{
    std::ifstream    data("plop");
    std::ofstream    output("plep");

    // DO STUFF
    // Files closed auto-magically
}

デストラクターがなく、finally 節がある言語。

void foo()
{
    File            data("plop");
    File            output("plep");

    try
    {
        // DO STUFF
    }
    finally
    {
        // Must guarantee that both files are closed.
        try {data.close();}  catch(Throwable e){/*Ignore*/}
        try {output.close();}catch(Throwable e){/*Ignore*/}
    }
}

これは単純な例ですが、すでにコードが複雑になっています。ここでは、2 つの単純なリソースをマーシャリングしようとしているだけです。しかし、管理する必要があるリソースの数が増加したり、リソースの複雑さが増加したりすると、例外が発生した場合に、finally ブロックを正しく使用することがますます困難になります。

を使用すると、最終的にオブジェクトの正しい使用に対する責任がオブジェクトのユーザーに移されます。C++ が提供するコンストラクター/デストラクター メカニズムを使用すると、正しく使用する責任がクラスの設計者/実装者に移ります。これは、設計者がクラス レベルで一度正しく実行するだけで済むため、継承的に安全です (異なるユーザーに異なる方法で正しく実行してもらうよりも)。

C++11 を使用する ラムダ式, 私は最近、次のコードを使用して模倣し始めました。 finally:

class FinallyGuard {
private:
  std::function<void()> f_;
public:
  FinallyGuard(std::function<void()> f) : f_(f) { }
  ~FinallyGuard() { f_(); }
};

void foo() {
  // Code before the try/finally goes here
  { // Open a new scope for the try/finally
    FinallyGuard signalEndGuard([&]{
      // Code for the finally block goes here
    });
    // Code for the try block goes here
  } // End scope, will call destructor of FinallyGuard
  // Code after the try/finally goes here
}

FinallyGuard 呼び出し可能な関数のような引数、できればラムダ式で構築されるオブジェクトです。デストラクターが呼び出されるまで、その関数は単に記憶されます。これは、通常の制御フローまたは例外処理中のスタックの巻き戻しにより、オブジェクトがスコープ外になった場合です。どちらの場合も、デストラクターは関数を呼び出し、問題のコードを実行します。

のコードを書かなければならないのは少し奇妙です。 finally 前に のコード try ブロックですが、それ以外は実際には本物のように感じます try/finally ジャワから。独自の適切なデストラクタを持つオブジェクトの方が適切な状況では、これを乱用すべきではないと思いますが、上記のアプローチの方が適切であると考えるケースもあります。そのようなシナリオの 1 つについて私は次の記事で説明しました この質問.

私が理解している限りでは、 std::function<void()> 何らかのポインタ間接指定と少なくとも 1 つの仮想関数呼び出しを使用して、その実行を実行します。 タイプ消去, 、したがって、 パフォーマンスのオーバーヘッド. 。パフォーマンスが重要なタイトなループではこの手法を使用しないでください。このような場合、デストラクターが 1 つのことだけを行う特殊なオブジェクトの方が適切です。

C++ デストラクタは次のようにします。 finally 冗長です。クリーンアップ コードをfinallyから対応するデストラクターに移動することで、同じ効果を得ることができます。

あなたは何の要点を見逃していると思いますか catch (...) できる。

あなたの例では、「残念ながら、例外を調べることができません」と言います。そうですね、例外の種類に関する情報はありません。それがポリモーフィック型であるかどうかさえわからないため、それに対する何らかの型なし参照があったとしても、安全に試行することさえできません。 dynamic_cast.

何かを実行できる特定の例外または例外階層について知っている場合は、ここが明示的に名前が付けられた型を持つ catch ブロックの場所です。

catch (...) C++ ではあまり役に立ちません。これは、特定の契約例外をスローしない、またはスローするだけであることを保証する必要がある場所で使用できます。使用している場合 catch (...) クリーンアップの場合、コードはいかなる場合でも堅牢な例外安全性を備えていない可能性が非常に高くなります。

他の回答で述べたように、ローカル オブジェクトを使用してリソース (RAII) を管理している場合、必要な catch ブロックがいかに少ないかは驚くべきことであり、啓発的です。多くの場合、例外を使用してローカルで何もする必要がない場合、 try ブロックは、リソースの問題がないことを保証しながら例外に応答できるクライアント コードに例外を流すため、冗長になる可能性があります。

元の質問に答えると、ブロックの最後、例外または例外なしで実行するコードが必要な場合は、レシピになります。

class LocalFinallyReplacement {
    ~LocalFinallyReplacement() { /* Finally code goes here */ }
};
// ...
{ // some function...
    LocalFinallyReplacement lfr; // must be a named object

    // do something
}

完全に廃止する方法に注目してください。 try, catch そして throw.

もともと try ブロックの外で宣言された関数内のデータがあり、そのデータに「finally」ブロックでアクセスする必要がある場合は、それをヘルパー クラスのコンストラクターに追加し、デストラクターまで保存する必要がある場合があります。ただし、この時点で、ローカル リソース処理オブジェクトの設計を変更することで問題を解決できるかどうか、真剣に再考します。これは、設計に何か問題があることを意味するからです。

完全に的外れというわけではありません。

Java でのボイラー メッキ DB リソースのクリーンアップ

皮肉モード:Java のイディオムは素晴らしいと思いませんか?

私はこの 15 年間、C++ で多くのクラス設計とテンプレート ラッパー設計を行ってきましたが、デストラクターのクリーンアップに関してはすべて C++ の方法で行いました。ただし、すべてのプロジェクトには、オープン、使用、クローズの使用モデルでリソースを提供する C ライブラリの使用も常に含まれていました。try/finally は、そのようなリソースが必要な場所で (完全に堅牢な方法で) 消費され、完了することを意味します。その状況をプログラミングするための最も退屈なアプローチ。ラッパー デストラクターでスコープを設定することなく、クリーンアップのロジック中に発生する他のすべての状態を処理できます。

私は C++ コーディングのほとんどを Windows で行ったので、そのような状況では常に Microsoft の __try/__finally を使用することに頼ることができました。(構造化例外処理には、例外と対話するためのいくつかの強力な機能があります。) 残念ながら、C 言語は移植可能な例外処理構造を承認したことがないようです。

ただし、どちらかのスタイルの例外がスローされる可能性がある try ブロック内で C と C++ のコードをブレンドするのは簡単ではなかったため、これは理想的な解決策とは言えませんでした。C++ に追加された Finally ブロックは、そのような状況に役立ち、移植性を可能にするでしょう。

追記編集に関しては、C++0x 用のクロージャーが検討されています。RAII スコープ ガードと一緒に使用すると、使いやすいソリューションを提供できます。 パイザーのブログ. 。また、try-finally を模倣するために使用することもできます。 この答え ;しかし これは本当に良い考えですか? .

これに独自のソリューションを追加しようと思いました。これは、非 RAII 型を処理する必要がある場合の一種のスマート ポインター ラッパーです。

このように使用されます:

Finaliser< IMAPITable, Releaser > contentsTable;
// now contentsTable can be used as if it were of type IMAPITable*,
// but will be automatically released when it goes out of scope.

Finaliser の実装は次のとおりです。

/*  Finaliser
    Wrap an object and run some action on it when it runs out of scope.
    (A kind of 'finally.')
    * T: type of wrapped object.
    * R: type of a 'releaser' (class providing static void release( T* object )). */
template< class T, class R >
class Finaliser
{
private:
    T* object_;

public:
    explicit Finaliser( T* object = NULL )
    {
        object_ = object;
    }

    ~Finaliser() throw()
    {
        release();
    }

    Finaliser< T, R >& operator=( T* object )
    {
        if (object_ != object && object_ != NULL)
        {
            release();
        }
        object_ = object;

        return *this;
    }

    T* operator->() const
    {
        return object_;
    }

    T** operator&()
    {
        return &object_;
    }

    operator T*()
    {
        return object_;
    }

private:
    void release() throw()
    {
        R::release< T >( object_ );
    }
};

...そしてリリーサーは次のとおりです。

/*  Releaser
    Calls Release() on the object (for use with Finaliser). */
class Releaser
{
public:
    template< class T > static void release( T* object )
    {
        if (object != NULL)
        {
            object->Release();
        }
    }
};

このようなリリーサーはいくつかあり、free() 用と CloseHandle() 用が 1 つあります。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top