C++のgoto文について

AI実装検定のご案内

C++のgoto文は、同じ関数内にある指定したラベルへ、プログラムの実行位置を移動するための構文です。

基本構文は次のとおりです。

goto ラベル名;

// 途中の処理

ラベル名:
    処理;

例えば、次のコードでは、goto skip;が実行されると、skip:まで処理が移動します。

#include <iostream>

int main() {
    std::cout << "処理1\n";

    goto skip;

    std::cout << "この処理は実行されません\n";

skip:
    std::cout << "処理2\n";

    return 0;
}

実行結果は次のようになります。

処理1
処理2

goto skip;からskip:までの処理は実行されません。

目次

goto文で使用するラベル

ラベルは、goto文の移動先を示す目印です。

ラベル名:
    処理;

ラベル名の後ろにはコロン:を付けます。

start:
    std::cout << "開始\n";

ラベルは変数名とは異なり、関数内の実行位置を示すために使用されます。

また、ラベルはコード上で登場する前からgoto文で参照できます。

void process() {
    goto finish;

    std::cout << "この処理は実行されません\n";

finish:
    return;
}

ラベルの直後に処理がない場合

ラベルの直後に実行する処理がない場合は、空文としてセミコロンを置けます。

finish:
    ;

古いC++規格やコンパイラとの互換性を考慮する場合にも、この書き方が使われます。

C++23では、ラベルを複合文の末尾や宣言の直前に置けるよう、ラベルに関する文法が拡張されています。

void process() {
finish:
    int value = 10;

    (void)value;
}

ただし、ラベルの直後に変数を宣言できることと、変数の初期化を飛び越えられることは別の問題です。

goto文による前方ジャンプ

コード上で後ろにあるラベルへ移動することを、前方ジャンプと呼ぶことがあります。

#include <iostream>

int main() {
    int number = 10;

    if (number < 0) {
        goto error;
    }

    std::cout << "正常な値です\n";
    goto finish;

error:
    std::cout << "エラーです\n";

finish:
    return 0;
}

このコードでは、numberが負の場合にerror:へ移動します。

正常な場合は、エラー処理を飛ばしてfinish:へ移動します。

ただし、この程度の条件分岐であれば、通常はif文とelse文を使うほうが分かりやすくなります。

#include <iostream>

int main() {
    int number = 10;

    if (number < 0) {
        std::cout << "エラーです\n";
    } else {
        std::cout << "正常な値です\n";
    }

    return 0;
}

goto文による後方ジャンプ

goto文では、コード上で前にあるラベルへ戻ることもできます。

#include <iostream>

int main() {
    int count = 0;

start:
    std::cout << count << '\n';
    ++count;

    if (count < 5) {
        goto start;
    }

    return 0;
}

実行結果は次のとおりです。

0
1
2
3
4

このコードは、goto文を使って繰り返し処理を実現しています。

ただし、通常はwhile文を使用するほうが適切です。

#include <iostream>

int main() {
    int count = 0;

    while (count < 5) {
        std::cout << count << '\n';
        ++count;
    }

    return 0;
}

while文を使うことで、繰り返し処理であることが一目で分かります。

goto文で関数の外には移動できない

C++のgoto文は、同じ関数内でのみ使用できます。

別の関数にあるラベルへ移動することはできません。

void functionA() {
    goto target; // コンパイルエラー
}

void functionB() {
target:
    ;
}

target:が別の関数にあるため、このコードは不正です。

別のソースファイルや別のクラスのメンバー関数へ移動することもできません。

goto文と変数の初期化

C++でgoto文を使用する際は、変数の初期化に注意が必要です。

初期化を飛び越えるジャンプ

次のコードはコンパイルエラーになります。

#include <iostream>
#include <string>

int main() {
    goto label;

    std::string message = "Hello";

label:
    std::cout << "終了\n";

    return 0;
}

goto label;によって、std::string messageの初期化を飛び越えています。

std::stringはコンストラクタによる初期化が必要なため、その初期化を実行せずに変数のスコープへ入ることはできません。

整数型であっても、初期値が指定されている場合は同様です。

int main() {
    goto label;

    int value = 10;

label:
    return 0;
}

このコードも、valueの初期化を飛び越えるため不正です。

初期化子のない単純な変数

一方、初期化子を持たない単純な変数は、標準上許される場合があります。

int main() {
    goto label;

    int value;

label:
    return 0;
}

ただし、valueは有効な値で初期化されていません。

そのため、ジャンプ後にvalueを読み取ると、未初期化値を使用する危険があります。

int main() {
    goto label;

    int value;

label:
    // valueの値を読み取るのは危険
    return 0;
}

文法上許される場合があっても、実務では変数宣言を飛び越えるgoto文は避けるのが安全です。

ブロックでスコープを分ける方法

変数のスコープを独立したブロックに限定することで、不正なジャンプを避けられます。

#include <iostream>
#include <string>

int main() {
    goto label;

    {
        std::string message = "Hello";
        std::cout << message << '\n';
    }

label:
    std::cout << "終了\n";

    return 0;
}

このコードでは、messageのスコープは内側のブロックだけです。

ジャンプ先のlabel:は、そのブロックの外にあるため、messageのスコープへ途中から入ることにはなりません。

goto文でスコープを抜けた場合

goto文によってスコープの外へ移動することは可能です。

その場合、構築済みの自動変数は適切に破棄されます。

#include <iostream>
#include <string>

int main() {
    {
        std::string message = "処理中";
        std::cout << message << '\n';

        goto finish;
    }

finish:
    std::cout << "終了\n";

    return 0;
}

goto finish;でブロックを抜ける際、messageのデストラクタが呼び出されます。

デストラクタの呼び出しを確認する例

#include <iostream>

class Sample {
public:
    Sample() {
        std::cout << "コンストラクタ\n";
    }

    ~Sample() {
        std::cout << "デストラクタ\n";
    }
};

int main() {
    {
        Sample object;

        std::cout << "ジャンプします\n";
        goto finish;
    }

finish:
    std::cout << "終了\n";

    return 0;
}

実行結果は次のようになります。

コンストラクタ
ジャンプします
デストラクタ
終了

goto文でブロックを抜けても、デストラクタは無視されません。

複数のオブジェクトがある場合

複数のオブジェクトがある場合は、構築された順番と逆の順番で破棄されます。

#include <iostream>

class Sample {
private:
    const char* name_;

public:
    explicit Sample(const char* name) : name_(name) {
        std::cout << name_ << "を構築\n";
    }

    ~Sample() {
        std::cout << name_ << "を破棄\n";
    }
};

int main() {
    {
        Sample first("first");
        Sample second("second");

        goto finish;
    }

finish:
    std::cout << "終了\n";

    return 0;
}

実行結果は次のようになります。

firstを構築
secondを構築
secondを破棄
firstを破棄
終了

多重ループから抜ける場合のgoto文

C++のbreak文は、最も内側にあるループまたはswitch文だけを終了します。

for (int i = 0; i < 10; ++i) {
    for (int j = 0; j < 10; ++j) {
        if (条件) {
            break;
        }
    }
}

このbreak文で抜けられるのは、内側のfor文だけです。

外側のループも一度に抜けたい場合、goto文を使用する方法があります。

#include <iostream>
#include <vector>

int main() {
    const std::vector<std::vector<int>> matrix{
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };

    const int target = 8;
    bool found = false;

    for (std::size_t row = 0; row < matrix.size(); ++row) {
        for (std::size_t column = 0;
             column < matrix[row].size();
             ++column) {
            if (matrix[row][column] == target) {
                found = true;
                goto search_finished;
            }
        }
    }

search_finished:
    if (found) {
        std::cout << "見つかりました\n";
    } else {
        std::cout << "見つかりませんでした\n";
    }

    return 0;
}

このような使い方は、goto文が比較的分かりやすく機能する例です。

フラグを使用する方法

goto文を使わず、フラグによって外側のループを終了することもできます。

#include <iostream>

int main() {
    bool found = false;

    for (int i = 0; i < 5 && !found; ++i) {
        for (int j = 0; j < 5; ++j) {
            if (i + j == 7) {
                found = true;
                break;
            }
        }
    }

    if (found) {
        std::cout << "見つかりました\n";
    }

    return 0;
}

ただし、ループが3重、4重になると、フラグを複数の階層へ伝える必要があり、コードが複雑になる場合があります。

関数に分割してreturnする方法

処理を関数に分割すれば、return文で多重ループから抜けられます。

#include <vector>

bool containsValue(
    const std::vector<std::vector<int>>& matrix,
    int target
) {
    for (const auto& row : matrix) {
        for (int value : row) {
            if (value == target) {
                return true;
            }
        }
    }

    return false;
}

実務では、検索処理を関数へ切り出す方法が分かりやすいことが多いです。

エラー処理で使用されるgoto文

C言語では、取得したリソースをまとめて解放するために、goto文がよく使われます。

ResourceA* resource_a = acquire_a();

if (resource_a == NULL) {
    goto error;
}

ResourceB* resource_b = acquire_b();

if (resource_b == NULL) {
    goto cleanup_a;
}

/* メイン処理 */

release_b(resource_b);

cleanup_a:
release_a(resource_a);

error:
return;

リソースの取得に失敗した場合、取得済みのリソースだけを逆順に解放できます。

C++ではRAIIを優先する

C++では、通常はgoto文による後片付けよりもRAIIを使用します。

RAIIとは、オブジェクトの寿命とリソース管理を結び付ける設計方法です。

#include <fstream>
#include <stdexcept>

void process() {
    std::ifstream file("data.txt");

    if (!file) {
        throw std::runtime_error("ファイルを開けません");
    }

    // 処理
}

std::ifstreamは、関数を終了すると自動的にファイルを閉じます。

途中でreturnした場合や例外が発生した場合でも、デストラクタが呼び出されます。

動的メモリについても、通常はスマートポインタを使用します。

#include <memory>

void process() {
    auto value = std::make_unique<int>(100);

    // 関数終了時に自動解放される
}

そのため、C++では後片付けのためにgoto文が必要になる場面は、C言語よりも少なくなります。

goto文とbreak文の違い

break文は、現在のループまたはswitch文を終了します。

for (int i = 0; i < 10; ++i) {
    if (i == 5) {
        break;
    }
}

一方、goto文は指定したラベルへ移動します。

for (int i = 0; i < 10; ++i) {
    if (i == 5) {
        goto finish;
    }
}

finish:
    ;

主な違いは次のとおりです。

項目breakgoto
移動先現在のループまたはswitch文の直後指定したラベル
使用できる場所ループ、switch文同じ関数内
多重ループ1段だけ抜ける複数のループを一度に抜けられる
可読性比較的高い使い方によって低下しやすい

goto文とcontinue文の違い

continue文は、現在のループの残りの処理を飛ばし、次の反復へ進みます。

for (int i = 0; i < 5; ++i) {
    if (i == 2) {
        continue;
    }

    std::cout << i << '\n';
}

実行結果は次のようになります。

0
1
3
4

goto文でも似た処理を作れます。

for (int i = 0; i < 5; ++i) {
    if (i == 2) {
        goto next;
    }

    std::cout << i << '\n';

next:
    ;
}

ただし、この場合はcontinue文を使うほうが目的が明確です。

goto文とreturn文の違い

return文は、現在の関数を終了します。

void process(int value) {
    if (value < 0) {
        return;
    }

    std::cout << value << '\n';
}

一方、goto文は、関数内の別の位置へ移動するだけです。

void process(int value) {
    if (value < 0) {
        goto finish;
    }

    std::cout << value << '\n';

finish:
    std::cout << "処理終了\n";
}

関数を終了して問題ない場合は、通常はreturn文を使うほうが分かりやすくなります。

goto文と例外処理の違い

例外処理は、異常状態を呼び出し元へ伝えるための仕組みです。

#include <stdexcept>

void process(int value) {
    if (value < 0) {
        throw std::invalid_argument("負の値です");
    }
}

goto文は同じ関数内でしか使用できません。

void process(int value) {
    if (value < 0) {
        goto error;
    }

    return;

error:
    // 同じ関数内のエラー処理
    return;
}

両者の役割は次のように異なります。

  • goto文:同じ関数内の制御移動
  • 例外処理:関数呼び出し階層を越えたエラー通知
  • return文:現在の関数の終了

goto文とswitch文のcaseラベル

switch文のcasedefaultもラベルの一種ですが、通常のgoto文で使用する識別子ラベルとは役割が異なります。

switch (value) {
case 1:
    std::cout << "1\n";
    break;

case 2:
    std::cout << "2\n";
    break;

default:
    break;
}

次のように、goto文で特定のcaseへ直接移動する構文はありません。

goto case 1; // 不正

標準C++では、goto文の後ろには識別子ラベルを指定します。

goto finish;

finish:
    ;

標準C++にcomputed gotoはない

GCCなど一部のコンパイラには、ラベルのアドレスを使用するcomputed gotoという拡張機能があります。

void* destination = &&target;
goto *destination;

target:
    ;

しかし、これは標準C++の構文ではありません。

標準C++のgoto文は、次の形式です。

goto ラベル名;

computed gotoはコンパイラ拡張に依存するため、移植性が必要なプログラムでは避けるべきです。

インタープリターや仮想マシンの命令ディスパッチなど、特殊な低レベル処理で使用されることがあります。

goto文が避けられる理由

goto文はC++で使用できる正式な構文ですが、一般的なアプリケーションコードでは避けられる傾向があります。

処理の流れを追いにくくなる

通常の制御構文は、上から下へ処理を追いやすくなっています。

if (condition) {
    processA();
} else {
    processB();
}

一方、goto文が多いコードでは、実行位置が前後に移動します。

goto check;

retry:
    processA();
    goto finish;

check:
    if (condition) {
        goto retry;
    }

finish:
    ;

ジャンプ先を探しながら読む必要があるため、処理の理解に時間がかかります。

修正の影響が分かりにくい

ラベルとgoto文の間に変数を追加すると、初期化を飛び越える問題が発生することがあります。

goto finish;

std::string message = "Hello";

finish:
    ;

もともと動作していたコードでも、変数の追加によってコンパイルできなくなる可能性があります。

無限ループを作りやすい

start:
    std::cout << "繰り返し\n";
    goto start;

これは終了条件のない無限ループです。

同じ処理はwhile文でも書けます。

while (true) {
    std::cout << "繰り返し\n";
}

while (true)のほうが、意図的な無限ループであることが明確です。

制御構造が複雑になりやすい

現代的なC++では、処理を次のような構造で整理します。

  • if文による条件分岐
  • for文やwhile文による繰り返し
  • 関数への分割
  • クラスによる責務の整理
  • 例外処理
  • RAIIによるリソース管理

goto文を乱用すると、これらの構造を越えて処理が移動し、コード全体の見通しが悪くなる可能性があります。

スパゲッティコードとは

goto文などによって処理の流れが複雑に絡み合ったコードは、スパゲッティコードと呼ばれることがあります。

#include <iostream>

int main() {
    int value = 0;

start:
    if (value == 0) {
        goto first;
    }

    if (value == 1) {
        goto second;
    }

    goto finish;

first:
    std::cout << "first\n";
    value = 1;
    goto start;

second:
    std::cout << "second\n";
    value = 2;
    goto start;

finish:
    return 0;
}

この程度であれば動作を追えますが、ラベルやジャンプが増えるほど理解が難しくなります。

ただし、goto文を使っただけで必ずスパゲッティコードになるわけではありません。

問題になるのは、複数のラベルが相互に行き来し、処理の入口と出口が分かりにくくなることです。

goto文が許容されやすい場面

goto文は、すべての場面で禁止される構文ではありません。

限定的な用途では、他の方法より分かりやすくなることがあります。

多重ループを一括で抜ける場合

for (...) {
    for (...) {
        if (found) {
            goto search_finished;
        }
    }
}

search_finished:
    ;

フラグを複数のループへ伝えるよりも、処理が単純になる場合があります。

低レベルなエラー処理

OS、組み込みシステム、デバイスドライバー、C言語との互換コードなどでは、複数のリソースを逆順に解放するために使用されることがあります。

ただし、通常のC++ではRAIIを優先します。

自動生成コード

パーサーや状態機械などの自動生成コードでは、生成処理の単純化や性能上の理由からgoto文が使われる場合があります。

人間が日常的に保守するコードとは目的が異なるため、一般的なアプリケーションコードと同じ基準で評価する必要はありません。

特殊な状態機械

状態に応じて処理を移動する低レベルな実装で使用されることがあります。

ただし、一般的にはswitch文とループを使うほうが分かりやすくなります。

#include <iostream>

int main() {
    int state = 0;
    bool running = true;

    while (running) {
        switch (state) {
        case 0:
            std::cout << "状態0\n";
            state = 1;
            break;

        case 1:
            std::cout << "状態1\n";
            state = 2;
            break;

        case 2:
            std::cout << "状態2\n";
            running = false;
            break;

        default:
            running = false;
            break;
        }
    }

    return 0;
}

goto文の主な代替手段

多くの処理は、goto文を使わずに表現できます。

条件分岐にはif文を使う

if (condition) {
    processA();
} else {
    processB();
}

繰り返しにはfor文やwhile文を使う

while (condition) {
    process();
}

ループの終了にはbreak文を使う

while (true) {
    if (condition) {
        break;
    }
}

次の反復にはcontinue文を使う

for (int i = 0; i < 10; ++i) {
    if (i % 2 == 0) {
        continue;
    }

    std::cout << i << '\n';
}

関数の終了にはreturn文を使う

void process(int value) {
    if (value < 0) {
        return;
    }

    // 正常処理
}

複雑な処理は関数に分割する

bool processItems() {
    for (...) {
        for (...) {
            if (error) {
                return false;
            }
        }
    }

    return true;
}

局所的な処理にはラムダ式を使う

#include <iostream>

int main() {
    bool found = [&]() {
        for (int i = 0; i < 10; ++i) {
            for (int j = 0; j < 10; ++j) {
                if (i + j == 12) {
                    return true;
                }
            }
        }

        return false;
    }();

    std::cout << std::boolalpha << found << '\n';

    return 0;
}

エラー通知には戻り値や例外を使う

#include <stdexcept>

void process() {
    if (エラー条件) {
        throw std::runtime_error("処理に失敗しました");
    }
}

実務でgoto文を使う場合の注意点

実務でgoto文を使用する場合は、制御フローを複雑にしないように注意する必要があります。

狭い範囲で使用する

ラベルとgoto文が大きく離れていると、処理を追いにくくなります。

できるだけ短い範囲に限定します。

前方ジャンプを基本にする

後方ジャンプはループのような動作を作るため、制御フローが複雑になりやすくなります。

特別な理由がなければ、終了地点への前方ジャンプに限定するほうが安全です。

ラベル名に目的を表す

意味の分からないラベル名は避けます。

goto label1;

label1:
    ;

目的が分かる名前を使用します。

goto cleanup;

cleanup:
    ;

または、次のように記述します。

goto search_finished;

search_finished:
    ;

多数のラベルを作らない

複数のラベルが相互に行き来すると、コードの流れが分かりにくくなります。

変数の初期化を飛び越えない

特に、コンストラクタを持つオブジェクトや初期値を持つ変数の宣言を飛び越えないようにします。

RAIIやreturn文で置き換えられないか確認する

リソース解放にはRAII、早期終了にはreturn文、多重ループの終了には関数分割を優先して検討します。

使用理由をコメントで残す

goto文を使う理由が明確でない場合、後から修正する人が意図を理解できない可能性があります。

// 3重ループ全体を終了するため、局所的にgotoを使用する
goto search_finished;

比較的分かりやすいgoto文の例

bool found = false;

for (int row = 0; row < rows; ++row) {
    for (int column = 0; column < columns; ++column) {
        if (matrix[row][column] == target) {
            found = true;
            goto search_finished;
        }
    }
}

search_finished:
    if (found) {
        std::cout << "対象を発見しました\n";
    }

このコードには次の特徴があります。

  • ジャンプ方向が前方だけ
  • ジャンプ先が一つだけ
  • 多重ループから抜けるという目的が明確
  • ラベル名から役割が分かる
  • ジャンプ範囲が短い
  • 複数のラベルが相互に移動しない

このような使い方であれば、goto文が必ずしも可読性を大きく下げるとは限りません。

避けるべきgoto文の例

start:
    if (conditionA) {
        goto middle;
    }

    goto finish;

middle:
    if (conditionB) {
        goto start;
    }

    goto another;

another:
    if (conditionC) {
        goto middle;
    }

finish:
    ;

このコードでは、前方ジャンプと後方ジャンプが混在しています。

複数のラベルが相互に行き来しているため、処理の流れを把握しにくくなっています。

このような場合は、次の方法への書き換えを検討します。

  • 関数への分割
  • if文やswitch文による整理
  • ループ構造の見直し
  • 状態機械としての再設計
  • クラスや関数による責務の分離

まとめ

C++のgoto文は、同じ関数内にある指定したラベルへ実行位置を移動する構文です。

goto destination;

// 途中の処理

destination:
    ;

主なポイントは次のとおりです。

  • goto文は同じ関数内でのみ使用できる
  • 前方ジャンプと後方ジャンプの両方が可能
  • ラベルはコード上で登場する前から参照できる
  • 初期化が必要な変数を飛び越えて、その変数のスコープへ入ることはできない
  • 初期化子のない単純な変数は許される場合があるが、未初期化値の使用に注意が必要
  • スコープから外へジャンプすると、構築済みオブジェクトのデストラクタが呼び出される
  • 多重ループから一度に抜ける用途では、局所的に使用されることがある
  • 一般的にはifforwhilebreakcontinuereturn、関数分割、例外処理、RAIIを優先する
  • computed gotoは標準C++ではなく、コンパイラ拡張である
  • 乱用すると、処理の流れが分かりにくいスパゲッティコードになりやすい

goto文は、絶対に使用してはいけない構文ではありません。

ただし、他の制御構文よりも明らかに処理が単純になる限定的な場面で、狭い範囲に慎重に使用することが重要です。

以上、C++のgoto文についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次