7/22に開始される Go toキャンペーン
に先駆けて
プログラミングでもgotoキャンペーンをしてみよう
#gotoとは
古代から存在する制御構文であり、無条件にジャンプを行う
アセンブラ時代の非常に古い文法であるため忌み嫌われる事が多いが
未だに数多くの言語に存在する
コーディング規約でgotoの禁止が明記されたり、スパゲッティーコードの原因と言われたり
使ってはいけない構文と言われる事も多いが
正しく使えばコードの可読性が上がるため、脳死でgotoは禁止と考えるべきではない
現代の言語では関数というものが存在し、goto文は関数を飛び越えられないため
gotoでスパゲッティーコードになる状況は、そもそも関数を分割すべきである
##goto論争
古い言語ではgotoが頻繁に利用されていた
1968年にコンピュータサイエンスのダイクストラが「Go To Statement Considered Harmful」
という論文を出し、一躍goto文が悪役におどりでる
本当はダイクストラの言い分は違い、もっとライトなものであったのだが
強烈なタイトルだけが独り歩きした
嫌なgoto文の例
goto TRAVEL;
TRAVEL:
goto CHIBA;
IWATE:
goto TOKYO;
CHIBA:
goto KYOTO;
TOKYO:
goto CANCEL;
NAGOYA:
goto FUKUOKA;
KYOTO:
goto NAGOYA;
FUKUOKA:
goto IWATE;
CANCEL:
##構造化
以下の3つの構文でgoto文は全て置き換えることが可能
###sequence
上から順にコードは実行される
通常の言語だと、上記は満たされている
###selection
if文等の事
例えばgoto文で書くと(注:参考コード)
#include<iostream>
using namespace std;
void hoge(bool b){
if(b) goto LABEL_TRUE;
cout << "false" << endl;
goto LABEL_END;
LABEL_TRUE:
cout << "true" << endl;
LABEL_END:
return; // ラベルの後に最低でも1行必要
}
int main(){
hoge(true);
hoge(false);
return(1);
}
--------
true
false
これをif文にすれば下記のように単純にできる
using namespace std;
void hoge(bool b){
if(b){
cout << "true" << endl;
}else{
cout << "false" << endl;
}
}
int main(){
hoge(true);
hoge(false);
return(1);
}
--------
true
false
これでgoto文を減らせた
###iteration
for あるいはwhile文の事
普通に0から9までのLOOPを書くとこうなる
#include<iostream>
using namespace std;
int main(){
int n=0;
LABEL_LOOP:
cout << n << endl;
n++;
if(n >= 10) goto LABEL_END;
goto LABEL_LOOP;
LABEL_END:
return(1);
}
----
0
1
2
3
4
5
6
7
8
9
for文を使うとこうなる
using namespace std;
int main(){
for(int n=0; n<10; n++){
cout << n << endl;
}
return(1);
}
----
0
1
2
3
4
5
6
7
8
9
###iteration2
LABEL_INPUT:
string s = input();
if( invalid(s) ) goto LABEL_INPUT;
string s;
do{
s = input();
}
while( invalid(s)) ;
このようにする事で、gotoを避けることができる
#良いgoto文
上記のように、goto文を使わなくてもコードは書ける
だが、近代のプログラミング言語でもgoto文を採用するものが多い
例えば、命令を極限まで減らしシンプルに設計されたGolangにおいてもgoto文は存在するし
php、pythonやJavascriptといった言語にも採用されている
gotoのない言語としてJavaが有名だが、今後goto文が追加される可能性を考慮し予約語になっているし
多重ループを抜ける手段として、ラベル付きbreakなどがある(実質gotoと同じである)
Rubyにはgoto文が存在しないので、よく例外で多重ループで抜けるのをみかけますが
エラーじゃない時に例外を投げるというのは、良くないし
例外は、本当にパフォーマンスが悪い。
RubyのVMがどのように実装しているか細かく見てないので一般論だけど
コンテキストスイッチを行ったり。
なので、なるべく例外で多重ループを抜けたくないと思う。
その場合は関数を分割するか、フラグで管理することになるが
後述するとおり、フラグ管理は好ましくない
コーディング規約でgotoの仕様を禁止される事が多いが
goto文は正しく使えばコードの見通しを良くし、無駄を省けるので
闇雲に禁止せずに、一度考えてみて欲しい
goto文を使うほうがコードの見通しが良くなる事も多い。
下記に良いgoto文を示す
##リソース開放
void foo(){
auto hoge = new Hoge();
if( hoge == nullptr ){
return;
}
if( なんかの処理() == エラー ){
delete hoge;
return;
}
if( なんかの処理2() == エラー ){
delete hoge;
return;
}
正常時の処理
delete hoge;
}
上記のように、エラーのたびにリソースの開放が必要
ただし、newとdeleteが1対1に対応せずdeleteのほうが多くなるのは気持ち悪いし
どこかのケースでdeleteを忘れる事もあり、非常に見つけにくい不具合になりやすい
もちろん、newやmallocの場合はnullptrで判断するのが定跡
void foo(){
auto hoge = nullptr;
do{
hoge = new Hoge();
if( hoge == nullptr ) break;
if( なんかの処理() == エラー ) break;
if( なんかの処理2() == エラー ) break;
正常時の処理
}while(false);
if(hoge != nullptr )delete hoge;
}
と、たとえばdo while文でbreakを使い、構造化っぽく処理をすることもできる
が、このdo~whileは無駄である。ループさせる意図がないのにwhileを使うべきではないし
間違って無限ループになると目も当てられない
newの場合はnullptrかどうかで初期化が行われたかの判断が可能だが、何かのライブラリの初期化処理だと
判別できないかもしれないので、その場合はフラグを使うことになるかもしれない
void foo(){
bool init = false;
do{
if( !hoge_init() ) break;
init = true;
if( なんかの処理() == エラー ) break;
if( なんかの処理2() == エラー ) break;
正常時の処理
}while(false);
if( init ) hoge_release();
}
こんなフラグ制御を行い、どのような不具合が作られるか想像に易い
void foo(){
if( !hoge_init() ) return;
if( なんかの処理() == エラー ) goto LABEL_ERR;
if( なんかの処理2() == エラー ) goto LABEL_ERR;
正常時の処理
LABEL_ERR:
hoge_release();
}
gotoを利用すれば、比較的に綺麗に記述できた
C++であれば本当に正しい答えは、newなどはunique_ptr等をつかい、それ以外はカスタムデリータを使い
スコープが終われば確実に開放されるようにする事だ
void foo(){
auto hoge = std::make_unique<Hoge>();
if( なんかの処理() == エラー ) return;
if( なんかの処理2() == エラー ) return;
正常時の処理
}
スマートポインタを使えば、スコープを抜けると自動で開放されるので不具合を作る可能性は減る
もちろんC言語や他の言語の場合は、newとdeleteを関数の外で行う事で対応する
void bar(){
void* buf = malloc(100);
if(buf != 0){
foo(buf);
free(&buf);
}
}
void foo(void **buf){
if( なんかの処理() == エラー ) return;
if( なんかの処理2() == エラー ) return;
正常時の処理
}
##複数のリソース
複数のリソースの開放が絡む場合は非常に厄介だ
void foo(){
if(initA()){
if(initB()){
if(initC()){
正常処理
releaseC();
}
releaseB();
}
releaseA();
}
}
ネストも深く非常に嫌なコードになる
ネストが深くなると終了処理のタイミングを間違いやすくなる
ので、フラグを使って平べったっくしてみる
void foo(){
bool flagA = flagB = flagC = false;
do{
if(!initA()) break;
flagA = true;
if(!initB()) break;
flagB = true;
if(!initC()) break;
flagC = true;
正常処理
}while(false);
if(flagC) releaseC;
if(flagB) releaseB;
if(flagA) releaseA;
}
読みやすくなったかもしれないが、無駄な do while文も無駄であるし
フラグも不具合を誘発しやすい
gotoに書き直してみる
void foo(){
if(!initA()) goto LABEL_END;
if(!initB()) goto RELEASE_A;
if(!initC()) goto RELEASE_B;
正常処理
releaseC;
RELEASE_B:
releaseB;
RELEASE_A:
releaseA;
LABEL_END;
return;
}
無駄がなくなった。
もちろん、複数のリソースが絡み合う場合でも
C++ではスマートポインタによる自動リソースの開放を使うべきだ
##多重ループから抜ける
多重ループから抜ける際にgotoを使わないと可読性が下がる事がある
なぜなら、大抵の言語ではループから抜けるbreakは1つのループからしか抜けることができず
複数のループを抜けるにはフラグなどを使う必要がある
bool flag = false;
for(int i=0; i<10 && !flag; i++ ){
for(int j=0; j<10 && !flag; j++ ){
for(int k=0; k<10; k++ ){
if( buf[i][j][k] == 1 ){
flag = true;
break;
}
}
}
}
外のforループの終了条件にフラグが追加される
不具合が出る可能性は無限大だ
gotoを使えば下記になる
for(int i=0; i<10; i++ ){
for(int j=0; j<10; j++ ){
for(int k=0; k<10; k++ ){
if( buf[i][j][k] == 1 ){
goto LABEL_FOUND;
}
}
}
}
LABEL_FOUND:
非常にわかりやすい。
もちろん王道な方法は、ループを関数に分けたりすることだ。
#おまけ(研究)
##Coroutineを作る
C言語にはコルーチンがなく不便なので作ってみた
#include<iostream>
using namespace std;
void hoge(int &n){
if(n==0) goto LABEL_0;
if(n==1) goto LABEL_1;
if(n==2) goto LABEL_2;
LABEL_0:
cout << 0 << endl;
n++;
goto LABEL_END;
LABEL_1:
cout << 1 << endl;
n++;
goto LABEL_END;
LABEL_2:
cout << 2 << endl;
n = 0;
goto LABEL_END;
LABEL_END:
return;
}
int main(){
int coro = 0;
hoge(coro);
hoge(coro);
hoge(coro);
hoge(coro);
hoge(coro);
return(1);
}
----
0
1
2
0
1
あ、はい、switch case文でよくね?
#include<iostream>
using namespace std;
void hoge(int &n){
switch(n){
case 0:
cout << n << endl;
n++;
break;
case 1:
cout << n << endl;
n++;
break;
case 2:
cout << n << endl;
n = 0;
break;
}
return;
}
int main(){
int coro = 0;
hoge(coro);
hoge(coro);
hoge(coro);
hoge(coro);
hoge(coro);
return(1);
}
----
0
1
2
0
1
##fizzbuzz
fizzbuzzも簡単に書くことができる
#include<iostream>
using namespace std;
int main(){
int n = 0;
LOOP:
if(n%15 == 0) goto FIZZBUZZ;
if(n%3 == 0) goto FIZZ;
if(n%5 == 0) goto BUZZ;
cout << n << endl;
goto LOOP_FIN;
FIZZBUZZ:
cout << "FIZZBUZZ" << endl;
goto LOOP_FIN;
FIZZ:
cout << "FIZZ" << endl;
goto LOOP_FIN;
BUZZ:
cout << "BUZZ" << endl;
goto LOOP_FIN;
LOOP_FIN:
n++;
if(n < 100) goto LOOP;
return(1);
}
#まとめ
gotoは無条件で否定すべきではないと思われる
if~else、do~whileやforで単純に書き換えられる箇所は書き換える方が良い事が多い
do~while(false) イデオムより、gotoに変えたほうが良い場合も多い
エラー処理、依存するリソースの開放、深いネストから抜ける際に、gotoを使う時もある
反面、上に戻るgotoは、読みにくくなる事が多いので、while等で戻る事を検討する