閉じる

C++で複数の例外をcatchする方法|記述順序の注意点と書き方

C++を用いた開発において、予期せぬエラーへの対応はプログラムの堅牢性を左右する極めて重要な要素です。

特に、一つの処理ブロックから発生し得るエラーが多岐にわたる場合、複数の例外を適切に捕捉(catch)し、それぞれに応じた処理を切り分ける技術が必要不可欠となります。

本記事では、C++で複数の例外を処理するための構文から、実務で陥りやすい記述順序の注意点、そして標準例外クラスの活用方法までを詳しく解説します。

複数の例外をcatchする基本構文

C++の例外処理は、tryブロック内で発生した例外を、直後のcatchブロックで受け取る仕組みです。

一つのtryに対して複数のcatchを記述することで、投げられた型に応じた個別のエラーハンドリングが可能になります。

catchブロックの連結

複数の例外を捕捉する場合、tryブロックのすぐ後ろに複数のcatchを連続して記述します。

実行時に例外がスローされると、上から順番に型の一致が確認され、最初に型が一致したブロックが実行されます。

サンプルプログラム:基本の複数catch

C++
#include <iostream>
#include <string>

// ダミーの処理関数
void process(int mode) {
    if (mode == 1) {
        throw 404; // int型の例外をスロー
    } else if (mode == 2) {
        throw std::string("File not found"); // string型の例外をスロー
    } else if (mode == 3) {
        throw 3.14; // double型の例外をスロー
    }
}

int main() {
    for (int i = 1; i <= 3; ++i) {
        try {
            std::cout << "Mode " << i << " 実行中..." << std::endl;
            process(i);
        } catch (int e) {
            // int型の例外をキャッチ
            std::cout << "エラー(int): コード " << e << " が発生しました。" << std::endl;
        } catch (const std::string& e) {
            // std::string型の例外をキャッチ
            std::cout << "エラー(string): " << e << " が発生しました。" << std::endl;
        } catch (...) {
            // 上記以外のすべての例外をキャッチ(キャッチオール)
            std::cout << "予期せぬエラーが発生しました。" << std::endl;
        }
    }
    return 0;
}
実行結果
Mode 1 実行中...
エラー(int): コード 404 が発生しました。
Mode 2 実行中...
エラー(string): File not found が発生しました。
Mode 3 実行中...
予期せぬエラーが発生しました。

上記のように、投げられた型に合わせて適切な処理が選択されます。

最後に記述されているcatch (...)省略記号(エクリシス)を使用した特殊なハンドラで、どんな型でもキャッチできる「キャッチオール」と呼ばれるものです。

記述順序における重要なルール

複数のcatchを並べる際、最も注意しなければならないのが記述する「順序」です。

C++の例外処理は、最初に見つかった「適合する型」のブロックで処理を終了します。

派生クラスから先に記述する

C++の例外処理では、クラスの継承関係がある場合、「派生クラス(子)」を「基底クラス(親)」よりも先に記述する必要があります。

基底クラスの参照やポインタは、その派生クラスのオブジェクトも受け取ることができるため、基底クラスを先に書いてしまうと、派生クラス専用のハンドラが決して実行されない「デッドコード」になってしまいます。

サンプルプログラム:継承関係がある場合のcatch

C++
#include <iostream>
#include <exception>
#include <stdexcept>

int main() {
    try {
        // std::runtime_errorはstd::exceptionを継承している
        throw std::runtime_error("ランタイムエラーが発生");
    } 
    // 注意:具体的な派生クラスを先に書く
    catch (const std::runtime_error& e) {
        std::cout << "具体的なエラーを補足: " << e.what() << std::endl;
    }
    // 基底クラスは後に書く
    catch (const std::exception& e) {
        std::cout << "一般的なエラーを補足: " << e.what() << std::endl;
    }

    return 0;
}
実行結果
具体的なエラーを補足: ランタイムエラーが発生

もしこの順序を逆にして、std::exceptionを先に記述してしまうと、std::runtime_errorが投げられた際もstd::exceptionのブロックでキャッチされてしまいます。

コンパイラによっては警告を出してくれることもありますが、論理的なバグの原因になるため、必ず「狭い範囲(具体的)」から「広い範囲(抽象的)」の順で記述しましょう。

標準例外クラスの階層構造

C++の標準ライブラリ(<stdexcept>など)では、さまざまな例外クラスが提供されています。

これらはすべてstd::exceptionを頂点とした継承ツリーを構成しています。

クラス名概要主な派生クラス
std::exceptionすべての標準例外の基底クラス全般
std::logic_errorプログラムの論理的な誤りinvalid_argument, out_of_range
std::runtime_error実行時にしか判明しない誤りoverflow_error, range_error
std::bad_allocメモリ確保失敗(new失敗時)

これらの階層を意識することで、「特定の論理エラーだけ個別に扱い、それ以外は一括で処理する」といった柔軟な設計が可能になります。

全ての例外をキャッチする「catch (…)」

プログラムの予期せぬ強制終了を防ぐために、最後に「どんな例外でも捕まえる」ブロックを用意することがあります。

これが前述したcatch (...)です。

キャッチオールの役割と注意点

キャッチオールハンドラ内では、投げられたオブジェクトの型情報や値を取得することができません。

そのため、詳細なログを出力することは難しいですが、「システムを安全に終了させる」あるいは「リソースを確実に解放する」といった最低限のクリーンアップ処理を行うのに適しています。

サンプルプログラム:キャッチオールの活用

C++
#include <iostream>

void riskyFunction() {
    // 外部ライブラリなどが何を投げるか分からない場合を想定
    throw "Unexpected Error"; 
}

int main() {
    try {
        riskyFunction();
    } catch (int e) {
        std::cout << "Int error: " << e << std::endl;
    } catch (...) {
        // 全ての例外をここで食い止める
        std::cerr << "不明な例外が発生しました。安全にシャットダウンします。" << std::endl;
    }
    return 0;
}
実行結果
不明な例外が発生しました。安全にシャットダウンします。

例外の再スローと複数キャッチの組み合わせ

複数の例外をキャッチする中で、一部の処理だけ自分で行い、残りの判断を呼び出し元(上位の関数)に任せたい場合があります。

その際に使用するのがthrow;(引数なしのthrow)による再スローです。

再スローによる責任の分担

特定の型だけをログに記録し、例外自体は解消せずにそのまま外側へ放り投げることで、多層的なエラーハンドリングを実現できます。

サンプルプログラム:再スロー

C++
#include <iostream>
#include <stdexcept>

void nestedFunction() {
    try {
        throw std::out_of_range("インデックスが範囲外です");
    } catch (const std::out_of_range& e) {
        std::cout << "nestedFunctionでログ記録: " << e.what() << std::endl;
        // 例外をそのまま外側に投げる
        throw; 
    }
}

int main() {
    try {
        nestedFunction();
    } catch (const std::exception& e) {
        std::cout << "mainで最終的なエラー処理: " << e.what() << std::endl;
    }
    return 0;
}
実行結果
nestedFunctionでログ記録: インデックスが範囲外です
mainで最終的なエラー処理: インデックスが範囲外です

再スローを行う際、throw e;のようにオブジェクトを指定してしまうと、コピーが発生したり、オブジェクトの型がスライス(情報欠落)されたりする危険があります。

単に上の階層へ渡すだけなら、引数なしのthrow;を使うのが鉄則です。

現場で役立つベストプラクティス

複数の例外を扱う際、コードの可読性と保守性を高めるためのポイントをまとめました。

1. 例外は「const参照」でキャッチする

catch (std::exception e)のように値渡しでキャッチすると、コピーコストが発生するだけでなく、派生クラスの情報が失われる「スライス問題」が発生します。

必ずconst T&の形式で受け取るようにしましょう。

2. 標準例外をベースにする

独自の例外クラスを作成する場合でも、std::exceptionstd::runtime_errorを継承させるのが望ましいです。

これにより、既存のcatch (const std::exception& e)という共通の網でキャッチできるようになります。

3. キャッチオールの乱用を避ける

catch (...)は便利ですが、本来個別に処理すべきエラーまで隠蔽してしまう可能性があります。

可能な限り具体的な型でキャッチし、どうしても不明なエラーに備える場合のみ、最後に記述するようにしてください。

4. noexceptとの兼ね合い

C++11以降、例外を投げない関数にはnoexceptを指定できます。

複数の例外処理を記述する場合、呼び出す関数が例外を投げる可能性があるかどうかを型システムとして明示することも重要です。

まとめ

C++において複数の例外をcatchする方法は、単にエラーを防ぐだけでなく、エラーの種類に応じた適切な「回復手段」を提供するために欠かせません。

記述の際は、「具体的な例外(派生クラス)から抽象的な例外(基底クラス)へ」という順序を守ることが最も重要です。

また、標準ライブラリの例外階層を理解し、const参照でのキャッチを徹底することで、安全かつ効率的なエラーハンドリングが可能になります。

キャッチオール(...)や再スローを上手く組み合わせ、予期せぬ事態にも動じない堅牢なアプリケーション開発を目指しましょう。

本記事の内容を参考に、あなたのプロジェクトに最適な例外処理を実装してみてください。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!