goto 文の使い道 / プログラミング雑記

概要

悪い悪いと言われながらも C 系プログラミング言語に残り続ける goto 文。残っているのは当然、良い使い方ができる上に、他の構文ではその機能を実現できないからです。

そこで、私が普段使っている goto 文の「良い」と思う使い方を書いてみます。

一括した後処理の実行

何らかのリソースを使うプログラムではそれを後で開放する必要があります。その場合、リソースを使う処理は複数あり、それぞれが失敗する可能性を持つでしょう。例として、二つのファイルを使って処理を実行する ProcessA、ProcessB、ProcessC という三つの関数を続けて実行するプログラムを考えます。複数のリソースを開放するコードを複数箇所に分散させるのはバグの元とし、多くの人はリソース開放のコードを一箇所にまとめたいと考えます。したがって、次のようなコードができあがります(C++風のコード)。

int rc = -1; // result code
FILE * file1;
FILE * file2;

// ファイルを開く
file1 = fopen( "hoge.txt", "r" );
if( file1 == NULL )
{
    return -1;
}

// もうひとつのファイルを開く
file2 = fopen( "piyo.txt", "r" );
if( file2 != NULL )
{
    // 処理A
    rc = ProcessA( file1, file2 );
    if( rc == 0 )
    {
        // 処理B
        rc = ProcessB( file1, file2 );
        if( rc == 0 )
        {
            // 処理C
            ProcessC( file1, file2 );
            /**不要**if( rc != 0 )
            {
                // do nothing
            }*/

        }
    }
}

// リソースを開放
if( file1 != NULL )
{
    fclose( file1 );
}
if( file2 != NULL )
{
    fclose( file2 );
}

return rc;

これは、各処理が成功した場合の処理を if 文の中に書き、どんどん入れ子していく方法です。この場合は if の入れ子が 4 つ程度ですのでまだ良いですが、7 つ等となってくると読む気すら消えてしまいます。このように入れ子を繰り返したコードを読みやすいと言う人には、いまだ会った事がありません。ですから世の中にあるこのようなコードは、書いた人本人が読みにくいと思いながらも妥協したコードなのではないかと思います。

さて、これを goto 文をうまく使うと次のように書き直せます。

int rc = -1; // result code
FILE * file1 = NULL;
FILE * file2 = NULL;

// ファイルを開く
file1 = fopen( "hoge.txt", "r" );
if( file1 == NULL )
{
    goto cleanup;
}

// もうひとつのファイルを開く
file2 = fopen( "piyo.txt", "r" );
if( file2 == NULL )
{
    goto cleanup;
}

// 処理A
rc = ProcessA( fp );
if( rc != 0 )
{
    goto cleanup;
}

// 処理B
rc = ProcessB( fp );
if( rc != 0 )
{
    goto cleanup;
}

// 処理C
rc = ProcessC( fp );
/**不要**if( rc == 0 )
{
    goto cleanup;
}*/


cleanup:
// リソースを開放
if( file1 != NULL )
{
    fclose( file1 );
}
if( file2 != NULL )
{
    fclose( file2 );
}

return rc;

このコードでは、 各処理をごとにエラーを判定して、 失敗したらリソース開放部分へジャンプしています。 ジャンプに goto を利用しているわけですが、 ラベルに cleanup という名前を使うことで、さらに可読性が高くなっていると思います。例えば、ProcessC 失敗後のエラー判定を「不要」とコメントアウトしてある部分を読んだとき、何がなぜ不要となっているのか、は明らかに後者の書き方の方が分かりやすいでしょう。

後者の書き方をすると、処理の流れが箇条書きのように上から下へ流れていきます。私はプログラムの処理を 「まずAをして、Bをして、そこでCでなければDをして、・・・」 といったように箇条書きに近い感覚で作っていきます。そのため、この書き方を好むわけです。

前者の書き方で一番問題だと思うのは、人情として if の入れ子が嫌なので必要なエラー処理を省略したくなってしまう事です。たとえば、この例における ProcessA は失敗する確率が非常に低い、あるいは失敗する条件がよく分かっていない、としましょう。すると先に述べた人情によって、ProcessA のエラー処理を省略したくなってしまいます。もし省略すると、コードは短くなり、インデントも少なくなり、自己満足も達成されますが、顧客満足は達成されないでしょう。

なお、このパターンは try/finally 構文によるリソース開放のパターンとまったく同じです。goto 文はこのように良い形で使える一方、プログラムをメチャクチャにする使い方もできてしまいます。Java には goto が無いかわりに try/finally 構文があり、後者の書き方を実現できます。実際、try/finally には goto のリソース開放用途限定版、という側面もあります。

ループ処理中のエラー時に次へ処理を進める

例として、長さ aryLength の ary という配列の各要素に対して同じ処理を行っていく場合を考えましょう。先ほどと同様、要素に対して ProcessA、ProcessB、ProcessC と実行する場合、次のようになります。

int i, rc; // result code

i = 0;
while( i < aryLength )
{
    // 処理A
    rc = ProcessA( ary[i] );
    if( rc == 0 )
    {
        // 処理B
        rc = ProcessB( ary[i] );
        if( rc == 0 )
        {
            // 処理C
            rc = ProcessC( ary[i] );
            if( rc != 0 )
            {
                break;
            }
        }
    }

    // 次の要素へ
    i++;
}

もう前節と同じようなパターンになっていることに気づかれた方も多いと思います。これは、前節の例と同じように、「成功した場合、成功した場合、・・・」という形で if 文を入れ子にしています。

これを goto 文をうまく利用して書くと次のようになります。

int i, rc; // result code

i = 0;
while( i < aryLength )
{
    // 処理A
    rc = ProcessA( ary[i] );
    if( rc != 0 )
    {
        goto next_element;
    }
   
    // 処理B
    rc = ProcessB( ary[i] );
    if( rc != 0 )
    {
        goto next_element;
    }
   
    // 処理C
    rc = ProcessC( ary[i] );
    if( rc != 0 )
    {
        break;
    }

    next_element:
    // 次の要素へ
    i++;
}

このコードは前節の例と同様、各処理ごとにエラーを判定して、失敗したら次の要素を取得する部分へジャンプしています。ジャンプ先のラベルに next_element という名前を使っているため、次の要素取得に「goto next_element」という非常に自然言語に近い表現ができています。可読性は、if の多段入れ子版より高いと思います。

この書き方は、不要とする人もいるでしょう。というのは、このループを for 文で書き直せば goto を使わずに単純に continue すれば良いからです。しかし次状態へ移行する式が複雑な場合、for 文は読みにくくなります。私は頭が大きくなった for 文が嫌いなのでこのような goto の使い方をしています。

なお、この使い方は for 文による書き換えが可能だからだと思いますが、Java で代わりになる文法が用意されませんでした。個人的にはちょっと悲しいです(苦笑)。

多重ループからの脱出

実を言うとあまり私が使う機会の無いパターンなのですが、多重ループから脱出するのに goto 文は活躍します。

個人的には、文脈的に多重ループが自然な形といえる場合は、そう多くないと思います。たとえば行列などの多次元データを扱う処理を多重ループで書くのは非常に自然な形でしょう。こういった場合に、goto を使って脱出するのは良い使い方でしょう。しかし、そうでない場合は内側のループを別関数に切り出せないか考えるべきだと思います。もし別関数として切り出せれば、goto で脱出していた部分を「その関数が失敗したら break」とするだけで済みます。その方がデバッグも楽だと思いますし、そのような目安を設けていれば不必要に関数が複雑化する心配が減ります。

Java ではこの用途のためにラベル付き break/continue という構文が用意されています。どのようなものかというと、ラベルで「ループに」名前を付けておき、そのループを break あるいは continue する、というものです。具体的には多重ループの外側のループにラベルを付けておき、内側のループ中でそのラベルを指定して break あるいは continue します。

最後に

goto 文に限らず、道具は使いようだと思います。読みやすくするために凝った事をしすぎて逆に読みにくくなるのは本末転倒ですが、この程度ならば言語の機能の範囲内ですし、文脈を歪めたりもしません。こんな方法を採っている人もいるのだなぁ、という程度に参考にしていただければと思います。

目次

  1. 概要
  2. 一括した後処理の実行
  3. ループ処理中のエラー時に次へ処理を進める
  4. 多重ループからの脱出
  5. 最後に