この質問は、実際は少し前のprogramming.reddit.comでの 興味深い議論 の結果です。基本的には、次のコードに要約されます。
int foo(int bar)
{
int return_value = 0;
if (!do_something( bar )) {
goto error_1;
}
if (!init_stuff( bar )) {
goto error_2;
}
if (!prepare_stuff( bar )) {
goto error_3;
}
return_value = do_the_thing( bar );
error_3:
cleanup_3();
error_2:
cleanup_2();
error_1:
cleanup_1();
return return_value;
}
ここでgoto
を使用するのが最善の方法であるように見えます。その結果、すべての可能性の中で最もクリーンで効率的なコードになります。 Code CompleteでSteve McConnellを引用:
Gotoは、リソースを割り当て、それらのリソースに対して操作を実行し、リソースの割り当てを解除するルーチンで役立ちます。 gotoを使用すると、コードの1つのセクションでクリーンアップできます。 gotoを使用すると、エラーを検出した各場所でリソースの割り当てを忘れる可能性が低くなります。
このアプローチの別のサポートは、 このセクション のLinux Device Driversブックにあります。
どう思いますか?この場合、Cでgoto
を有効に使用できますか?より複雑で効率の悪いコードを生成する他の方法を好むが、goto
を避けますか?
FWIF、質問の例で示したエラー処理のイディオムは、これまでの回答で示されたどの選択肢よりも読みやすく、理解しやすいと思います。 goto
は一般に悪い考えですが、単純で統一された方法で実行されると、エラー処理に役立ちます。この状況では、goto
であるにもかかわらず、明確に定義された構造化された方法で使用されています。
一般的なルールとして、gotoを回避することは良い考えですが、ダイクストラが最初に「GOTOを考慮した有害」と書いたときに流行していた虐待は、最近ではほとんどの人の心を越えません。
あなたが概説するのは、エラー処理の問題に対する一般化可能な解決策です-慎重に使用する限り、私はそれで問題ありません。
特定の例は、次のように簡略化できます(ステップ1)。
int foo(int bar)
{
int return_value = 0;
if (!do_something(bar)) {
goto error_1;
}
if (!init_stuff(bar)) {
goto error_2;
}
if (prepare_stuff(bar))
{
return_value = do_the_thing(bar);
cleanup_3();
}
error_2:
cleanup_2();
error_1:
cleanup_1();
return return_value;
}
プロセスの継続:
int foo(int bar)
{
int return_value = 0;
if (do_something(bar))
{
if (init_stuff(bar))
{
if (prepare_stuff(bar))
{
return_value = do_the_thing(bar);
cleanup_3();
}
cleanup_2();
}
cleanup_1();
}
return return_value;
}
これは、元のコードと同等だと思います。元のコード自体は非常にきれいで、よく整理されていたため、これは特にきれいに見えます。多くの場合、コードフラグメントはそれほど整頓されていません(ただし、そうあるべきだという議論は受け入れます)。たとえば、多くの場合、初期化(セットアップ)ルーチンに渡す状態が示されているよりも多いため、クリーンアップルーチンに渡す状態も多くなります。
誰もこの代替案を提案していなかったので驚いたので、しばらくの間質問を追加しましたが、この問題に対処する良い方法の1つは、変数を使用して現在の状態を追跡することです。これは、goto
がクリーンアップコードに到達するために使用されるかどうかに関係なく使用できるテクニックです。他のコーディング手法と同様に、長所と短所があり、すべての状況に適しているわけではありませんが、スタイルを選択する場合は考慮する価値があります-特にgoto
を避けて深くしたい場合ネストされたif
s。
基本的な考え方は、実行する必要のあるクリーンアップアクションごとに、クリーンアップを実行する必要があるかどうかを判断できる値を持つ変数が存在するということです。
goto
バージョンを最初に表示します。元の質問のコードに近いためです。
_int foo(int bar)
{
int return_value = 0;
int something_done = 0;
int stuff_inited = 0;
int stuff_prepared = 0;
/*
* Prepare
*/
if (do_something(bar)) {
something_done = 1;
} else {
goto cleanup;
}
if (init_stuff(bar)) {
stuff_inited = 1;
} else {
goto cleanup;
}
if (prepare_stuff(bar)) {
stufF_prepared = 1;
} else {
goto cleanup;
}
/*
* Do the thing
*/
return_value = do_the_thing(bar);
/*
* Clean up
*/
cleanup:
if (stuff_prepared) {
unprepare_stuff();
}
if (stuff_inited) {
uninit_stuff();
}
if (something_done) {
undo_something();
}
return return_value;
}
_
他のいくつかの手法に対するこの利点の1つは、初期化関数の順序が変更された場合でも正しいクリーンアップが行われることです。たとえば、別の回答で説明されているswitch
メソッドを使用すると、初期化の変更の場合、最初に実際に初期化されなかった何かをクリーンアップしようとしないように、switch
を非常に慎重に編集する必要があります。
さて、このメソッドは多くの余分な変数を追加すると主張するかもしれません-実際、この場合は本当です-しかし実際には、既存の変数はすでに必要な状態をすでに追跡しているか、追跡することができます。たとえば、prepare_stuff()
が実際にmalloc()
またはopen()
の呼び出しである場合、返されたポインターまたはファイル記述子を保持する変数を使用できます。例:
_int fd = -1;
....
fd = open(...);
if (fd == -1) {
goto cleanup;
}
...
cleanup:
if (fd != -1) {
close(fd);
}
_
さらに、変数を使用してエラーステータスを追加で追跡すると、goto
を完全に回避し、初期化が必要になるほどインデントが深くなることなく、正しくクリーンアップできます。
_int foo(int bar)
{
int return_value = 0;
int something_done = 0;
int stuff_inited = 0;
int stuff_prepared = 0;
int oksofar = 1;
/*
* Prepare
*/
if (oksofar) { /* NB This "if" statement is optional (it always executes) but included for consistency */
if (do_something(bar)) {
something_done = 1;
} else {
oksofar = 0;
}
}
if (oksofar) {
if (init_stuff(bar)) {
stuff_inited = 1;
} else {
oksofar = 0;
}
}
if (oksofar) {
if (prepare_stuff(bar)) {
stuff_prepared = 1;
} else {
oksofar = 0;
}
}
/*
* Do the thing
*/
if (oksofar) {
return_value = do_the_thing(bar);
}
/*
* Clean up
*/
if (stuff_prepared) {
unprepare_stuff();
}
if (stuff_inited) {
uninit_stuff();
}
if (something_done) {
undo_something();
}
return return_value;
}
_
繰り返しますが、これには潜在的な批判があります。
if (oksofar)
チェックのシーケンスをクリーンアップコードへの1回のジャンプに最適化します(GCCは確かにそうです)-とにかく、通常、エラーケースはパフォーマンスにとってそれほど重要ではありません。これはさらに別の変数を追加していませんか?この場合、はい、しかし、多くの場合、_return_value
_変数を使用して、ここでoksofar
が果たしている役割を果たすことができます。一貫した方法でエラーを返すように関数を構成する場合、2番目のif
をそれぞれ回避することもできます。
_int return_value = 0;
if (!return_value) {
return_value = do_something(bar);
}
if (!return_value) {
return_value = init_stuff(bar);
}
if (!return_value) {
return_value = prepare_stuff(bar);
}
_
そのようなコーディングの利点の1つは、一貫性により、元のプログラマーが戻り値をチェックするのを忘れていた場所が痛い親指のように突き出て、バグ(の1つのクラス)を見つけやすくなることです。
したがって、これは(まだ)この問題を解決するために使用できるもう1つのスタイルです。正しく使用すると、非常にクリーンで一貫性のあるコードが可能になります-そして、他の手法と同様に、間違った手で長いコードを作成し混乱させるコードを生成することになります:-)
goto
キーワードの問題はほとんど誤解されています。それは平悪ではありません。すべてのgotoで作成する追加の制御パスに注意する必要があります。あなたのコードについて推論することが難しくなり、そのため、その妥当性がわかりません。
FWIW、developer.Apple.comチュートリアルを検索すると、エラー処理にgotoアプローチを使用します。
Gotoは使用しません。戻り値に高い重要性が置かれています。例外処理はsetjmp/longjmp
を介して行われます。
Gotoステートメントについて、道徳的に間違っているものは何もありません。(void)*ポインターには道徳的に間違っているものがあります。
ツールの使用方法がすべてです。あなたが提示した(些細な)ケースでは、caseステートメントはオーバーヘッドが増えますが、同じロジックを実現できます。本当の質問は、「私の速度要件は何ですか?」です。
特に短いジャンプにコンパイルするように注意している場合、gotoは単純に高速です。速度が重要なアプリケーションに最適です。他のアプリケーションでは、保守性のためにif/else + caseでオーバーヘッドヒットをとることがおそらく理にかなっています。
覚えておいてください:gotoはアプリケーションを殺すのではなく、開発者はアプリケーションを殺します。
更新:ケースの例を次に示します
int foo(int bar) {
int return_value = 0 ;
int failure_value = 0 ;
if (!do_something(bar)) {
failure_value = 1;
} else if (!init_stuff(bar)) {
failure_value = 2;
} else if (prepare_stuff(bar)) {
return_value = do_the_thing(bar);
cleanup_3();
}
switch (failure_value) {
case 2: cleanup_2();
case 1: cleanup_1();
default: break ;
}
}
GOTOは便利です。それはあなたのプロセッサができることであり、これがあなたがそれにアクセスする必要がある理由です。
関数にちょっとしたものを追加したい場合、gotoを1つ追加すれば簡単にできます。時間を節約できます。
一般に、コードの一部はgoto
を使用して最も明確に記述できるという事実を考慮します。 症状 プログラムの流れは一般的に望ましいよりも複雑である可能性が高いこと。 goto
の使用を避けるために、他のプログラム構造を奇妙な方法で組み合わせると、病気ではなく症状を治療しようとします。あなたの特定の例は、goto
なしで実装するのはそれほど難しくないかもしれません:
do { ..早期終了の場合にのみクリーンアップが必要なthing1を設定します if(error)break; do { ..早期終了の場合にクリーンアップが必要なthing2を設定します if(error)break; // *****この行に関するテキストを参照してください } while(0); .. cleanup thing2; } while(0); .. cleanup thing1;
しかし、関数が失敗したときにのみクリーンアップが行われることになっている場合、goto
ケースは、最初のターゲットラベルの直前にreturn
を置くことで処理できます。上記のコードでは、_*****
_でマークされた行にreturn
を追加する必要があります。
「通常の場合でもクリーンアップ」シナリオでは、goto
の使用がdo
/while(0)
構造よりも明確であると見なします。ラベル自体は、実際に「LOOK AT ME」と叫びます。break
およびdo
/while(0)
構造よりもはるかに大きくなります。エラーが発生した場合のみ、return
ステートメントは読みやすさの観点から可能な限り最悪の場所に配置する必要があります(returnステートメントは通常、関数の先頭か、 「終わり」のように);「ループ」の終わりの直前にラベルを付けるよりも、ターゲットラベルがその資格を満たす直前にreturn
を持っている方がはるかに簡単です。
ところで、複数のケースのコードが同じエラーコードを共有している場合、エラー処理にgoto
を時々使用する1つのシナリオは、switch
ステートメント内にあります。私のコンパイラは、多くの場合、複数のケースが同じコードで終了することを認識するのに十分なほどスマートですが、私が言う方が明確だと思います:
REPARSE_PACKET: switch(packet [0]) { case PKT_THIS_OPERATION: if(problem condition) goto PACKET_ERROR ; ... THIS_OPERATION break;を処理します。 case PKT_THAT_OPERATION: if(問題状態) goto PACKET_ERROR; 。 .. THAT_OPERATION break; ... case PKT_PROCESS_CONDITIONALLY if(packet_length <9) goto PACKET_ERROR; if(packet [4]を含むpacket_condition) { packet_length-= 5; memmove(packet、packet + 5、packet_length); goto REPARSE_PACKET; } else { packet [0] = PKT_CONDITION_SKIPPED; packet [4] = packet_length; packet_length = 5; packet_status = READY_TO_SEND; } break; ... d efault: { PACKET_ERROR: packet_error_count ++; packet_length = 4; packet [0] = PKT_ERROR; packet_status = READY_TO_SEND; break; } }
goto
ステートメントを{handle_error(); break;}
で置き換えることができ、do
/while(0)
ループをcontinue
とともに使用することもできますが、ラップされた条件付き実行パケットを処理するには、goto
を使用するよりも明確だとは本当に思いません。さらに、_PACKET_ERROR
_が使用されているすべての場所で_goto PACKET_ERROR
_からコードをコピーすることは可能かもしれませんが、コンパイラーは複製されたコードを一度書き出して、ほとんどの発生をその共有コピーへのジャンプに置き換えます、goto
を使用すると、パケットの設定が少し異なる場所に簡単に気付くことができます(「条件付きで実行」命令が実行しないと判断した場合など)。
質問で与えられた逆順のgotoクリーンアップが、ほとんどの機能でクリーンアップする最もクリーンな方法であることに同意します。しかし、とにかく関数をクリーンアップしたい場合があることも指摘したかったのです。これらの場合、if(0){label:}イディオムがクリーンアッププロセスの適切なポイントに移動する場合、次のバリアントを使用します。
int decode ( char * path_in , char * path_out )
{
FILE * in , * out ;
code c ;
int len ;
int res = 0 ;
if ( path_in == NULL )
in = stdin ;
else
{
if ( ( in = fopen ( path_in , "r" ) ) == NULL )
goto error_open_file_in ;
}
if ( path_out == NULL )
out = stdout ;
else
{
if ( ( out = fopen ( path_out , "w" ) ) == NULL )
goto error_open_file_out ;
}
if( read_code ( in , & c , & longueur ) )
goto error_code_construction ;
if ( decode_h ( in , c , out , longueur ) )
goto error_decode ;
if ( 0 ) { error_decode: res = 1 ;}
free_code ( c ) ;
if ( 0 ) { error_code_construction: res = 1 ; }
if ( out != stdout ) fclose ( stdout ) ;
if ( 0 ) { error_open_file_out: res = 1 ; }
if ( in != stdin ) fclose ( in ) ;
if ( 0 ) { error_open_file_in: res = 1 ; }
return res ;
}
私は個人的に "10の力-安全クリティカルコードを書くための10のルール"のフォロワーです 。
Gotoについて良いアイデアだと思うことを示す、そのテキストからの小さなスニペットを含めます。
ルール:すべてのコードを非常に単純な制御フロー構造に制限します。gotoステートメント、setjmpまたはlongjmp構造、および直接または間接再帰を使用しないでください。
理由:シンプルな制御フローは、より強力な検証機能に変換され、多くの場合、コードの明確性が向上します。再帰の追放は、おそらく最大の驚きです。ただし、再帰がなければ、非周期的な関数呼び出しグラフが保証されます。これは、コードアナライザーによって悪用される可能性があり、バインドされるべきすべての実行が実際にバインドされることを証明するのに直接役立ちます。 (このルールでは、すべての関数に単一のリターンポイントがある必要はないことに注意してください。ただし、多くの場合、制御フローも単純化されます。ただし、早期エラーリターンの方が簡単な場合もあります。)
Gotoの使用をやめる悪いようですしかし:
ルールが最初はドラコニアのように思える場合それらは、文字通り、あなたの人生がその正確さに依存するコードをチェックできるようにすることを意図していることを覚えておいてください。飛行、あなたが住んでいる場所から数マイル離れた原子力発電所、または宇宙飛行士を軌道に乗せる宇宙船。 ルールはあなたの車のシートベルトのように機能します。最初はおそらく少し不快ですが、しばらくすると、その使用は第二の性質になり、使用しないと想像できなくなります。
私が好んだものは次のとおりです。
bool do_something(void **ptr1, void **ptr2)
{
if (!ptr1 || !ptr2) {
err("Missing arguments");
return false;
}
bool ret = false;
//Pointers must be initialized as NULL
void *some_pointer = NULL, *another_pointer = NULL;
if (allocate_some_stuff(&some_pointer) != STUFF_OK) {
err("allocate_some_stuff step1 failed, abort");
goto out;
}
if (allocate_some_stuff(&another_pointer) != STUFF_OK) {
err("allocate_some_stuff step 2 failed, abort");
goto out;
}
void *some_temporary_malloc = malloc(1000);
//Do something with the data here
info("do_something OK");
ret = true;
// Assign outputs only on success so we don't end up with
// dangling pointers
*ptr1 = some_pointer;
*ptr2 = another_pointer;
out:
if (!ret) {
//We are returning an error, clean up everything
//deallocate_some_stuff is a NO-OP if pointer is NULL
deallocate_some_stuff(some_pointer);
deallocate_some_stuff(another_pointer);
}
//this needs to be freed every time
free(some_temporary_malloc);
return ret;
}
cleanup_3
がクリーンアップを行い、cleanup_2
を呼び出す必要があるように思えます。同様に、cleanup_2
はクリーンアップを実行してからcleanup_1を呼び出す必要があります。 cleanup_[n]
を行うときはいつでも、cleanup_[n-1]
が必要であるように見えるため、メソッドの責任である必要があります(たとえば、cleanup_3
は、cleanup_2
およびリークの可能性があります。)
そのアプローチを考えると、gotoの代わりに、クリーンアップルーチンを呼び出してから戻るだけです。
goto
アプローチは、間違ったまたは悪いではありませんが、必ずしも「最もクリーンな」アプローチ(IMHO)ではないことに注意する価値があります。
最適なパフォーマンスを探しているなら、goto
解が最適だと思います。ただし、一部のパフォーマンスが重要な一部のアプリケーション(デバイスドライバー、組み込みデバイスなど)にのみ関連することを期待しています。それ以外の場合は、コードの明快さよりも優先度が低いマイクロ最適化です。
ここでの質問は、与えられたコードに関して間違っていると思います。
考慮してください:
したがって、do_something()、init_stuff()、prepare_stuff()は独自のクリーンアップを行うである必要があります。 do_something()の後にクリーンアップする別のcleanup_1()関数があると、カプセル化の哲学が崩れます。それは悪いデザインです。
独自のクリーンアップを行った場合、foo()は非常に単純になります。
一方。 foo()が実際に破棄する必要のある独自の状態を作成した場合、gotoが適切です。