2012-01-31 21 views
1

[OK]を世紀の質問:)次のような状況でgotoを使用してもよろしいですか?

あなたが言うか何かを考える前に、私はこの非常にトピックに関する同様の質問のカップルを読んだことを教えてみましょうが、私は私のための明確な解決策を見つけられませんでした問題。私のケースは具体的であり、システムプログラマのための典型的なものです。

私はこの状況が非常によくあります。私は、本当に誰もがそれが悪いと叫んでいる理由を知っていない、おそらく嫌いだ。しかし、今まで私は自分のシナリオに対してより良い解決策を見つけることはできませんでした。私がやっているやり方は現在、gotoの使用よりも醜いかもしれません。

ここに私のケースです:私はWindowsアプリケーションの開発にはC++(Visual C++)を使用しており、私のルーチンではかなりの頻度でAPIを使用しています。だから、各APIの後、私はそれがなかった場合、それが成功したかどうかを確認し、私の機能を中断する必要が

int MyMemberFunction() 
{ 
    // Some code... // 

    if (!SomeApi()) 
    { 
     // Cleanup code... // 

     return -1; 
    } 

    // Some code... // 

    if (!SomeOtherApi()) 
    { 
     // Cleanup code... // 

     return -2; 
    } 

    // Some more code... // 

    if (!AnotherApi()) 
    { 
     // Cleanup code... // 

     return -3; 
    } 

    // More code here... // 

    return 0; // Success 
} 

:次のような状況を仮定します。このために、私は// Cleanup code... //の束を使用します。しばしばかなり複製され、その後にreturnという文が続きます。この機能は、例えば10個のタスク(例えば10個のApisを使用する)を実行し、タスク#6が失敗した場合、以前のタスクによって作成されたリソースをクリーンアップする必要がある。クリーンアップは関数自体で行う必要があるため、例外処理は使用できません。また、このような状況で、いかに多くの話題のRAIIが私を助けてくれるのか分かりません。

私が考えた唯一のやり方は、gotoを使用して、そのようなすべての失敗事例から機能の最後に配置されたクリーンアップラベルにジャンプすることです。

これを実行する方法はありますか?このような状況では、gotoを使うのは悪い習慣と思われますか?何をしたらいいですか?このような状況は、私にとって(そして私のようなシステムプログラマにとっては)典型的なことです。

P .:リソースをクリーンアップする必要があるリソースは、種類が異なります。閉鎖を必要とするメモリの割り当て、さまざまなシステムオブジェクトハンドルなどがあるかもしれません

UPDATE:

私は、人々はまだ私は(おそらく私がひどく説明しています)望んで取得していないと思います。私は擬似コードは十分なはずと思ったが、ここでは実用的な例である:

  1. 私はCreateFileを持つ2つのファイルを開きます。この手順が失敗した場合:既に開いているファイルハンドルがあればそれをクリーンアップする必要があります。私は後であるファイルの一部を読んで、別のファイルに書き込む。

  2. 私はSetFilePointerを使用して、最初のファイルに読み取りポインタを配置します。この手順が失敗した場合:前の手順で開いたハンドルを閉じる必要があります。

  3. 私はGetFileSizeを使用してターゲットファイルサイズを取得します。 apiが失敗した場合、またはファイルサイズが異常な場合は、前の手順と同様にクリーンアップを行う必要があります。

  4. 最初のファイルから読み込むために指定されたサイズのバッファを割り当てます。メモリの割り当てに失敗した場合は、ファイルハンドルをもう一度閉じる必要があります。

  5. 最初のファイルから読み込むにはReadFileを使用する必要があります。これに失敗した場合は、バッファメモリを解放し、ファイルハンドルをクローズする必要があります。

  6. 私はSetFilePointerを使用して、2番目のファイルに書き込みポインタを配置します。これに失敗した場合は、同じクリーンアップを実行する必要があります。

  7. 2番目のファイルに書き込むためにWriteFileを使用する必要があります。これはまた、BLA-BLA-BLA ...

を失敗した場合、私はクリティカルセクションでこの機能を守る、と私は関数の先頭にEnterCriticalSectionを呼び出した後、私はすべてのreturnLeaveCriticalSectionを呼び出すことがあるとしステートメント。

これは非常に単純化された例です。より多くのリソースとクリーンアップが実行される可能性がありますが、ほとんど同じですが、時には少し異なりますが、どのステップが失敗したかに基づいています。しかし、この例の中で話しましょう:私はここでRAIIを使うことができますか?

+3

[それはどれくらい悪いことができますか?](ただし、真剣に、*より読みやすいコード*を作成すると悪くはありません。 ) –

+1

クリーンアップコードは共通ですか、クリーンアップコードはその機能にどれくらい手が届くかによって決まりますか? – Corbin

+0

gotoを回避する方法の1つは、intをスローしてキャッチすることです。 – QuentinUK

答えて

4

以前のタスクによって作成されたリソースがそれ自身の後でクリーンアップするクラス/オブジェクトの形で維持されている限り、これはRAIIでも機能します。あなたはメモリとシステムのオブジェクトハンドルについて言及しましたので、それらを開始点として使用しましょう。

// non RAII code: 
int MyMemberFunction() { 
    FILE *a = fopen("Something", "r"); 

    if (!task1()) { 
     fclose(a); 
     return -1; 
    } 

    char *x = new char[128]; 

    if (!task2()) { 
     delete [] x; 
     fclose(a); 
     return -2; 
    } 
} 

RAIIベースのコード:

int MyMemberFunction() { 
    std::ifstream a("Something"); 
    if (!task1()) 
     return -1; // a closed automatically when it goes out of scope 

    std::vector<char> x(128); 

    if (!task2()) 
     return -2; // a closed, x released when they go out of scope 
    return 0; // again, a closed, x released when they go out of scope 
} 

はまた、あなたが正常に動作する事を期待していた場合、あなたが描写するコードを書くことに注意しているより密接ビット:

int MyMemberFunction() { 
    bool one, two; 

    if ((one=task1()) && (two=task2())) 
     return 0; 

    // If there was an error, figure out/return the correct error code. 
    if (!one) 
     return -1; 
    return -2; 
} 

編集:これはかなり珍しいことですが、実際にはCスタイルのI/Oを使用する必要がある場合でも、iostreamに似ているC++クラスに包み込むことができます。

class stream { 
    FILE *file; 
public: 
    stream(char const &filename) : file(fopen(filename, "r")) {} 
    ~stream() { fclose(file); } 
}; 

これは明らかに簡略化されていますが、一般的な考え方は完全にうまく機能します。あまり明白ではありませんが、一般的に優れたアプローチです:iostreamは実際にはバッファークラスを使用して、読み込みにはunderflow、書き込みにはoverflowを使用します(今回はそれほど簡単ではありません)。読み書きを処理するためにFILE *を使用するバッファクラスを記述することは非常に難しいことではありません。関与するコードのほとんどは、機能に適切な名前を付けたり、パラメータを正しいタイプや注文に並べ替えるなど、かなり薄い翻訳レイヤーにすぎません。

メモリの場合、2つのアプローチがあります。一つは、あなたが使用する必要がどのようなメモリ管理のラッパーとして純粋に機能し、独自のベクトルのようなクラスを書いて、このようなものです(new/deletemalloc/freeなど)

別のアプローチがあることを観察することですstd::vectorにはAllocatorパラメータがあります。実際は既にラッパーであり、メモリの取得/解放方法を指定できます。たとえば、バックエンドが実際にmallocfreeである必要がある場合は、それらを使用したAllocatorクラスを書くのはかなり簡単です。このようにして、ほとんどのコードは普通のC++の規則に従います。std::vectorを他のものと同じように使用します(そしてRAIIを上記のようにサポートします)。同時に、メモリ管理の実装を完全に制御できるため、必要に応じてnew/delete,malloc/freeなどの情報をOSから直接取得できます(Windowsの場合はLocalAlloc/LocalFreeなど)。

+0

RAIIベースのコードは、単に異なるコードを使用します。 'std :: vector'の代わりに' new'を使い、 'std :: ifstream'の代わりに' fopen'を使って動的メモリ割り当てを使わなければならない場合はどうしたらいいですか?これはここで最も重要なポイントです。 –

+0

@TX_:答えの編集を参照してください。 –

+0

ここでは、メモリ割り当て/割り当て解除管理がそれほど問題になりません。ほとんどの場合、いくつかのOSリソースをクリーンアップする必要があります。これは、クローズ処理、オブジェクトの削除などを意味します。そのため、私は「システムプログラミング」という用語を使用しました。私のメインポストの編集をご覧ください。与えられた例では、メモリを解放し、ファイルハンドルを閉じ、クリティカルセクションを終了する必要があります。それは本当に単純化された状況です。ここでRAIIはどのように使用できますか? –

7

gotoの使用は必要ありません。エラーが発生しやすく、コードが冗長で、安全ではありません。

RAIIを使用し、gotoを使用する必要はありません。あなたのシナリオにはRAII〜smart pointersが最適です。

エラー状態が発生するたびにRAIIが管理している(スマートポインタまたは独自のリソース管理クラスを使用する)か、RAIIが魔法のようにリソースを暗黙的に解放する。

+0

私が投稿した疑似コードの例を教えてください。 –

+0

@ TX_、クリーンアップコードの詳細については、詳細を提供する必要があります。独自のRAIIクラスを作成することができます。 –

+0

彼がやっている "システムプログラミング"の種類によって、RAIIの導入はパフォーマンス上の理由から疑わしいかもしれません(おそらくそうではありませんが、私の意見では留意してください)。 – Corbin

関連する問題