ループと関数をサポートする言語で 'goto' を使用すると有利なことはありますか?もしそうなら、なぜですか?
-
09-06-2019 - |
質問
という印象をずっと抱いていました goto
可能であれば決して使用しないでください。先日、libavcodec (C で書かれている) をよく読んでいたときに、それが複数使用されていることに気づきました。利用すると有利になることはありますか goto
ループと関数をサポートする言語で?もしそうなら、なぜですか?
解決
私が知っている限り、「goto」ステートメントを使用する理由はいくつかあります (すでにこれについて話している人もいます)。
関数をきれいに終了する
多くの場合、関数ではリソースを割り当て、複数の場所で終了する必要があります。プログラマーは、リソースのクリーンアップ コードを関数の最後に置くことでコードを簡素化でき、関数のすべての「終了ポイント」がクリーンアップ ラベルに移動します。こうすることで、関数のすべての「終了点」でクリーンアップ コードを記述する必要がなくなります。
ネストされたループを終了する
ネストされたループに陥っていて、そこから抜け出す必要がある場合 全て ループの場合、goto を使用すると、break ステートメントや if チェックよりもはるかにクリーンで簡単になります。
低レベルのパフォーマンスの向上
これはパフォーマンスが重要なコードでのみ有効ですが、goto ステートメントは非常に高速に実行されるため、関数内を移動する際の速度が向上します。ただし、コンパイラは通常、goto を含むコードを最適化できないため、これは両刃の剣です。
これらすべての例で、goto は 1 つの関数のスコープに制限されていることに注意してください。
他のヒント
アンチの人みんなgoto
直接的または間接的に、エドガー・ダイクストラの論文を引用します。 GoToは有害と考えられる 彼らの立場を裏付ける記事。残念なことに、ダイクストラ氏の記事は実質的に 何もない 道に関わること goto
ステートメントは最近使用されているため、この記事で述べられていることは、現代のプログラミングシーンにはほとんど、またはまったく適用できません。の goto
レス・ミームは現在、上から命じられた経典、大祭司、そして異端者とみなされる人々の(あるいはさらにひどい)忌避に至るまで、宗教に近づいている。
この主題に少し光を当てるために、ダイクストラの論文を文脈に入れてみましょう。
ダイクストラが論文を書いたとき、当時人気のあった言語は、BASIC、FORTRAN (初期の方言)、さまざまなアセンブリ言語などの非構造化手続き型言語でした。高水準言語を使用する人々がジャンプするのは非常に一般的でした コードベース全体にわたって 「スパゲッティ コード」という用語を生み出した、ねじれ、歪んだ実行の糸です。に飛び乗ればこれを見ることができます 古典的なトレック ゲーム マイク・メイフィールドによって書かれ、物事がどのように機能するかを理解しようとしています。少し時間をとってそれを見てください。
これ それは、ダイクストラが1968年の論文で激しく非難していた「go to声明の無制限の使用」である。 これ それは、彼がその論文を書くきっかけとなった、彼が住んでいた環境です。コード内の好きな場所に好きなときにジャンプできる機能は、彼が批判し、中止を要求していたものでした。それを貧血の力と比較すると、 goto
C やその他のより現代的な言語では、単純に可能です。
異端者と対峙する狂信者たちの高らかに叫ぶ声がすでに聞こえています。「しかし、」と彼らは唱えるでしょう、「これを使用するとコードが非常に読みにくくなる可能性があります」 goto
Cで」「そうそう?そうしないと、コードが非常に読みにくくなる可能性があります。 goto
同じように。このように:
#define _ -F<00||--F-OO--;
int F=00,OO=00;main(){F_OO();printf("%1.3f\n",4.*-F/OO/OO);}F_OO()
{
_-_-_-_
_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_-_-_-_-_
_-_-_-_-_-_-_-_
_-_-_-_
}
ではありません goto
目に見えるところにあるので、読みやすいはずですよね?あるいは、これはどうでしょうか。
a[900]; b;c;d=1 ;e=1;f; g;h;O; main(k,
l)char* *l;{g= atoi(* ++l); for(k=
0;k*k< g;b=k ++>>1) ;for(h= 0;h*h<=
g;++h); --h;c=( (h+=g>h *(h+1)) -1)>>1;
while(d <=g){ ++O;for (f=0;f< O&&d<=g
;++f)a[ b<<5|c] =d++,b+= e;for( f=0;f<O
&&d<=g; ++f)a[b <<5|c]= d++,c+= e;e= -e
;}for(c =0;c<h; ++c){ for(b=0 ;b<k;++
b){if(b <k/2)a[ b<<5|c] ^=a[(k -(b+1))
<<5|c]^= a[b<<5 |c]^=a[ (k-(b+1 ))<<5|c]
;printf( a[b<<5|c ]?"%-4d" :" " ,a[b<<5
|c]);} putchar( '\n');}} /*Mike Laman*/
いいえ goto
そこにもある。したがって、読み取れるものでなければなりません。
これらの例で私が言いたいことは何でしょうか?コードを読みにくく、保守しにくくするのは、言語機能ではありません。それを行うのは構文ではありません。これを引き起こすのは悪いプログラマーです。そして、上記の項目でわかるように、悪いプログラマは次のようなことを行う可能性があります。 どれでも 言語機能が読み取れず、使用できません。以下のような for
そこにループします。(見えますよね?)
公平を期すために言うと、一部の言語構造は他の構造よりも悪用しやすいものがあります。しかし、もしあなたが C プログラマーであれば、私は、 #define
私が十字軍に向かうずっと前に goto
!
したがって、ここまで読んでくださった方のために、いくつかの重要な点に注意してください。
- ダイクストラの論文
goto
ステートメントはプログラミング環境向けに書かれています。goto
でした 多くアセンブラではない最新の言語よりも有害な可能性が高くなります。 - すべての使用を自動的に破棄します。
goto
このため、「一度楽しんでみようとしたが、気に入らなかったので、今は反対」と言っているのと同じくらい合理的です。 - 現代(貧血)の正当な使用法があります
goto
他のコンストラクトで適切に置き換えることができないコードのステートメント。 - もちろん、同じステートメントの違法な使用も存在します。
- また、「」のような最新の制御ステートメントの不正な使用もあります。
godo
「常に偽りが存在する忌まわしいこと」do
ループが使用不能になったbreak
の代わりにgoto
. 。これらは多くの場合、賢明に使用するよりも悪いものになります。goto
.
ベスト プラクティスに盲目的に従うことはベスト プラクティスではありません。避けるという考え方 goto
フロー制御の主な形式としてステートメントを使用することは、判読できないスパゲッティ コードの生成を避けることです。適切な場所で控えめに使用すれば、アイデアを最も単純かつ明確に表現できる方法になる場合があります。Zortech C++ コンパイラと D プログラミング言語の作成者である Walter Bright は、それらを頻繁に、しかし賢明に使用しています。たとえ goto
ステートメントを削除しても、彼のコードは依然として完全に読み取れます。
結論:避ける goto
避けるために goto
無意味です。本当に避けたいのは、判読できないコードが生成されることです。もしあなたの goto
-loaded コードが読み取れる場合は、何も問題ありません。
以来 goto
プログラムの流れについての推論が困難になる1 (別名。「スパゲッティコード」)、 goto
通常、欠落している機能を補うためにのみ使用されます。の用法 goto
実際には受け入れられるかもしれませんが、それは言語が同じ目的を達成するためのより構造化されたバリアントを提供していない場合に限ります。ダウトの例を見てみましょう。
私たちが使用する goto のルールは、関数内の 1 つの出口クリーンアップ ポイントに前方にジャンプする場合は goto が問題ないということです。
これは当てはまりますが、言語がクリーンアップ コード (RAII や finally
)、同じジョブをより適切に実行する場合 (それを実行するために特別に構築されているため)、または構造化例外処理を使用しない正当な理由がある場合 (ただし、非常に低レベルの場合を除いて、このケースは発生しません)。
他のほとんどの言語では、唯一許容される使用法は、 goto
ネストされたループを終了することです。そしてそこでさえ、ほとんどの場合、外側のループを独自のメソッドに持ち上げて使用する方が良いでしょう。 return
その代わり。
それ以外、 goto
これは、コードの特定の部分について十分な検討が行われていないことを示しています。
1 サポートする現代言語 goto
いくつかの制限を実装します(例: goto
関数にジャンプしたり関数から抜けたりすることはできない)が、問題は基本的に同じままです。
ちなみに、同じことはもちろん、他の言語機能にも当てはまりますが、最も顕著なのは例外です。また、通常は、例外ではないプログラム フローを制御するために例外を使用しないというルールなど、指定された場合にのみこれらの機能を使用するための厳格なルールが定められています。
まあ、それよりも常に悪いことが 1 つあります goto's
;goto を避けるための他のプログラムフロー演算子の奇妙な使用:
例:
// 1
try{
...
throw NoErrorException;
...
} catch (const NoErrorException& noe){
// This is the worst
}
// 2
do {
...break;
...break;
} while (false);
// 3
for(int i = 0;...) {
bool restartOuter = false;
for (int j = 0;...) {
if (...)
restartOuter = true;
if (restartOuter) {
i = -1;
}
}
etc
etc
で C# スイッチ 声明 フォールスルーを許さない. 。それで 後藤 特定のスイッチケース ラベルまたは デフォルト ラベル。
例えば:
switch(value)
{
case 0:
Console.Writeln("In case 0");
goto case 1;
case 1:
Console.Writeln("In case 1");
goto case 2;
case 2:
Console.Writeln("In case 2");
goto default;
default:
Console.Writeln("In default");
break;
}
編集:「フォールスルーなし」ルールには例外が 1 つあります。case ステートメントにコードがない場合、フォールスルーが許可されます。
#ifdef TONGUE_IN_CHEEK
パールには、 goto
これにより、貧乏人の末尾呼び出しを実装できるようになります。:-P
sub factorial {
my ($n, $acc) = (@_, 1);
return $acc if $n < 1;
@_ = ($n - 1, $acc * $n);
goto &factorial;
}
#endif
まあ、それはCとは関係ありません goto
. 。もっと真剣に、私は使用に関する他のコメントに同意します goto
クリーンアップまたは実装のために ダフの装置, 、など。すべては悪用ではなく使用することです。
(同じコメントが次の場合にも当てはまります) longjmp
, 、例外、 call/cc
, 、など - これらには正当な用途がありますが、簡単に悪用される可能性があります。たとえば、完全に例外ではない状況で、深くネストされた制御構造を回避するためだけに例外をスローします。)
私は長年にわたってアセンブリ言語を数行以上書いてきました。最終的には、すべての高級言語は goto にコンパイルされます。わかりました。それらを「ブランチ」または「ジャンプ」などと呼んでも構いませんが、それらは goto です。goto なしのアセンブラを書ける人はいますか?
Fortran、C、または BASIC のプログラマーに、goto で riot を実行するのはスパゲッティ ボロネーズのレシピであると指摘できます。ただし、答えはそれらを避けることではなく、慎重に使用することです。
ナイフは食事を準備したり、誰かを解放したり、誰かを殺したりするために使用できます。後者を恐れてナイフなしで生活するのでしょうか?同様にgoto:不用意に使えば邪魔になるが、慎重に使えば助けになる。
を見てみましょう C でプログラミングする場合に Goto を使用する場合:
goto の使用は、ほとんどの場合悪いプログラミング手法です (XYZ を実行するより良い方法がきっと見つかるはずです) が、実際には悪い選択ではない場合もあります。それが役立つ場合には、それが最良の選択であると主張する人もいるかもしれません。
goto について私が言わなければならないことのほとんどは、実際には C にのみ当てはまります。C++ を使用している場合、例外の代わりに goto を使用する正当な理由はありません。ただし、C には例外処理メカニズムの力がないため、エラー処理をプログラム ロジックの残りの部分から分離し、コード全体でクリーンアップ コードを何度も書き直すことを避けたい場合は、次のようにします。その場合は goto が良い選択になります。
どういう意味ですか?次のようなコードがあるかもしれません。
int big_function()
{
/* do some work */
if([error])
{
/* clean up*/
return [error];
}
/* do some more work */
if([error])
{
/* clean up*/
return [error];
}
/* do some more work */
if([error])
{
/* clean up*/
return [error];
}
/* do some more work */
if([error])
{
/* clean up*/
return [error];
}
/* clean up*/
return [success];
}
クリーンアップ コードを変更する必要があることに気づくまでは、これで問題ありません。次に、4 つの変更を行う必要があります。ここで、すべてのクリーンアップを 1 つの関数にカプセル化するだけでよいと判断するかもしれません。それは悪い考えではありません。ただし、ポインタには注意する必要があるということです。クリーンアップ関数でポインタを解放する予定がある場合、ポインタをポインタに渡さない限り、ポインタを NULL に設定する方法はありません。多くの場合、いずれにしてもそのポインターを再び使用することはないので、それは大きな問題ではないかもしれません。一方、新しいポインタ、ファイル ハンドル、またはクリーンアップが必要なものを追加した場合は、クリーンアップ関数を再度変更する必要があります。次に、その関数の引数を変更する必要があります。
を使用することで goto
, 、 そうなる
int big_function()
{
int ret_val = [success];
/* do some work */
if([error])
{
ret_val = [error];
goto end;
}
/* do some more work */
if([error])
{
ret_val = [error];
goto end;
}
/* do some more work */
if([error])
{
ret_val = [error];
goto end;
}
/* do some more work */
if([error])
{
ret_val = [error];
goto end;
}
end:
/* clean up*/
return ret_val;
}
ここでの利点は、後続のコードがクリーンアップを実行するために必要なものすべてにアクセスでき、変更点の数を大幅に減らすことができることです。もう 1 つの利点は、関数の出口ポイントが複数あったのが 1 つだけになったことです。誤ってクリーンアップせずに関数から戻ってしまう可能性はありません。
さらに、goto は 1 つのポイントにジャンプするためにのみ使用されているため、関数呼び出しをシミュレートするために前後にジャンプする大量のスパゲッティ コードを作成しているわけではありません。むしろ、goto はより構造化されたコードを書くのに役立ちます。
一言で、 goto
常に慎重に、最後の手段として使用する必要があります。しかし、それを使用する時と場所があります。問題は、「それを使用しなければならないか」ではなく、それを使用することが「最良の選択か」であるべきです。
goto がダメな理由の 1 つは、コーディング スタイル以外に、これを使用して次のようなことができることです。 重複, 、 しかし 入れ子になっていない ループ:
loop1:
a
loop2:
b
if(cond1) goto loop1
c
if(cond2) goto loop2
これにより、(a、b、c、b、a、b、a、b、...) のようなシーケンスが可能となる、奇妙ではあるがおそらく合法的な制御フロー構造が作成され、コンパイラ ハッカーは不満を感じます。どうやら、このタイプの構造が発生しないことに依存する、賢い最適化トリックが多数存在します。(ドラゴンブックのコピーを確認する必要があります...) この結果、(一部のコンパイラを使用すると) 以下のコードを含むコードに対して他の最適化が行われない可能性があります。 goto
s.
もしそうなら役立つかもしれません 知る それは、「ああ、ところで」たまたまコンパイラーに高速なコードを出力させるだけです。個人的には、goto のようなトリックを使用する前に、何があり得るのか、何がそうでないのかをコンパイラに説明したいと考えていますが、おそらく、試してみることもできるでしょう。 goto
アセンブラをハックする前に。
goto が許容されるケースのリストまで挙げて、他のすべての使用は許容されないと言う人がいるのは面白いと思います。アルゴリズムを表現するために goto が最良の選択であるすべてのケースを知っていると本当に思いますか?
説明のために、ここではまだ誰も示していない例を示します。
今日はハッシュ テーブルに要素を挿入するコードを書いていました。ハッシュ テーブルは以前の計算のキャッシュであり、自由に上書きできます (パフォーマンスには影響しますが、正確さには影響しません)。
ハッシュ テーブルの各バケットには 4 つのスロットがあり、バケットがいっぱいになったときにどの要素を上書きするかを決定するための基準がたくさんあります。現時点では、これは次のようにバケットを介して最大 3 つのパスを作成することを意味します。
// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
if (slot_p[add_index].hash_key == hash_key)
goto add;
// Otherwise, find first empty element
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
goto add;
// Additional passes go here...
add:
// element is written to the hash table here
goto を使用しなかったら、このコードはどうなるでしょうか?
このようなもの:
// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
if (slot_p[add_index].hash_key == hash_key)
break;
if (add_index >= ELEMENTS_PER_BUCKET) {
// Otherwise, find first empty element
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
break;
if (add_index >= ELEMENTS_PER_BUCKET)
// Additional passes go here (nested further)...
}
// element is written to the hash table here
さらにパスが追加されると見た目はますます悪くなりますが、goto を使用したバージョンでは常に同じインデント レベルが維持され、前のループの実行によって結果が暗示される偽の if ステートメントの使用が回避されます。
つまり、goto によってコードがクリーンになり、記述しやすくなり、理解しやすくなる別のケースもあります...他にもたくさんあると思うので、goto が役立つすべてのケースを知っているふりをして、思いつかなかった良いケースを軽視しないでください。
私たちが使用する goto のルールは、関数内の 1 つの出口クリーンアップ ポイントに前方にジャンプする場合は goto が問題ないということです。本当に複雑な関数では、他の関数が前にジャンプできるようにそのルールを緩和します。どちらの場合も、エラー コード チェックで頻繁に発生する深くネストされた if ステートメントを回避しているため、読みやすさと保守が容易になります。
goto ステートメント、その正当な使用法、および「有益な goto ステートメント」の代わりに使用できるが、goto ステートメントと同じくらい簡単に悪用できる代替構造について、最も思慮深く徹底的に議論しているのは、Donald Knuth の記事です。goto ステートメントを使用した構造化プログラミング」、1974 年 12 月の Computing Surveys (volume 6、no.4.pp.261 - 301)。
当然のことながら、この 39 年前の論文には古い部分もあります。処理能力が桁違いに向上したため、中規模の問題では Knuth のパフォーマンス向上の一部が目立たなくなり、それ以来、新しいプログラミング言語構造が発明されてきました。(たとえば、try-catch ブロックは Zahn の Construct を包含しますが、そのように使用されることはほとんどありません。) しかし、Knuth は議論のあらゆる側面をカバーしているため、誰かがこの問題を再び蒸し返す前に必ず読む必要があります。
Perl モジュールでは、サブルーチンやクロージャをオンザフライで作成したい場合があります。問題は、サブルーチンを作成した後、どのようにしてそれに到達するかということです。ただ呼び出すこともできますが、サブルーチンが caller()
それはあまり役に立ちません。そこで、 goto &subroutine
バリエーションが役立つ場合があります。
以下に簡単な例を示します。
sub AUTOLOAD{
my($self) = @_;
my $name = $AUTOLOAD;
$name =~ s/.*:://;
*{$name} = my($sub) = sub{
# the body of the closure
}
goto $sub;
# nothing after the goto will ever be executed.
}
この形式も使用できます goto
末尾呼び出し最適化の基本的な形式を提供します。
sub factorial($){
my($n,$tally) = (@_,1);
return $tally if $n <= 1;
$tally *= $n--;
@_ = ($n,$tally);
goto &factorial;
}
( で Perl 5 バージョン 16 それは次のように書いた方がよいでしょう goto __SUB__;
)
をインポートするモジュールがあります tail
モディファイアとインポートするモディファイア recur
この形式を使用したくない場合は、 goto
.
use Sub::Call::Tail;
sub AUTOLOAD {
...
tail &$sub( @_ );
}
use Sub::Call::Recur;
sub factorial($){
my($n,$tally) = (@_,1);
return $tally if $n <= 1;
recur( $n-1, $tally * $n );
}
その他の使用理由のほとんどは、 goto
他のキーワードを使用した方がよいでしょう。
のように redo
少しコードを書きます:
LABEL: ;
...
goto LABEL if $x;
{
...
redo if $x;
}
またはに行く last
複数の場所からのコードの一部:
goto LABEL if $x;
...
goto LABEL if $y;
...
LABEL: ;
{
last if $x;
...
last if $y
...
}
もしそうなら、なぜですか?
C にはマルチレベル/ラベル付きブレークがなく、すべての制御フローが C の反復および決定プリミティブで簡単にモデル化できるわけではありません。goto は、これらの欠陥を修正するのに大いに役立ちます。
場合によっては、ある種のフラグ変数を使用して、一種の疑似マルチレベル ブレークを実行する方が明確な場合もありますが、常に goto よりも優れているわけではありません (少なくとも、goto を使用すると、フラグ変数とは異なり、制御がどこに行くかを簡単に決定できます) )、また、goto を回避するためにフラグやその他のゆがみのパフォーマンスの代償を払いたくない場合もあります。
libavcodec はパフォーマンスに依存するコードです。制御フローを直接表現することは、より適切に実行される傾向があるため、おそらく優先事項です。
同様に、誰も「COME FROM」ステートメントを実装したことがありません。
do{} while(false) の使用法はまったく不快なものだと思います。特殊なケースでは必要であると思われるかもしれませんが、それがクリーンな賢明なコードであるとは決して言えません。
このようなループを実行する必要がある場合は、フラグ変数への依存関係を明示的にしないのはなぜでしょうか。
for (stepfailed=0 ; ! stepfailed ; /*empty*/)
もちろん GOTO を使用することもできますが、コード スタイルよりも重要なことが 1 つあります。コードが読みやすいかどうかは、GOTO を使用するときに念頭に置く必要があります。 内部のコードはあなたが思っているほど堅牢ではない可能性があります.
たとえば、次の 2 つのコード スニペットを見てください。
If A <> 0 Then A = 0 EndIf
Write("Value of A:" + A)
GOTOを使用した同等のコード
If A == 0 Then GOTO FINAL EndIf
A = 0
FINAL:
Write("Value of A:" + A)
最初に考えられるのは、両方のコードの結果が「A の値:」になるということです。0" (もちろん、並列処理なしの実行を想定しています)
それは正しくありません:最初のサンプルでは、A は常に 0 になりますが、2 番目のサンプル (GOTO ステートメントを使用) では、A は 0 にならない可能性があります。なぜ?
その理由は、プログラムの別のポイントから、 GOTO FINAL
A の値を制御せずに。
この例は非常に明白ですが、プログラムが複雑になると、そのようなものを見るのが難しくなります。
関連資料は、氏の有名な記事にあります。ディクストラ 「GO TO宣言に対する訴訟」
1) 私が知る限りの goto の最も一般的な使用法は、goto を提供していない言語、つまり C で例外処理をエミュレートすることです。(上記の Nuclear によって与えられたコードはまさにそのコードです。) Linux のソース コードを見ると、そのように使用されている膨大な goto がわかります。2013 年に実施された簡単な調査によると、Linux コードには約 100,000 の goto がありました。 http://blog.regehr.org/archives/894. 。Goto の使用法は、Linux コーディング スタイル ガイドにも記載されています。 https://www.kernel.org/doc/Documentation/CodingStyle. 。オブジェクト指向プログラミングが関数ポインターが設定された構造体を使用してエミュレートされるのと同じように、goto は C プログラミングでもその役割を果たします。では、誰が正しいのか:Dijkstra または Linus (およびすべての Linux カーネル プログラマー)?それは理論 vs.基本的に練習します。
ただし、コンパイラ レベルのサポートがないため、一般的な構造/パターンをチェックできないという、よくある問題があります。コンパイル時のチェックを行わずにそれらを間違って使用し、バグを引き起こす可能性が高くなります。Windows と Visual C++ (ただし C モードでは) は、まさに次の理由から SEH/VEH を介した例外処理を提供します。例外は OOP 言語の外でも役立ちます。手続き型言語で。ただし、コンパイラは、言語内の例外に対する構文サポートを提供している場合でも、常にベーコンを保存できるわけではありません。後者の例として、有名な Apple SSL の「goto 失敗」バグを考えてみましょう。このバグは、1 つの goto を重複させただけで悲惨な結果をもたらしました (https://www. Imperialviolet.org/2014/02/22/applebug.html):
if (something())
goto fail;
goto fail; // copypasta bug
printf("Never reached\n");
fail:
// control jumps here
コンパイラでサポートされている例外を使用すると、まったく同じバグが発生する可能性があります。C++ では:
struct Fail {};
try {
if (something())
throw Fail();
throw Fail(); // copypasta bug
printf("Never reached\n");
}
catch (Fail&) {
// control jumps here
}
ただし、コンパイラが到達不能なコードを分析して警告すれば、どちらのバグも回避できます。たとえば、/W4 警告レベルで Visual C++ をコンパイルすると、どちらの場合でもバグが見つかります。たとえば、Java は、かなり正当な理由から、到達不能なコード (コードが見つかる場合) を禁止します。それは平均的な Joe のコードのバグである可能性があります。goto 構造が、計算されたアドレス (**) への goto など、コンパイラーが簡単に理解できないターゲットを許可しない限り、コンパイラーが goto を使用して関数内で到達不能なコードを見つけることは、ダイクストラを使用するよりも難しくありません。 -承認されたコード。
(**) 脚注:計算された行番号への移動は、Basic の一部のバージョンで可能です。GOTO 10*x ここで、x は変数です。かなり紛らわしいことに、Fortran では「計算された goto」は、C の switch ステートメントに相当する構造を指します。標準 C では、言語内で計算された goto は許可されませんが、静的/構文的に宣言されたラベルへの goto のみが許可されます。ただし、GNU C には、ラベルのアドレス (単項接頭辞 && 演算子) を取得するための拡張機能があり、void* 型の変数への goto も許可されます。見る https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html このあいまいなサブトピックについて詳しくは、こちらをご覧ください。この投稿の残りの部分は、そのあいまいな GNU C 機能には関係しません。
標準 C (すなわち、計算されません) goto は、通常、コンパイル時に到達不能なコードが見つからない理由ではありません。通常の理由は、次のようなロジック コードです。与えられた
int computation1() {
return 1;
}
int computation2() {
return computation1();
}
コンパイラが次の 3 つの構造のいずれかで到達不能なコードを見つけることも同様に困難です。
void tough1() {
if (computation1() != computation2())
printf("Unreachable\n");
}
void tough2() {
if (computation1() == computation2())
goto out;
printf("Unreachable\n");
out:;
}
struct Out{};
void tough3() {
try {
if (computation1() == computation2())
throw Out();
printf("Unreachable\n");
}
catch (Out&) {
}
}
(中括弧関連のコーディング スタイルを申し訳ありませんが、例をできるだけコンパクトに保つよう努めました。)
Visual C++ /W4 (/Ox を使用した場合でも) は、これらのいずれのコードでも到達不能なコードを見つけることができません。ご存知のとおり、到達不能なコードを見つけるという問題は一般に判断がつきません。(これが信じられないなら: https://www.cl.cam.ac.uk/teaching/2006/OptComp/slides/lecture02.pdf)
関連する問題として、C の goto は関数の本体内でのみ例外をエミュレートするために使用できます。標準 C ライブラリは、非ローカル終了/例外をエミュレートするための関数 setjmp() と longjmp() のペアを提供しますが、これらには他の言語が提供するものと比較して重大な欠点がいくつかあります。ウィキペディアの記事 http://en.wikipedia.org/wiki/Setjmp.h この後者の問題についてはかなりよく説明されています。この関数ペアは Windows でも動作します (http://msdn.microsoft.com/en-us/library/yz2ez4as.aspx) ですが、SEH/VEH の方が優れているため、そこで使用する人はほとんどいません。Unixでもsetjmpやlongjmpはあまり使われないと思います。
2) C での goto の 2 番目に一般的な使用法は、マルチレベルの Break またはマルチレベルの continue の実装だと思いますが、これもかなり議論の余地のない使用例です。Java では goto ラベルは許可されませんが、break label または continue label は許可されることを思い出してください。によると http://www.oracle.com/technetwork/java/simple-142616.html, 、これは実際には C での goto の最も一般的な使用例 (90% と言われています) ですが、私の主観的な経験では、システム コードはエラー処理に goto をより頻繁に使用する傾向があります。おそらく科学コードや OS が例外処理を提供する場所 (Windows) では、マルチレベルの終了が主な使用例となります。彼らは調査の内容についてはまったく詳細を明らかにしていません。
編集して以下を追加しました:これら 2 つの使用パターンは、Kernighan と Ritchie の C ブックの 60 ページあたりにあることがわかりました (版によって異なります)。もう 1 つの注目すべき点は、どちらのユースケースにも前方 goto のみが含まれることです。そして、MISRA C 2012 版 (2004 版とは異なり) では、前方の goto のみである限り、goto が許可されることがわかりました。
Perl では、ラベルを使用してループから「goto」します。これは、break に似た「last」ステートメントを使用します。
これにより、ネストされたループをより適切に制御できるようになります。
伝統的な後藤 ラベル もサポートされていますが、これが目的を達成する唯一の方法である例がそれほど多くないかはわかりません。ほとんどの場合、サブルーチンとループで十分です。
「goto」の問題、そして「goto なしプログラミング」運動の最も重要な議論は、これを頻繁に使用しすぎると、コードは正しく動作する可能性があっても、読めなくなったり、保守できなくなったり、レビューできなくなったりすることです。99.99% の場合、「goto」はスパゲッティ コードにつながります。個人的には、なぜ「goto」を使用するのかについては、正当な理由が思いつきません。
この分野で多大な貢献をしたコンピューター科学者のエドガー・ダイクストラ氏も、GoToの利用を批判したことで有名だった。彼の議論についての短い記事があります ウィキペディア.
次の場合に goto を使用します。別の場所の関数から戻る必要があり、戻る前に初期化を解除する必要がある場合:
goto 以外のバージョン:
int doSomething (struct my_complicated_stuff *ctx)
{
db_conn *conn;
RSA *key;
char *temp_data;
conn = db_connect();
if (ctx->smth->needs_alloc) {
temp_data=malloc(ctx->some_size);
if (!temp_data) {
db_disconnect(conn);
return -1;
}
}
...
if (!ctx->smth->needs_to_be_processed) {
free(temp_data);
db_disconnect(conn);
return -2;
}
pthread_mutex_lock(ctx->mutex);
if (ctx->some_other_thing->error) {
pthread_mutex_unlock(ctx->mutex);
free(temp_data);
db_disconnect(conn);
return -3;
}
...
key=rsa_load_key(....);
...
if (ctx->something_else->error) {
rsa_free(key);
pthread_mutex_unlock(ctx->mutex);
free(temp_data);
db_disconnect(conn);
return -4;
}
if (ctx->something_else->additional_check) {
rsa_free(key);
pthread_mutex_unlock(ctx->mutex);
free(temp_data);
db_disconnect(conn);
return -5;
}
pthread_mutex_unlock(ctx->mutex);
free(temp_data);
db_disconnect(conn);
return 0;
}
バージョンに移動:
int doSomething_goto (struct my_complicated_stuff *ctx)
{
int ret=0;
db_conn *conn;
RSA *key;
char *temp_data;
conn = db_connect();
if (ctx->smth->needs_alloc) {
temp_data=malloc(ctx->some_size);
if (!temp_data) {
ret=-1;
goto exit_db;
}
}
...
if (!ctx->smth->needs_to_be_processed) {
ret=-2;
goto exit_freetmp;
}
pthread_mutex_lock(ctx->mutex);
if (ctx->some_other_thing->error) {
ret=-3;
goto exit;
}
...
key=rsa_load_key(....);
...
if (ctx->something_else->error) {
ret=-4;
goto exit_freekey;
}
if (ctx->something_else->additional_check) {
ret=-5;
goto exit_freekey;
}
exit_freekey:
rsa_free(key);
exit:
pthread_mutex_unlock(ctx->mutex);
exit_freetmp:
free(temp_data);
exit_db:
db_disconnect(conn);
return ret;
}
2 番目のバージョンでは、割り当て解除ステートメント (コード内でそれぞれ 1 回ずつ使用されます) で何かを変更する必要がある場合が簡単になり、新しいブランチを追加するときにそれらをスキップする可能性が減ります。割り当て解除はさまざまな「レベル」で実行できるため、関数内でそれらを移動してもここでは役に立ちません。
C++ では goto を使用する理由はないと言う人もいます。99%の場合、より良い代替手段があるという人もいます。 これは理屈ではなく、ただの非合理的な感想です。 以下は、goto が強化された do-while ループのような優れたコードにつながる確実な例です。
int i;
PROMPT_INSERT_NUMBER:
std::cout << "insert number: ";
std::cin >> i;
if(std::cin.fail()) {
std::cin.clear();
std::cin.ignore(1000,'\n');
goto PROMPT_INSERT_NUMBER;
}
std::cout << "your number is " << i;
goto フリーのコードと比較してください。
int i;
bool loop;
do {
loop = false;
std::cout << "insert number: ";
std::cin >> i;
if(std::cin.fail()) {
std::cin.clear();
std::cin.ignore(1000,'\n');
loop = true;
}
} while(loop);
std::cout << "your number is " << i;
次のような違いが見られます。
- 入れ子になった
{}
ブロックは必要です(とはいえ、do {...} while
見慣れたものに見えます) - 余分な
loop
変数が必要です。4 か所で使用されます - 作品を読んで理解するには、より長い時間がかかります。
loop
- の
loop
データを保持せず、実行のフローを制御するだけであり、単純なラベルよりも理解しにくいです。
別の例もあります
void sort(int* array, int length) {
SORT:
for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
swap(data[i], data[i+1]);
goto SORT; // it is very easy to understand this code, right?
}
}
さあ、「邪悪な」goto を取り除きましょう。
void sort(int* array, int length) {
bool seemslegit;
do {
seemslegit = true;
for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
swap(data[i], data[i+1]);
seemslegit = false;
}
} while(!seemslegit);
}
これは goto を使用するのと同じタイプであり、よく構造化されたパターンであり、多くの場合唯一推奨される方法である forward goto ではありません。確かに、次のような「スマートな」コードは避けたいでしょう。
void sort(int* array, int length) {
for(int i=0; i<length-1; ++i) if(array[i]>array[i+1]) {
swap(data[i], data[i+1]);
i = -1; // it works, but WTF on the first glance
}
}
重要なのは、goto は簡単に悪用される可能性があるが、goto 自体が悪いわけではないということです。C++ では label には関数スコープがあるため、純粋なアセンブリのようにグローバル スコープを汚染しないことに注意してください。 重複するループ 8051 の次のコードのように、7 セグメント ディスプレイが P1 に接続されています。プログラムは稲妻セグメントをループします。
; P1 states loops
; 11111110 <-
; 11111101 |
; 11111011 |
; 11110111 |
; 11101111 |
; 11011111 |
; |_________|
init_roll_state:
MOV P1,#11111110b
ACALL delay
next_roll_state:
MOV A,P1
RL A
MOV P1,A
ACALL delay
JNB P1.5, init_roll_state
SJMP next_roll_state
もう 1 つの利点があります。goto は、名前付きループ、条件、その他のフローとして機能します。
if(valid) {
do { // while(loop)
// more than one page of code here
// so it is better to comment the meaning
// of the corresponding curly bracket
} while(loop);
} // if(valid)
または、インデントを使用して同等の goto を使用することもできるため、ラベル名を賢く選択すればコメントは必要ありません。
if(!valid) goto NOTVALID;
LOOPBACK:
// more than one page of code here
if(loop) goto LOOPBACK;
NOTVALID:;