回答:
私が知っている "goto"ステートメントを使用する理由はいくつかあります(一部はすでにこれについて話しています):
関数をきれいに終了する
多くの場合、関数では、リソースを割り当て、複数の場所で終了する必要があります。プログラマは、関数の最後にリソースクリーンアップコードを配置することでコードを簡略化でき、関数のすべての「出口点」はクリーンアップラベルに移動します。このように、関数のすべての「出口点」でクリーンアップコードを記述する必要はありません。
ネストされたループを終了する
入れ子になったループで、すべてのループから抜け出す必要がある場合、gotoを使用すると、breakステートメントやifチェックよりもずっとクリーンでシンプルになります。
低レベルのパフォーマンスの改善
これは、パフォーマンスが重要なコードでのみ有効ですが、gotoステートメントは非常に高速に実行され、関数を移動するときに効果を発揮します。ただし、コンパイラは通常、gotoを含むコードを最適化できないため、これは両刃の剣です。
これらすべての例で、gotoは単一の関数のスコープに制限されていることに注意してください。
goto
にはreturn
ちょうど愚かです。それは何も「リファクタリング」するのではなく、単に「名前を変更する」だけなので、goto
抑圧された環境で育った人々(つまり、私たち全員)は、道徳的にに相当するものを使うことについてより良い気分になりますgoto
。私はずっとループを参照することを好む私はそれを使用し、少し見goto
ているそれだけで、ただのツールを参照してください誰かがちょうど避けるためにどこかに無関係なループを移動したよりも、goto
。
break
、continue
、return
基本的にあるgoto
だけで素敵なパッケージで、。
do{....}while(0)
Javaで動作するという事実を除いて、gotoよりも優れたアイデアになるとは思いません。
goto
Edsger DijkstraのGoToは有害であると見なされ、直接的または間接的にアンチサイトであるすべての人記事を反論彼らの立場を実証します。悪いダイクストラの記事は、最近のステートメントの使用方法とは実質的に何の関係もないためgoto
、この記事で述べていることは、現代のプログラミングシーンにはほとんどまたはまったく適用できません。goto
今の宗教上の-lessミームvergesは、その経典の右下には高い、その高司祭と認識される異端のシャニング(またはより悪い)上から指示されます。
ダイクストラの論文を文脈に当てはめて、主題に少し光を当てましょう。
ダイクストラが彼の論文を書いたとき、当時の一般的な言語は、BASIC、FORTRAN(初期の方言)、さまざまなアセンブリ言語などの非構造化手続き言語でした。高級言語を使用している人々が、「スパゲッティコード」という用語を生み出したねじれた、歪んだ実行スレッドでコードベース全体をジャンプすることはごく一般的でした。これは、Mike Mayfieldによって作成された古典的なトレックゲームに飛び乗って、物事がどのように機能するかを理解しようとすることで確認できます。少し時間をかけて、それを見てください。
これは、ダイクストラが1968年に彼の論文で非難していた「行き止まりのない使用の声明」です。 、これは彼がその論文を書くために彼を導いたことに住んでいた環境です。コードの好きな場所で好きな場所にジャンプできる機能は、彼が非難し、停止するように要求していたことです。それを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
反対の十字軍に行くよりずっと前に、の使用の約50%をはるかに詳しく調べgoto
ます!
ですから、ここまで読んだことが気になった人にとって、注意すべき重要な点がいくつかあります。
goto
ステートメントに関する論文goto
は、多くのプログラミング環境向けに書かれた
より多くの潜在的にそれは、アセンブラではありません最も近代的な言語であるよりも、ダメージを与えます。goto
このため、「一度は楽しんだが、気に入らなかったので、今は反対だ」と言うのと同じくらい合理的です。goto
他の構成体で適切に置き換えることができないコードステートメントの。godo
」の代わりに、常に偽のdo
ループが使用さbreak
れていない「」のような現代の制御ステートメントの不正な使用法もありますgoto
。これらは、賢明なの使用よりも悪い場合がよくありgoto
ます。goto
実際の利点を示すことを忘れている(これは投稿された質問です)
盲目的にベストプラクティスに従うことは、ベストプラクティスではありません。goto
フロー制御の主要な形式としてステートメントを回避するという考えは、判読不能なスパゲッティコードの生成を回避することです。適切な場所で控えめに使用すると、アイデアを表現する最も簡単で明確な方法になることがあります。Zortech C ++コンパイラとDプログラミング言語の作成者であるWalter Brightは、それらを頻繁に、しかし慎重に使用しています。でもgoto
ステートメント、彼のコードは完全に読み取り可能です。
結論:回避goto
するために回避するのgoto
は無意味です。本当に避けたいのは、読み取り不可能なコードを作成することです。あなたの場合はgoto
-ladenコードが読み取り可能である、それで何も間違ってあります。
以来goto
プログラムについての推論ますが流れにくい1(別名「スパゲッティコード」。)、goto
一般的な機能のみを欠けを補償するために使用される:の使用がgoto
実際に許容できるかもしれないが、言語は、より構造化バリアントを提供していない場合にのみ取得します同じ目標。ダウトの例を見てみましょう:
私たちが使用するgotoのルールは、関数内の単一の終了クリーンアップポイントにジャンプするためにgotoが問題ないということです。
これは当てはまりますが、言語がクリーンアップコード(RAIIや finally
)を)は、同じ処理をより特別に作成されているため)、または正当な理由がない場合に限ります。構造化された例外処理を採用する(ただし、非常に低いレベルを除いて、このケースが発生することはありません)。
他のほとんどの言語では、許容される唯一の使用法goto
は、ネストされたループを終了することです。そして、ほとんどの場合、外側のループを独自のメソッドに持ち上げてreturn
代わりに使用する方が良いでしょう。
それ以外goto
は、特定のコードに十分な考慮が払われていないという兆候です。
1goto
いくつかの制限を実装することをサポートする現代の言語(たとえばgoto
、関数にジャンプしたり、関数からジャンプしたりできない)が、問題は基本的に同じままです。
ちなみに、他の言語機能、特に例外を除いて、もちろん同じことが当てはまります。また、例外を使用して例外的でないプログラムフローを制御しないというルールなど、指定された場所でのみこれらの機能を使用するための厳格なルールが通常あります。
finally
?したがって、エラー処理以外のものに例外を使用することは良いですが、使用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
do{}while(false)
私は慣用的であると考えることができると思います。同意しないでください:D
goto after_do_block;
実際のことを言わずに、ただの言い方にすぎないことに気づくでしょう。そうでなければ...ちょうど1回だけ実行される「ループ」?私はそれを制御構造の乱用と呼んでいます。
#define
sの数、時々使用するよりもはるかに悪い方法を非常にうまく示しgoto
ました:D
ではC#の スイッチ文のdoestフォールスルーができません。したがって、gotoは、特定のスイッチケースラベルまたはデフォルトラベルに制御を移すために使用されます。
例えば:
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ステートメントにコードがない場合、フォールスルーが許可されます。
goto case 5:
です(ケース1にいるときに言うことができるため)。ここでは、Konrad Rudolphの答えが正しいようです。goto
欠落している機能を補っています(実際の機能ほど明確ではありません)。私たちが本当に望んでいるのがフォールスルーである場合、おそらく最良のデフォルトはフォールスルーではなくcontinue
、明示的に要求するようなものです。
#ifdef TONGUE_IN_CHEEK
Perlにはgoto
貧乏人の尻尾の呼び出しを実装できるがあります。:-P
sub factorial {
my ($n, $acc) = (@_, 1);
return $acc if $n < 1;
@_ = ($n - 1, $acc * $n);
goto &factorial;
}
#endif
OK、Cとは関係ありませんgoto
。もっと真剣に、私goto
はクリーンアップのために、またはダフのデバイスを実装するための使用についての他のコメントに同意しますます。それは悪用ではなく、使用に関するすべてです。
(同じコメントがlongjmp
、例外、call/cc
などに適用できます---それらは正当な用途がありますが、簡単に悪用される可能性があります。たとえば、完全に例外のない状況下で、深くネストされた制御構造をエスケープするために純粋に例外をスローする。)
長年にわたって私は数行以上のアセンブリ言語を書いてきました。最終的に、すべての高水準言語はゴトにコンパイルされます。さて、それらを「ブランチ」や「ジャンプ」などと呼びますが、それらはゴトです。誰でもgotoなしのアセンブラを作成できますか?
これで、Fortran、C、またはBASICプログラマーに、gotoで暴動を実行するのはスパゲッティボロネーズのレシピであることを指摘できます。しかし、答えはそれらを回避することではなく、慎重に使用することです。
ナイフは、食事の準備、誰かの解放、または誰かの殺害に使用できます。私たちは後者を恐れてナイフなしでやっていますか?同様に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つの変更を行ってください。ここで、すべてのクリーンアップを単一の関数にカプセル化できると決めるかもしれません。それは悪い考えではありません。しかし、それはポインタに注意する必要があることを意味します-クリーンアップ関数でポインタを解放することを計画している場合、ポインタにポインタを渡さない限り、ポインタを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が受け入れられるケースのリストを提供する人もいるのは面白いと思います。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
各機能を抽象化の同じレベルにすることは、他のどんな反対よりもはるかに重要だと私は思います。それが避けることgoto
はボーナスです。
container::iterator it = slot_p.find(hash_key); if (it != slot_p.end()) it->overwrite(hash_key); else it = slot_p.find_first_empty();
種のプログラミングははるかに読みやすくなっています。この場合の各関数は、純粋な関数として記述できます。メイン関数は、コードが関数の名前だけで何を行うかを説明します。必要に応じて、それらの定義を見て、それがどのように行われるかを確認できます。
私たちが使用するgotoのルールは、関数内の単一の終了クリーンアップポイントにジャンプするためにgotoが問題ないということです。本当に複雑な関数では、そのルールを緩和して他のジャンプを許可します。どちらの場合も、エラーコードのチェックで頻繁に発生するifステートメントが深くネストされることを避けています。これにより、読みやすさと保守が容易になります。
gotoステートメント、その正当な用途、および「仮想gotoステートメント」の代わりに使用でき、gotoステートメントと同じくらい簡単に悪用できる代替構成体の最も思慮深く徹底的な議論は、Donald Knuthの記事「gotoステートメントによる構造化プログラミング」です。」です。 、1974年12月のコンピューティング調査(第6巻、第4号、261〜301ページ)。
当然のことながら、この39年の古い論文のいくつかの側面は古くなっています。処理能力の桁違いの増加により、中程度のサイズの問題ではKnuthのパフォーマンスの向上の一部が目立たなくなり、それ以降、新しいプログラミング言語の構成要素が発明されました。(たとえば、try-catchブロックはZahnの構成を包含しますが、そのように使用されることはめったにありません。)しかし、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の反復プリミティブおよび決定プリミティブで簡単にモデル化できるわけではありません。gotosは、これらの欠陥を修正するのに大いに役立ちます。
ある種のフラグ変数を使用して一種の疑似マルチレベルのブレークを実行する方が明確な場合がありますが、常にgotoより優れているわけではありません(少なくともgotoを使用すると、フラグ変数とは異なり、どこに制御が移動するかを簡単に判断できます)、そしてときどき、gotoを回避するためにフラグやその他のゆがみのパフォーマンス価格を支払いたくないだけの場合もあります。
libavcodecは、パフォーマンスに敏感なコードです。制御フローは直接実行するほうが優先される可能性があります。これは、実行速度が向上する傾向があるためです。
do {} while(false)の使用法は完全に反抗的です。奇妙なケースで必要であると私に納得させるかもしれないと考えられるかもしれませんが、それがクリーンで賢明なコードであるとは決して言えません。
このようなループを実行する必要がある場合は、フラグ変数への依存を明示的にしないのはなぜですか?
for (stepfailed=0 ; ! stepfailed ; /*empty*/)
/*empty*/
ことstepfailed = 1
?いずれにせよ、これはaよりも優れていdo{}while(0)
ますか?両方とも、あなたbreak
はそれから(またはあなたのものでstepfailed = 1; continue;
)抜け出す必要があります。私には不要のようです。
1)私が知っている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プログラミングでその位置を占めています。それでは、誰が正しいのでしょうか?ダイクストラまたはライナス(およびすべてのLinuxカーネルプログラマ)基本的には理論対実践です。
ただし、コンパイラレベルのサポートがないことと、一般的な構成/パターンをチェックしないことには通常の落とし穴があります。それらを間違って使用し、コンパイル時のチェックなしにバグを導入する方が簡単です。WindowsとVisual C ++ですが、Cモードでは、SEH / VEHを介して例外処理を提供します。これはまさにその理由です。例外は、OOP言語の外、つまり手続き型言語でも役立ちます。ただし、コンパイラが言語の例外の構文サポートを提供している場合でも、常にベーコンを保存できるとは限りません。後者の例として、1つのgotoを複製して悲惨な結果をもたらした有名なApple SSLの「goto fail」バグを考えてみます(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は到達不可能なコードを(それを見つけることができる場所で)禁止しています。goto構文が、計算されたアドレス(**)へのgotosのように、コンパイラーが簡単に理解できないターゲットを許可しない限り、コンパイラーがDijkstraを使用するよりもgotoを使用して関数内の到達不能コードを見つけることは難しくありません-承認済みコード。
(**)脚注:Basicの一部のバージョンでは、計算された行番号へのGotoが可能です。たとえば、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番目に一般的な使用法は、マルチレベルのブレークまたはマルチレベルの継続の実装であると思います。これは、かなり議論の余地のないユースケースでもあります。Javaではgotoラベルは許可されていませんが、breakラベルまたはcontinueラベルは許可されています。http://www.oracle.com/technetwork/java/simple-142616.htmlによると、これは実際にはCでのgotoの最も一般的な使用例です(90%は彼らが言う)が、私の主観的な経験では、システムコードはエラー処理にgotosをより頻繁に使用する。おそらく科学的なコードやOSが例外処理を提供する場合(Windows)、マルチレベルの出口が主な使用例です。彼らは実際に彼らの調査のコンテキストについての詳細を提供しません。
追加のために編集:これらの2つの使用パターンは、60ページ(エディションによって異なります)の周りのKernighanとRitchieのCブックにあることがわかりました。もう1つの注目すべき点は、どちらのユースケースにも前方ゴトのみが含まれることです。そして、MISRA C 2012エディション(2004エディションとは異なります)は、フォワードのみである限り、gotoを許可することがわかりました。
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-freeコードと比較してください:
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を使用するのと同じタイプであり、適切に構造化されたパターンであり、唯一の推奨される方法よりも多くのプロモートではない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 ++で関数スコープを持っているため、重複するループがその場所にあり、非常に一般的である純粋なアセンブリのようにグローバルスコープを汚染しないことに注意してください。次の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
別の利点があります。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:;
「goto」の問題と「goto-less programming」運動の最も重要な議論は、コードを頻繁に使用すると、正しく動作する可能性はあるものの、読み取り不能、保守不能、レビュー不能などになることです。99.99%で'goto'の場合は、スパゲッティコードになります。個人的には「後藤」を使う理由はなかなか思いつきません。
goto
)。@cscholの使用法も同様です。現在言語を設計していない可能性がありますが、基本的にはデザイナーの努力を評価しています。
goto
変数が存在するコンテキストを除いて言語を許可する方が、誰かが必要とする可能性のあるあらゆる種類の制御構造をサポートしようとするよりも安くなる傾向があります。でコードを書くgoto
ことは他の構造を使用するよりも良くないかもしれませんが、そのようなコードを書くことができることはgoto
「表現力の穴」(言語が効率的なコードを書くことができない構成)を避けるのに役立ちます。
goto
がコードレビューサイトに含まれているコードを投稿するたびにgoto
、コードのロジックが大幅に簡素化されます。
もちろんGOTOを使用することもできますが、コードスタイルよりも重要な点が1つあります。つまり、コードを読みやすくしたり読み取ったりできない場合、使用時に注意が必要です。内部のコードは、あなたほど堅牢ではない可能性があります。と思います。
たとえば、次の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)
私たちが最初に考えることは、コードの両方のビットの結果は "Value of A:0"になるということです(もちろん、並列処理なしの実行を想定しています)。
それは不正解です。最初のサンプルでは、Aは常に0ですが、2番目のサンプル(GOTOステートメントを使用)では、Aは0ではない可能性があります。なぜですか?
その理由は、プログラムの別のポイントからGOTO FINAL
、Aの値を制御せずにを挿入できるからです。
この例は非常に明白ですが、プログラムが複雑になるにつれて、そのようなものを見ることの難しさが増します。
関連資料は、ダイクストラ氏の有名な記事「GO TOステートメントに対する訴訟」にあります。
私は次の場合に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;
}
gotoバージョン:
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回使用されます)が簡単になり、新しいブランチを追加するときにそれらのいずれかをスキップする可能性が低くなります。関数内でそれらを移動しても、割り当て解除はさまざまな「レベル」で実行できるため、ここでは役に立ちません。
finally
、C#にブロックがある理由です
finally
)。別の方法として、goto
sを使用しますが、常にすべてのクリーンアップを実行する共通の出口点に移動します。ただし、各クリーンアップメソッドは、nullまたは既にクリーンな値を処理できるか、条件テストによって保護されているため、適切でない場合はスキップされます。
goto
、すべて同じ出口点に行くs を使用する私の代替案を参照してください。これは同じロジックを持っています(これは、リソースごとにifが必要なように追加が必要です)。しかし、気にしC
ないでください。正しいコードを使用している場合、コードがCで記述されている理由が何であれ、ほとんどの場合、最も「直接的な」コードを優先するトレードオフになります。(私の提案ハンドル任意のリソースがまたは過剰、この場合には、ええ割り当てられた。しかしされていない可能性があり複雑な状況を。。)
時々、文字単位の文字列処理に便利です。
次のprintf-esqueの例のようなものを想像してみてください。
for cur_char, next_char in sliding_window(input_string) {
if cur_char == '%' {
if next_char == '%' {
cur_char_index += 1
goto handle_literal
}
# Some additional logic
if chars_should_be_handled_literally() {
goto handle_literal
}
# Handle the format
}
# some other control characters
else {
handle_literal:
# Complicated logic here
# Maybe it's writing to an array for some OpenGL calls later or something,
# all while modifying a bunch of local variables declared outside the loop
}
}
これgoto handle_literal
を関数呼び出しにリファクタリングすることもできますが、複数の異なるローカル変数を変更する場合は、言語が可変クロージャをサポートしていない限り、それぞれに参照を渡す必要があります。continue
ロジックでelseケースが機能しない場合は、呼び出し後にステートメント(おそらくgotoの形式の1つ)を使用して同じセマンティクスを取得する必要があります。
私はまた、典型的には同様のケースで、レクサーでgotoを慎重に使用しました。ほとんどの場合それらは必要ありませんが、これらの奇妙なケースに備えておくと便利です。