導入: なぜ今さら「goto」なのか
C++において、goto文は「悪の根源」として語られることが多く、コードレビューの場で遭遇すると反射的に修正を求められることも少なくありません。しかし、レガシーコードの保守や、極限までパフォーマンスが求められる組み込みシステム、あるいは特定のクリーンアップ処理においては、gotoが解決策となるケースが稀に存在します。本記事では、gotoの基本動作を理解した上で、実務でどのように扱うべきか、また現代的なC++ではどう代替すべきかを解説します。
基礎知識: goto文とラベルの仕組み
goto文は、同一関数内の「ラベル」が指定された位置へ、無条件に実行フローをジャンプさせる制御構造です。
ラベルは「識別子:」の形式で定義し、プログラムの特定の箇所に目印を付けます。コンパイラはラベルの位置をアドレスとして認識し、プログラムカウンタを強引に書き換えることで処理を飛ばします。しかし、これは構造化プログラミングの原則である「関数単位での処理の流れ」を分断するため、制御フローの追跡が困難になり、いわゆる「スパゲティコード(コードが複雑に絡み合った状態)」を招く要因となります。
実装/解決策: 現代的なアプローチとの比較
C++において、gotoの最も一般的な利用例は「エラー発生時のリソース解放(クリーンアップ)」です。しかし、C++にはRAII(Resource Acquisition Is Initialization)という強力な概念があります。デストラクタを活用することで、gotoを使わずに安全にリソースを解放するのが現代の標準です。どうしてもgotoを使う場合は、「関数内の後方(終了処理)へのジャンプ」という限定的な用途に留めるのが、実務における暗黙の了解です。
サンプルプログラム: gotoとRAIIの比較
以下は、ファイル操作を例にした実装例です。
include
include
// 古い手法:gotoによる終了処理
void process_file_legacy(const char filename) {
FILE fp = fopen(filename, “r”);
if (!fp) return;
if (/ 何らかのエラー発生 / false) {
goto error_cleanup;
}
// 処理…
error_cleanup:
// 終了処理を共通化するためにgotoを使用
fclose(fp);
std::cout << "Legacy cleanup executed." << std::endl;
}
// 現代的な手法:RAIIを用いた安全な実装
void process_file_modern(const char filename) {
// std::ifstreamはデストラクタで自動的にファイルを閉じる
std::ifstream file(filename);
if (!file.is_open()) return;
// 途中で関数を抜けても、fileのデストラクタが自動的に呼ばれる
if (/ 何らかのエラー発生 / false) {
return;
}
}
応用・注意点: 陥りやすい罠と回避策
実務でgotoを使用する際に最も注意すべきは、「変数の初期化を飛び越える」というバグです。C++では、変数が宣言された場所を飛び越えてgotoでジャンプすることは禁止されており、コンパイルエラーになります。また、ジャンプ先のスコープで変数が正しく初期化されていない状態で処理が続行されるリスクもあります。
結論として:
1. 新規コードでは、まずRAII(std::unique_ptrやstd::fstreamなど)で解決できないかを検討してください。
2. 既存のレガシーコードでgotoが使われている場合、単に削除するのではなく、そのジャンプが「何を守っているのか(排他制御の解除やメモリ解放など)」を慎重に調査してください。
3. gotoを使うなら、「前方へのジャンプ(後方処理)」のみに限定し、ループ内への飛び込みなどは絶対に行わないというルールをチーム内で徹底しましょう。

コメント