PMPの流儀

PMPの流儀

エンジニアのページ

MENU

C言語でgoto文を適切に使うとメリットがあります

C言語を筆頭として多くのプログラム言語にはgoto文と呼ばれる命令があります。にもかかわらずプログラム界からは長い間忌み嫌われるタブーな命令として君臨しています。
多用すると収拾がつかなくなるのは確かですが、適切な場面(多重ループからの脱出と異常処理系)でgoto文を使うと簡潔で読みやすいソースになります。勇気をもって踏み出しましょう。
f:id:ruruucky:20210311000040p:plain

 

1. goto 文が禁止の背景

1980年代、まだパソコンがマイコンと呼ばれていた頃に遡ります。NECPC-8801が一世を風靡していた時代。当時のマイコンにはOSとしてBASICが搭載されていました。ハードディスクなんてものは無く、フロッピーディスクも高価な時代です。このBASICにはC言語のような構造化ができないため、goto文だらけで構成されていました。そのため、ソース上を上に行ったり、下に行ったりとぐちゃぐちゃで、そのようなプログラムを指してスパゲッティプログラムと呼んでいました。
そこに現れたのがC言語。関数によるローカル変数の概念や、構造化を意識した命令によりgotoが無くてもプログラムが書けるようになりました。あまりにBASICのインパクトが大きかったため、goto文はタブー視されて今に至ります。

では、goto文は簡潔に書ける処理を紹介します。

2. 多重ループからの脱出

C言語では for文とwhile文でループ構造を作成でき、ループを途中で終了させるときに break で抜けることが可能です。
しかし、多重ループを一気に抜ける方法は用意されていません。

2.1 フラグにより脱出

フラグによる多重ループからの脱出はよく見ます。ですが、ちょっとややこしいです。1重ループの時にbreakだけで良かったのに、2重になっただけでこれです。可読性が落ちるので嫌いです。

f()
{
    int flg = 0;

    for( int x=0; x<xmax; x++ ) {
        for( int y=0; y<ymax; y++ ) {
            if( f( x, y ) { 
                flg = 1;            // フラグを立てて
                break;
            }
        }
        if( flg ) {
            break;              // 外で見る
        }
    }
    if( ! flg ) {
        k();        // ヒットしなかった場合の処理
    }
    g();            // 他の処理
}

2.2 多重ループを関数化

多重ループ部のみを切り出して関数化し、ループ内からreturn することでジャンプさせるイメージです。
このテクニックを使う人も多いですが、2重ループのためだけにわざわざ関数化するのは可読性が落ちるのでいまいち。関数化により引数がいろいろ必要になるかもしれません。

f()
{
    if( ! loop() ) {        // 多重ループ箇所を切り出して関数化
        k();            // ヒットしなかった場合の処理
    }
    g();
}

int loop()
{
    for( int x=0; x<xmax; x++ ) {
        for( int y=0; y<ymax; y++ ) {
            if( f( x, y ) { 
                return 1;       // breakの代わりにここでリターン
            }
        }
    }
    return 0;
}

2.3 goto文を使用

goto文を使用した例です。関数内であればラベルをつけてそこにダイレクトに処理を移すことができます。前の2つの例に比べて簡潔にまとめられています。
全く危険ではありません。これを示しても抵抗がある人は多くいます。私には謎です。

f()
{
    for( int x=0; x<xmax; x++ ) {
        for( int y=0; y<ymax; y++ ) {
            if( f( x, y ) { 
                goto end;
            }
        }
    }
    k();            // ヒットしなかった場合の処理
end:
    g();            // 他の処理
}

3. 異常処理の例

関数内で異常判定をしながら正常処理を行いますが、途中でエラーになったときにどのように関数を抜けるかで多くのパターンがあります。

3.1 if 文で異常系を先に見る

異常判定しながら、どの異常でもなかったら正常ケースを行います。1つのif文でつないでいくので、途中で切れないように気をつけないと異常なのに正常系に流れてしまうリスクがあります。
たとえば、途中のelse if の else が抜けるとそこでいったんif文が切れるため、それ以前の異常判定と正常判定が同じルートに合流してしまいます。

f()
{
    int ret = 0;
    if( check1() ) {
        ret = -1;
    }
    else if( check2() ) {
        ret = -2;
    }
    else if( check3() ) {
        ret = -3;
    } else {
        k();                     // 正常ケース
    }
    // 共通の終了処理
    g();
    free(p);
    return ret;
}

3.2 if 文で正常系を先に見る

正常判定を先に行って、その後に異常系の面倒を見ます。見ての通り、中括弧の位置を確認しないとどのケースかが分かりにくいです。

f()
{
    int ret = 0;

    if( ! check1() ) {
        if( ! check2() ) {
            if( ! check3() ) {
                k();                // 正常ケース
            } else { 
                ret = -3;
            }
        } else {
            ret  = -2;       // どこに対応しているか追いづらい
        }
    } else {
        ret = -1;           // どこに対応しているか追いづらい
    }
    // 共通の終了処理
    g();
    free(p);
    return ret;
}

3.3 goto文を使用する

3.1の例に近いですが、if文は1つ1つ独立しているのでシンプルです。例外処理の実装に近い感覚があります。
この実装はアセンブラでもよく記載する方法です。

f()
{
    int ret = 0;

    // エラーチェック3個所
    if( check1() ) {
        ret = -1;
        goto end;
    }
    if( check2() ) {
        ret = -2;
        goto end;
    }
    if( check3() ) {
        ret = -3;
        goto end;
    }

    // 正常ケースの記述
    k();
end:
    // 共通の終了処理
    g();
    free(p);
    return ret;
}