[C++] 例外処理の基本:try, catch, throwの使い方

C++の例外処理は、プログラムの実行中に発生するエラーを管理するための重要な機能です。

例外処理は主にtrycatchthrowの3つのキーワードを使用して行います。

tryブロックは、例外が発生する可能性のあるコードを囲みます。

例外が発生すると、throwキーワードを使って例外を投げます。

その後、catchブロックが例外を受け取り、適切な処理を行います。

これにより、プログラムのクラッシュを防ぎ、エラーを安全に処理することが可能になります。

この記事でわかること
  • try、catch、throwの基本的な使い方
  • 例外の伝播とキャッチの仕組み
  • 標準例外クラスとカスタム例外クラスの作成方法
  • 例外安全性の概念とRAIIの活用
  • ファイル操作やメモリ管理、ネットワークプログラミングにおける例外処理の応用例

目次から探す

try, catch, throwの基本

C++における例外処理は、プログラムの実行中に発生するエラーを適切に処理するための重要な機能です。

ここでは、例外処理の基本であるtrycatchthrowの使い方について詳しく解説します。

tryブロックの役割

tryブロックは、例外が発生する可能性のあるコードを囲むために使用されます。

このブロック内で例外が発生すると、プログラムの制御はcatchブロックに移ります。

#include <iostream>
int main() {
    try {
        // 例外が発生する可能性のあるコード
        throw std::runtime_error("エラーが発生しました");
    } catch (const std::exception& e) {
        // 例外を処理するコード
        std::cout << "例外をキャッチ: " << e.what() << std::endl;
    }
    return 0;
}
例外をキャッチ: エラーが発生しました

この例では、tryブロック内でstd::runtime_errorがスローされ、catchブロックでその例外が処理されます。

catchブロックの役割

catchブロックは、tryブロック内でスローされた例外を受け取り、適切に処理するために使用されます。

catchブロックは、例外の型に基づいて特定の例外を処理することができます。

#include <iostream>
#include <stdexcept>
int main() {
    try {
        // 例外が発生する可能性のあるコード
        throw std::out_of_range("範囲外のエラー");
    } catch (const std::out_of_range& e) {
        // std::out_of_range型の例外を処理
        std::cout << "範囲外の例外をキャッチ: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        // その他のstd::exception型の例外を処理
        std::cout << "その他の例外をキャッチ: " << e.what() << std::endl;
    }
    return 0;
}
範囲外の例外をキャッチ: 範囲外のエラー

この例では、std::out_of_range型の例外がスローされ、対応するcatchブロックで処理されます。

throw式の使い方

throw式は、例外を発生させるために使用されます。

throwの後に例外オブジェクトを指定することで、例外をスローします。

#include <iostream>
#include <stdexcept>
void checkValue(int value) {
    if (value < 0) {
        // 負の値の場合に例外をスロー
        throw std::invalid_argument("負の値は許可されていません");
    }
}
int main() {
    try {
        checkValue(-1);
    } catch (const std::invalid_argument& e) {
        std::cout << "例外をキャッチ: " << e.what() << std::endl;
    }
    return 0;
}
例外をキャッチ: 負の値は許可されていません

この例では、checkValue関数内で負の値が渡された場合にstd::invalid_argument型の例外がスローされます。

例外オブジェクトの種類

C++では、例外オブジェクトとして任意の型を使用できますが、通常は標準ライブラリの例外クラスを使用します。

以下は、一般的な例外オブジェクトの種類です。

スクロールできます
例外クラス名説明
std::exceptionすべての標準例外の基本クラス
std::runtime_error実行時エラーを表すクラス
std::logic_error論理エラーを表すクラス
std::out_of_range範囲外アクセスを表すクラス
std::invalid_argument無効な引数を表すクラス

これらのクラスを使用することで、特定のエラー状況に応じた例外処理を行うことができます。

例外の伝播とキャッチ

C++における例外処理では、例外が発生した際にその例外がどのように伝播し、どのようにキャッチされるかが重要です。

ここでは、例外の伝播の仕組みや複数のcatchブロックの利用、例外の再スローについて解説します。

例外の伝播の仕組み

例外がスローされると、C++のランタイムはその例外をキャッチするためのcatchブロックを探します。

tryブロック内で例外がキャッチされない場合、例外は呼び出し元の関数に伝播します。

このプロセスは、例外がキャッチされるか、プログラムが終了するまで続きます。

#include <iostream>
#include <stdexcept>
void functionB() {
    // 例外をスロー
    throw std::runtime_error("functionBでエラーが発生");
}
void functionA() {
    functionB(); // functionBを呼び出し
}
int main() {
    try {
        functionA(); // functionAを呼び出し
    } catch (const std::runtime_error& e) {
        std::cout << "例外をキャッチ: " << e.what() << std::endl;
    }
    return 0;
}
例外をキャッチ: functionBでエラーが発生

この例では、functionBでスローされた例外がfunctionAを経由してmain関数に伝播し、そこでキャッチされます。

複数のcatchブロックの利用

C++では、異なる型の例外を処理するために複数のcatchブロックを使用できます。

catchブロックは、特定の例外型に対して処理を行います。

#include <iostream>
#include <stdexcept>
int main() {
    try {
        // 例外をスロー
        throw std::out_of_range("範囲外エラー");
    } catch (const std::invalid_argument& e) {
        std::cout << "無効な引数例外をキャッチ: " << e.what() << std::endl;
    } catch (const std::out_of_range& e) {
        std::cout << "範囲外例外をキャッチ: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        std::cout << "その他の例外をキャッチ: " << e.what() << std::endl;
    }
    return 0;
}
範囲外例外をキャッチ: 範囲外エラー

この例では、std::out_of_range型の例外がスローされ、対応するcatchブロックで処理されます。

例外の再スロー

例外をキャッチした後、再度スローすることができます。

これにより、例外を一部処理した後、さらに上位の呼び出し元に例外を伝播させることが可能です。

#include <iostream>
#include <stdexcept>
void functionC() {
    try {
        // 例外をスロー
        throw std::runtime_error("functionCでエラーが発生");
    } catch (const std::runtime_error& e) {
        std::cout << "functionCで例外をキャッチ: " << e.what() << std::endl;
        throw; // 例外を再スロー
    }
}
int main() {
    try {
        functionC(); // functionCを呼び出し
    } catch (const std::runtime_error& e) {
        std::cout << "mainで例外をキャッチ: " << e.what() << std::endl;
    }
    return 0;
}
functionCで例外をキャッチ: functionCでエラーが発生
mainで例外をキャッチ: functionCでエラーが発生

この例では、functionCで例外がキャッチされ、再スローされることでmain関数でも例外がキャッチされます。

再スローは、例外の詳細な処理を行った後に、さらなる処理を上位の関数に委ねる際に有用です。

標準例外クラス

C++の標準ライブラリには、例外処理を行うための多くの例外クラスが用意されています。

これらのクラスを利用することで、プログラム内で発生するさまざまなエラーを効率的に処理することができます。

ここでは、標準例外クラスの概要と、std::exceptionおよびその派生クラス、さらにカスタム例外クラスの作成方法について解説します。

標準例外クラスの概要

標準例外クラスは、C++の標準ライブラリで提供される例外処理のための基本クラス群です。

これらのクラスは、プログラムの実行中に発生するさまざまなエラーを表現するために使用されます。

標準例外クラスは、エラーの種類に応じて適切な例外をスローし、キャッチするための基盤を提供します。

スクロールできます
クラス名説明
std::exceptionすべての標準例外の基本クラス
std::runtime_error実行時エラーを表すクラス
std::logic_error論理エラーを表すクラス
std::out_of_range範囲外アクセスを表すクラス
std::invalid_argument無効な引数を表すクラス

std::exceptionとその派生クラス

std::exceptionは、C++の標準例外クラスの基本クラスです。

このクラスは、すべての標準例外クラスの基底クラスとして機能し、例外の基本的なインターフェースを提供します。

std::exceptionクラスには、例外の説明を取得するためのwhat()メソッドが含まれています。

#include <iostream>
#include <exception>
int main() {
    try {
        // 例外をスロー
        throw std::runtime_error("実行時エラーが発生しました");
    } catch (const std::exception& e) {
        std::cout << "例外をキャッチ: " << e.what() << std::endl;
    }
    return 0;
}
例外をキャッチ: 実行時エラーが発生しました

この例では、std::runtime_errorがスローされ、std::exception型catchブロックでキャッチされます。

カスタム例外クラスの作成

標準例外クラスを利用するだけでなく、独自のカスタム例外クラスを作成することも可能です。

カスタム例外クラスを作成することで、特定のエラー状況に応じた詳細な例外処理を行うことができます。

#include <iostream>
#include <exception>
#include <string>
// カスタム例外クラスの定義
class CustomException : public std::exception {
private:
    std::string message;
public:
    explicit CustomException(const std::string& msg) : message(msg) {}
    const char* what() const noexcept override {
        return message.c_str();
    }
};
int main() {
    try {
        // カスタム例外をスロー
        throw CustomException("カスタム例外が発生しました");
    } catch (const CustomException& e) {
        std::cout << "カスタム例外をキャッチ: " << e.what() << std::endl;
    }
    return 0;
}
カスタム例外をキャッチ: カスタム例外が発生しました

この例では、CustomExceptionというカスタム例外クラスを定義し、main関数内でスローしています。

カスタム例外クラスを使用することで、特定のエラーに対する詳細なメッセージを提供することができます。

例外安全性

例外安全性とは、プログラムが例外をスローした際に、システムの一貫性やリソースの管理が適切に行われることを指します。

例外が発生しても、プログラムが予期しない動作をしないようにするための設計が重要です。

ここでは、例外安全性のレベル、RAII(Resource Acquisition Is Initialization)と例外安全性、例外安全なコードの書き方について解説します。

例外安全性のレベル

例外安全性には、一般的に以下の3つのレベルがあります。

それぞれのレベルは、例外が発生した際に保証されるプログラムの状態を示します。

スクロールできます
レベル名説明
基本保証例外が発生しても、プログラムの状態は一貫性を保ち、リソースリークがない
強い保証例外が発生した場合、プログラムの状態は例外発生前の状態に戻る
例外安全保証例外が発生しないことを保証する(例外がスローされない)

RAIIと例外安全性

RAII(Resource Acquisition Is Initialization)は、リソース管理をオブジェクトのライフサイクルに結びつける設計パターンです。

RAIIを利用することで、例外が発生してもリソースが適切に解放されることを保証できます。

C++では、デストラクタを利用してリソースを解放することで、例外安全性を高めることができます。

#include <iostream>
#include <memory>
class Resource {
public:
    Resource() {
        std::cout << "リソースを取得しました" << std::endl;
    }
    ~Resource() {
        std::cout << "リソースを解放しました" << std::endl;
    }
};
void process() {
    std::unique_ptr<Resource> res(new Resource());
    // 例外が発生する可能性のある処理
    throw std::runtime_error("エラーが発生しました");
}
int main() {
    try {
        process();
    } catch (const std::exception& e) {
        std::cout << "例外をキャッチ: " << e.what() << std::endl;
    }
    return 0;
}
リソースを取得しました
リソースを解放しました
例外をキャッチ: エラーが発生しました

この例では、std::unique_ptrを使用してRAIIを実現しています。

例外が発生しても、Resourceのデストラクタが呼ばれ、リソースが適切に解放されます。

例外安全なコードの書き方

例外安全なコードを書くためには、以下のポイントに注意する必要があります。

  1. RAIIの利用: リソース管理をオブジェクトのライフサイクルに結びつけることで、例外が発生してもリソースリークを防ぎます。
  2. スマートポインタの活用: std::unique_ptrstd::shared_ptrを使用して、メモリ管理を自動化します。
  3. 例外をスローしない関数の設計: 可能な限り例外をスローしない関数を設計し、例外の発生を防ぎます。
  4. 状態のロールバック: 例外が発生した場合に、プログラムの状態を元に戻す処理を実装します。

これらのポイントを意識することで、例外が発生してもプログラムの一貫性を保ち、予期しない動作を防ぐことができます。

応用例

例外処理は、さまざまなプログラミングの場面で応用されます。

ここでは、ファイル操作、メモリ管理、ネットワークプログラミングにおける例外処理の応用例を紹介します。

ファイル操作における例外処理

ファイル操作では、ファイルの存在確認や読み書きの失敗など、さまざまなエラーが発生する可能性があります。

例外処理を用いることで、これらのエラーを適切に処理し、プログラムの安定性を保つことができます。

#include <iostream>
#include <fstream>
#include <stdexcept>
void readFile(const std::string& filename) {
    std::ifstream file(filename);
    if (!file.is_open()) {
        throw std::runtime_error("ファイルを開けませんでした: " + filename);
    }
    std::string line;
    while (std::getline(file, line)) {
        std::cout << line << std::endl;
    }
}
int main() {
    try {
        readFile("example.txt");
    } catch (const std::exception& e) {
        std::cout << "例外をキャッチ: " << e.what() << std::endl;
    }
    return 0;
}
例外をキャッチ: ファイルを開けませんでした: example.txt

この例では、ファイルが開けない場合にstd::runtime_errorをスローし、main関数でキャッチしています。

メモリ管理における例外処理

メモリ管理では、メモリの確保に失敗することがあります。

例外処理を用いることで、メモリ確保の失敗を検出し、適切に対処することができます。

#include <iostream>
#include <new>
int main() {
    try {
        // 大量のメモリを確保しようとする
        int* largeArray = new int[1000000000];
        delete[] largeArray;
    } catch (const std::bad_alloc& e) {
        std::cout << "メモリ確保に失敗しました: " << e.what() << std::endl;
    }
    return 0;
}
メモリ確保に失敗しました: std::bad_alloc

この例では、new演算子がメモリ確保に失敗した場合にstd::bad_alloc例外をスローし、キャッチしています。

ネットワークプログラミングにおける例外処理

ネットワークプログラミングでは、接続の失敗やデータの送受信エラーが発生する可能性があります。

例外処理を用いることで、これらのエラーを検出し、適切に処理することができます。

#include <iostream>
#include <stdexcept>
// ダミーのネットワーク接続関数
void connectToServer(const std::string& server) {
    // 接続に失敗した場合の例外スロー
    throw std::runtime_error("サーバーへの接続に失敗しました: " + server);
}
int main() {
    try {
        connectToServer("example.com");
    } catch (const std::exception& e) {
        std::cout << "例外をキャッチ: " << e.what() << std::endl;
    }
    return 0;
}
例外をキャッチ: サーバーへの接続に失敗しました: example.com

この例では、サーバーへの接続に失敗した場合にstd::runtime_errorをスローし、main関数でキャッチしています。

ネットワークプログラミングでは、接続の再試行やエラーログの記録など、例外処理を活用してエラーに対処することが重要です。

よくある質問

例外処理を使うべきでない場合は?

例外処理は強力なエラーハンドリングの手段ですが、すべての状況で使用するのが適切とは限りません。

以下のような場合には、例外処理を避けることが推奨されます。

  • パフォーマンスが重要なループ内: 例外処理はオーバーヘッドがあるため、頻繁に発生するエラーを例外で処理するとパフォーマンスが低下する可能性があります。
  • 予測可能なエラー: ユーザー入力のバリデーションなど、予測可能で通常のプログラムフローで処理できるエラーは、例外ではなく通常のエラーチェックで処理する方が適切です。
  • リソース制約のある環境: 組み込みシステムなど、リソースが限られている環境では、例外処理のオーバーヘッドが問題になることがあります。

例外処理とエラーチェックの違いは?

例外処理とエラーチェックは、どちらもエラーハンドリングの手法ですが、目的や使用方法に違いがあります。

  • 例外処理:
    • プログラムの通常のフローを中断し、エラーを処理するための特別なメカニズム。
    • 予期しないエラーや致命的なエラーに対して使用されることが多い。
    • trycatchthrowを使用してエラーを処理。
  • エラーチェック:
    • プログラムの通常のフローの一部としてエラーを検出し、処理する。
    • 予測可能なエラーや軽微なエラーに対して使用されることが多い。
    • 関数の戻り値やエラーフラグを使用してエラーを検出。

例外処理がプログラムのパフォーマンスに与える影響は?

例外処理は、プログラムのパフォーマンスに影響を与える可能性があります。

以下の点に注意が必要です。

  • オーバーヘッド: 例外がスローされると、スタックの巻き戻しや例外オブジェクトの生成など、通常のプログラムフローにはない処理が行われるため、オーバーヘッドが発生します。
  • 頻繁な例外のスロー: 頻繁に例外をスローすると、パフォーマンスが大幅に低下する可能性があります。

特に、ループ内で例外を多用することは避けるべきです。

  • コンパイラの最適化: 例外処理を使用すると、コンパイラの最適化が制限される場合があります。

これは、例外処理がプログラムのフローを大きく変える可能性があるためです。

例外処理は、適切に使用することでプログラムの堅牢性を高めることができますが、パフォーマンスへの影響を考慮し、必要な場面でのみ使用することが重要です。

まとめ

この記事では、C++における例外処理の基本であるtrycatchthrowの使い方から、例外の伝播とキャッチ、標準例外クラス、例外安全性、そして応用例に至るまで、例外処理の重要な側面を詳しく解説しました。

例外処理は、プログラムの安定性と信頼性を高めるための重要な技術であり、適切に活用することで、予期しないエラーに対する堅牢な対応が可能になります。

この記事を参考に、実際のプログラムで例外処理を実装し、より安全で効率的なコードを書くことに挑戦してみてください。

当サイトはリンクフリーです。出典元を明記していただければ、ご自由に引用していただいて構いません。

関連カテゴリーから探す

  • URLをコピーしました!
目次から探す