閉じる

【C++】throwの使い方を徹底解説!例外処理の基本とtry-catch

C++でプログラムを開発していると、実行時に予期しないエラー(例外)が発生することがあります。

例えば、ゼロによる除算やメモリ不足、存在しないファイルの読み込みなどが挙げられます。

こうした問題に直面した際、プログラムを異常終了させるのではなく、適切に処理を継続するための仕組みが例外処理です。

本記事では、その中核を担うthrowの使い方を中心に、基本から応用までを詳しく解説します。

例外処理の基本構造とthrowの役割

C++の例外処理は、主にtrycatch、そしてthrowの3つのキーワードで構成されます。

これらはセットで利用されることが一般的です。

throwとは何か

throwは、プログラム内で「異常事態が発生したこと」を通知するためのキーワードです。

エラーが発生した箇所で特定の値を投げ(スローし)、その後の処理を中断して、そのエラーを受け取るためのcatchブロックを探します。

try-catchとの関係

例外処理の構文は、以下のような形をとります。

  1. tryブロック: 例外が発生する可能性のあるコードを囲みます。
  2. throw文: エラーを検知した際に、例外オブジェクトを生成して送出します。
  3. catchブロック: 送出された例外を受け取り、具体的なエラー対処(ログ出力や復旧作業)を行います。

それでは、最もシンプルなthrowの使用例を見てみましょう。

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

int main() {
    int divisor = 0;

    try {
        // ゼロ除算のチェック
        if (divisor == 0) {
            // 文字列のリテラルを例外として投げる
            throw "ゼロによる除算が発生しました。";
        }
        
        int result = 100 / divisor;
        std::cout << "結果: " << result << std::endl;
    }
    catch (const char* error_message) {
        // throwされた文字列を受け取って表示
        std::cerr << "エラー: " << error_message << std::endl;
    }

    std::cout << "プログラムを継続します。" << std::endl;
    return 0;
}
実行結果
エラー: ゼロによる除算が発生しました。
プログラムを継続します。

この例では、divisorが0である場合にthrow文が実行されます。

その瞬間、以降の割り算処理は実行されず、即座にcatchブロックへと処理が移ります。

投げられるデータの種類

C++のthrowは、非常に柔軟です。

JavaやC#などの言語とは異なり、基本データ型からユーザー定義のクラスまで、あらゆる型のデータを投げることができます

基本的な型を投げる

前述の例では文字列リテラル(const char*)を投げましたが、int型やenum型をエラーコードとして投げることも可能です。

C++
void checkValue(int val) {
    if (val < 0) {
        throw 404; // int型のエラーコードを投げる
    }
}

クラス(オブジェクト)を投げる

実務的な開発では、エラー情報をより詳細に保持するために、専用の例外クラスを作成してそのオブジェクトを投げることが一般的です。

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

// 独自の例外クラスを定義
class MyException {
public:
    MyException(std::string msg, int code) : message(msg), errorCode(code) {}
    void printError() const {
        std::cerr << "Error [" << errorCode << "]: " << message << std::endl;
    }
private:
    std::string message;
    int errorCode;
};

int main() {
    try {
        // 自作クラスのインスタンスを生成して投げる
        throw MyException("データベース接続に失敗しました", 500);
    }
    catch (const MyException& e) {
        // 参照で受け取るのが一般的(コピーを避けるため)
        e.printError();
    }
    return 0;
}
実行結果
Error [500]: データベース接続に失敗しました

標準例外ライブラリの活用

C++標準ライブラリには、あらかじめ定義された例外クラスが多数用意されています。

これらを利用することで、コードの可読性が向上し、他の開発者にとっても意図が伝わりやすくなります。

代表的な標準例外クラス

標準例外はすべてstd::exceptionを基底クラスとして持っており、ヘッダーファイル<stdexcept>に定義されています。

クラス名主な用途
std::runtime_error実行時にしか判明しないエラー(計算ミス、ファイル欠落など)
std::logic_errorプログラムの論理的な誤り(引数の不正など)
std::out_of_range配列のインデックス範囲外アクセスなど
std::invalid_argument関数に渡された引数が不適切

標準例外の使用例

標準例外を投げる際は、コンストラクタにエラーメッセージを渡すことができます。

受け取る側ではwhat()メソッドを使ってそのメッセージを取得します。

C++
#include <iostream>
#include <stdexcept> // 標準例外のために必要

double calculateSquareRoot(double value) {
    if (value < 0) {
        // 標準例外のruntime_errorを投げる
        throw std::runtime_error("負の数の平方根は計算できません。");
    }
    return /* 計算処理 */;
}

int main() {
    try {
        calculateSquareRoot(-1.0);
    }
    catch (const std::exception& e) {
        // 基底クラスの参照で受けることで、派生クラスの例外も一括で捕捉可能
        std::cerr << "捕捉した例外: " << e.what() << std::endl;
    }
    return 0;
}
実行結果
捕捉した例外: 負の数の平方根は計算できません。

throwが実行された時の動作:スタック巻き戻し

throwが呼ばれると、単にジャンプするだけではありません。

C++において非常に重要なスタック巻き戻し(Stack Unwinding)というプロセスが発生します。

スタック巻き戻しの重要性

throwが発生した関数内で定義されていたローカル変数は、スコープを抜ける際に正しくデストラクタが呼び出されます

これにより、メモリの解放やファイルのクローズが自動的に行われることが保証されます。

これをRAII(Resource Acquisition Is Initialization)と呼びます。

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

class Resource {
public:
    Resource(std::string name) : n(name) { std::cout << n << "を確保\n"; }
    ~Resource() { std::cout << n << "を解放\n"; }
private:
    std::string n;
};

void subFunction() {
    Resource res("サブリソース");
    throw std::runtime_error("エラー発生!");
    std::cout << "この行は実行されません\n";
}

int main() {
    try {
        Resource mainRes("メインリソース");
        subFunction();
    }
    catch (const std::exception& e) {
        std::cout << "catchブロック: " << e.what() << std::endl;
    }
    return 0;
}
実行結果
メインリソースを確保
サブリソースを確保
サブリソースを解放
メインリソースを解放
catchブロック: エラー発生!

このように、throwが実行されてもリソースの解放が漏れることはありません

ただし、生のポインタを使ってnewしたメモリなどは自動で解放されないため、スマートポインタ(std::unique_ptrなど)を使用することが推奨されます。

高度なthrowの使い方

基本的な使い方に慣れてきたら、より実戦的なテクニックについても押さえておきましょう。

例外の再送出 (Rethrow)

catchブロックの中で例外を一部処理し、さらに上位の関数へその例外を投げ直したい場合があります。

その場合は、引数なしのthrow;を記述します。

C++
void middleFunction() {
    try {
        // 何らかの処理
        throw std::runtime_error("致命的なエラー");
    }
    catch (...) {
        std::cerr << "中間層でエラーをログ記録しました。" << std::endl;
        throw; // 現在の例外をそのまま次に投げる
    }
}

引数にオブジェクトを指定せず、単にthrow;と書くことで、元の例外の型情報を保持したまま上位へ引き渡すことができます。

noexcept指定

関数が絶対に例外を投げないことを保証する場合、関数の宣言にnoexceptを付与します。

C++
void safeFunction() noexcept {
    // この中で例外が発生すると、std::terminate()が呼ばれて即座に終了する
}

コンパイラはこの情報を利用して最適化を行うことができます。

特に、ムーブコンストラクタやデストラクタにはnoexceptを付けることが一般的です。

throwを使用する際の注意点とベストプラクティス

例外処理は強力ですが、乱用するとプログラムの挙動を複雑にし、パフォーマンスを低下させる原因にもなります。

以下のルールを意識しましょう。

1. 例外を投げるべきではないケース

  • 通常の制御フローとして使わない: ループの脱出や条件分岐の代わりに例外を使ってはいけません。
  • 頻繁に発生する事象: 1秒間に数千回発生するような事象には、例外ではなく戻り値(エラーコード)を使用すべきです。throwは処理コストが比較的高いためです。

2. デストラクタ内では投げない

デストラクタの中で例外を投げ、それがさらに別の例外によるスタック巻き戻し中に発生した場合、C++プログラムは即座に強制終了します。

デストラクタ内でのエラーは、その中で完結させるのが鉄則です。

3. キャッチは参照で行う

例外をキャッチするときは、catch (const MyException& e)のように定数参照で受けるようにしましょう。

  • 値渡しにすると、オブジェクトのコピーが発生し、パフォーマンスが低下します。
  • 派生クラスのオブジェクトを基底クラスの型で受け取った際、情報が欠落する「スライシング問題」を防ぐことができます。

まとめ

C++におけるthrowは、単なるエラー通知の手段ではなく、プログラムの堅牢性を高めるための重要な仕組みです。

try-catchと組み合わせることで、エラー発生時のクリーンアップ(スタック巻き戻し)を自動化し、安全に処理を継続させることができます。

標準例外クラスを積極的に活用し、自作の例外クラスを適切に設計することで、メンテナンス性の高いコードを実現しましょう。

一方で、パフォーマンスへの影響やデストラクタでの制限など、C++特有の注意点を理解しておくことも欠かせません。

今回学んだ基本を活かし、エラーに強いアプリケーション開発に取り組んでみてください。

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

URLをコピーしました!