C++の開発において、goto文は古くから存在する制御構文の一つです。
しかし、プログラミングの初学者からベテランエンジニアに至るまで、「goto文は使うべきではない」という教えを一度は耳にしたことがあるのではないでしょうか。
エドガー・ダイクストラ氏による有名な論文「Go To Statement Considered Harmful」以来、構造化プログラミングの観点から忌避されてきた歴史があります。
しかし、現代のC++においてもgoto文は仕様として残されており、特定の条件下では他の構文よりも簡潔で読みやすいコードを実現できるケースが存在します。
本記事では、C++におけるgoto文の基本的な使い方から、多重ループの脱出といった具体的な活用シーン、そして使用上の注意点や制限事項について、テクニカルな視点から詳しく解説します。
C++におけるgoto文の基本構文
goto文は、プログラムの実行フローを指定したラベルの位置へ無条件にジャンプさせるための命令です。
基本的な構文は非常にシンプルで、移動先を示す「ラベル」の定義と、そのラベルへ移動を命じる「goto」キーワードのペアで構成されます。
ラベルの定義とジャンプの方法
ラベルは識別子の後ろにコロン : を付けることで定義します。
ラベルは、同一関数内であればどこにでも配置することが可能です。
#include <iostream>
int main() {
std::cout << "処理を開始します。" << std::endl;
// jump_targetという名前のラベルへ移動
goto jump_target;
// この行はスキップされる
std::cout << "このメッセージは表示されません。" << std::endl;
jump_target:
std::cout << "ジャンプ先に到達しました。" << std::endl;
return 0;
}
処理を開始します。
ジャンプ先に到達しました。
このように、goto文が実行されると、その間の処理はすべて無視され、即座にラベルの直後の命令へと制御が移ります。
なぜgoto文は「悪」とされるのか
活用シーンを解説する前に、なぜこれほどまでにgoto文が嫌われているのかを理解しておく必要があります。
その最大の理由は、プログラムの制御構造を破壊し、可読性を著しく低下させるためです。
スパゲッティコードの誘発
goto文を多用すると、処理が上下左右に飛び交い、どのような順番でコードが実行されているのかを追跡することが困難になります。
このような状態を「スパゲッティコード」と呼びます。
現代のプログラミングでは、if、for、whileといった構造化された制御構文を使うことで、コードの流れを上から下へと論理的に保つことが推奨されています。
メンテナンス性の低下
後からコードを修正する際、gotoによってどこからジャンプしてくるかわからない状態では、変数の状態や初期化のタイミングを正確に把握できず、予期せぬバグを引き起こすリスクが高まります。
そのため、基本的には「使わなくて済むなら使わない」というのが現代のC++における鉄則です。
goto文が有効な活用シーン:多重ループからの脱出
原則として使用を控えるべきgoto文ですが、C++において唯一と言っていいほど「合理的である」とされるシーンがあります。
それが多重ループ(ネストされたループ)からの即時脱出です。
ネストされたループにおける課題
C++のbreak文は、現在実行中の最も内側のループしか抜けることができません。
3重、4重に重なったループの奥深くから一気に外側へ抜けたい場合、通常の構文だけでは「フラグ変数」を用意し、各階層でチェックを行う必要があります。
まずは、gotoを使わない場合のコードを見てみましょう。
#include <iostream>
int main() {
bool found = false;
for (int i = 0; i < 10; ++i) {
for (int j = 0; j < 10; ++j) {
for (int k = 0; k < 10; ++k) {
if (i == 2 && j == 3 && k == 4) {
std::cout << "ターゲット発見: i=" << i << ", j=" << j << ", k=" << k << std::endl;
found = true;
break; // kのループを抜ける
}
}
if (found) break; // jのループを抜ける
}
if (found) break; // iのループを抜ける
}
std::cout << "探索終了" << std::endl;
return 0;
}
このように、フラグ変数の管理と各階層での条件分岐が必要になり、コードが冗長になります。
これをgoto文を使って書き換えると、以下のようになります。
goto文による簡潔な脱出
goto文を使えば、フラグ変数を用意することなく、一撃でループの外側へジャンプできます。
#include <iostream>
int main() {
for (int i = 0; i < 10; ++i) {
for (int j = 0; j < 10; ++j) {
for (int k = 0; k < 10; ++k) {
if (i == 2 && j == 3 && k == 4) {
std::cout << "ターゲット発見: i=" << i << ", j=" << j << ", k=" << k << std::endl;
goto end_search; // 全てのループを一気に抜ける
}
}
}
}
end_search:
std::cout << "探索終了" << std::endl;
return 0;
}
ターゲット発見: i=2, j=3, k=4
探索終了
このケースでは、goto文を使用することでコードのネストが浅くなり、論理構成が非常にすっきりすることがわかります。
Google C++ Style Guideなどの主要なコーディング規約においても、このような限定的な状況での使用は許容される傾向にあります。
エラーハンドリングにおけるgoto文(C言語的アプローチ)
C言語の古いソースコードや、低レイヤーのライブラリ開発では、リソースの解放を一箇所にまとめる目的でgoto文が使われることがあります。
void process_data() {
FILE* file = fopen("data.txt", "r");
if (!file) goto error_cleanup;
void* buffer = malloc(1024);
if (!buffer) goto error_cleanup;
// 何らかの処理
// ...
free(buffer);
fclose(file);
return;
error_cleanup:
if (buffer) free(buffer);
if (file) fclose(file);
std::cerr << "エラーが発生しました。" << std::endl;
}
ただし、現代のC++ではRAII (Resource Acquisition Is Initialization)やデストラクタ、スマートポインタ(std::unique_ptrなど)が提供されているため、この目的でgotoを使う必要性はほとんどありません。
例外処理(try-catch)も存在するため、C++らしい設計を優先すべきです。
C++特有の制約:変数の初期化を跨ぐジャンプ
C++でgoto文を使用する際、最も注意しなければならないのが変数の初期化をスキップしてはいけないという言語仕様上の制約です。
C++では、変数が定義された瞬間にコンストラクタが呼ばれるなどの初期化処理が行われます。
もしgoto文によってその初期化を飛び越えてしまい、その後の処理でその変数を使用しようとすると、プログラムの安全性が損なわれます。
そのため、コンパイラはこのようなジャンプをエラーとして検出します。
エラーになる例
#include <iostream>
void invalid_goto() {
goto skip_init;
// 変数の初期化を跨いでジャンプしようとしている
int value = 100;
skip_init:
// ここでvalueが使われる可能性があるため、コンパイルエラーになる
std::cout << "処理を終了します。" << std::endl;
}
このコードをコンパイルしようとすると、多くの環境で「error: jump to label 'skip_init' jumps across variable initialization」といったエラーメッセージが表示されます。
対処法
この制約を回避するには、変数のスコープを波括弧 {} で囲って限定するか、goto文よりも前に変数を宣言しておく必要があります。
| 対処法 | 内容 |
|---|---|
| ブロック化 | 変数の宣言を { ... } で囲み、ジャンプがそのスコープに干渉しないようにする。 |
| 宣言の移動 | 変数の宣言を関数やスコープの先頭(gotoより前)に移動させる。 |
goto文の代替手段と使い分け
goto文を使いたいと感じたとき、まずは他の手段で解決できないかを検討するのがプロのテクニカルライティング的思考です。
1. 関数化とreturn文
多重ループからの脱出は、そのループ処理自体を独立した「関数」に切り出すことで、return文による脱出に置き換えることができます。
bool find_target() {
for (int i = 0; i < 10; ++i) {
for (int j = 0; j < 10; ++j) {
if (i == 2 && j == 3) return true; // 関数ごと抜ける
}
}
return false;
}
2. 標準アルゴリズムの利用
std::any_of や std::find_if といった標準ライブラリのアルゴリズム(特にラムダ式と組み合わせたもの)を活用することで、ループそのものを抽象化し、gotoの必要性を排除できます。
3. 状態遷移マシン(State Machine)
複雑なジャンプが必要なロジックの場合、switch-case文を用いた状態遷移マシンとして設計し直す方が、将来的なメンテナンス性が大幅に向上します。
goto文使用時のベストプラクティス
もし、どうしてもgoto文を使う必要があると判断した場合は、以下のルールを厳守しましょう。
- ジャンプ先は下方向(前方)にする
上方向へのジャンプはループを自作していることになり、コードの解読が極端に難しくなります。
- ジャンプは同一関数内に留める
そもそもC++のgotoは関数を跨げませんが、setjmp/longjmpなどの利用は避けるべきです。
- ラベル名を明確にする
label1:のような意味のない名前ではなく、on_error:やexit_loop:など、目的がひと目で分かる名前にします。
- 多用しない
一つの関数内で複数のgotoが入り乱れる状況は、設計自体に問題があるサインです。
まとめ
C++のgoto文は、かつて言われていたような「絶対的な禁じ手」ではありません。
特に深いネストからの脱出においては、フラグ変数を管理するよりもコードをシンプルに保つための有効な手段となり得ます。
しかし、その強力なジャンプ能力は、使い方を誤ればプログラムの構造を破壊し、デバッグ困難な「負の遺産」を生み出す諸刃の剣でもあります。
現代のC++開発においては、RAIIや例外処理、スマートポインタ、ラムダ式といった強力な代替機能が揃っています。
まずはこれらの現代的な機能を優先的に検討し、どうしても他にスマートな解決策がない場合に限って、慎重に、かつ限定的にgoto文を活用するようにしましょう。
「正しい道具を、正しい場所で使う」というバランス感覚こそが、優れたC++エンジニアに求められる資質です。






