コンテンツにスキップ

C++/例外処理

出典: フリー教科書『ウィキブックス(Wikibooks)』
< C++

例外処理

[編集]

例外処理(れいがいしょり、exception handling)は、プログラム実行中に予期せぬ異常や例外的な状況が発生した場合に、通常の実行フローを安全に中断し、適切なエラー処理やリカバリ手段を提供するプログラミング技法です。

C++の例外処理の基本

[編集]

C++における例外処理は、主に以下のキーワードを使用して実装されます:

  • try:例外が発生する可能性のあるコードブロックを定義
  • catch:発生した例外を捕捉し、適切な処理を行う
  • throw:明示的に例外を発生させる
  • noexcept:関数が例外を送出しないことを宣言

基本的な例外処理の例:

#include <iostream>

void process(int value) {
  try {
    if (value < 0) {
      throw std::runtime_error("値が負です"); // 例外をスロー
    }
    std::cout << "処理された値: " << value << std::endl;
  } catch (const std::exception &errorMsg) {
    std::cout << "例外がキャッチされました: " << errorMsg.what() << std::endl;
  }
}

auto main() -> int {
  process(10);  // 正の値を処理
  process(-5);  // 負の値を処理(例外が発生)
  return 0;
}

std::exceptionとカスタム例外

[編集]

std::exceptionは、C++標準ライブラリの基本的な例外クラスで、他の例外クラスの基底クラスとして機能します。

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

#include <exception>
#include <iostream>

class CustomException : public std::exception {
public:
  const char* what() const noexcept override {
    return "カスタム例外が発生しました";
  }
};

auto main() -> int {
  try {
    throw CustomException();
  } catch (const std::exception &e) {
    std::cout << "例外をキャッチ: " << e.what() << std::endl;
  }
  return 0;
}

ゼロ除算の例外処理

[編集]

C++では、整数のゼロ除算は未定義動作となりますが、安全な除算関数を実装することができます:

#include <iostream>
#include <stdexcept>

double divideSafely(double numerator, double denominator) {
  if (denominator == 0) {
    throw std::runtime_error("ゼロ除算エラー");
  }
  return numerator / denominator;
}

auto main() -> int {
  try {
    double result = divideSafely(10.0, 0.0);
  } catch (const std::exception &e) {
    std::cout << "例外をキャッチ: " << e.what() << std::endl;
  }
  return 0;
}

多態的例外処理

[編集]

多態的例外処理では、継承を利用して柔軟な例外処理を実現できます:

#include <exception>
#include <iostream>

class ArithmeticError : public std::exception {
public:
  virtual const char* what() const noexcept override {
    return "算術エラーが発生しました";
  }
};

class DivideByZeroError : public ArithmeticError {
public:
  const char* what() const noexcept override {
    return "ゼロ除算エラーが発生しました";
  }
};

void performDivision(int a, int b) {
  if (b == 0) {
    throw DivideByZeroError();
  }
  std::cout << "計算結果: " << a / b << std::endl;
}

auto main() -> int {
  try {
    performDivision(10, 0);
  } catch (const ArithmeticError &e) {
    std::cout << "エラー: " << e.what() << std::endl;
  }
  return 0;
}

noexcept キーワード

[編集]

noexceptは、関数が例外を送出しないことを宣言するキーワードです:

void safeFunction() noexcept {
  // 例外をスローしない保証付きの関数
}

template <typename T>
void templateFunction() noexcept(noexcept(T())) {
  // テンプレートパラメータに基づく例外仕様
}

以下に、Wikitextの形式で洗練された版を提示します:

主な標準ライブラリ例外

[編集]

C++標準ライブラリの主な例外クラス:

  • std::bad_alloc:メモリ割り当て失敗
  • std::out_of_range:範囲外アクセス
  • std::runtime_error:実行時エラー
  • std::logic_error:論理エラー
  • std::invalid_argument:無効な引数

例外仕様の推論

[編集]

C++における例外仕様の推論Exception Specification Inference)は、コンパイラが関数の例外送出の可能性を静的に分析し、推測するプロセスです。この機能は、プログラムの安全性と例外処理の予測可能性を高めるために重要な役割を果たします。

noexcept仕様の推論

[編集]

noexceptキーワードを使用することで、関数が例外を投げないことを明示的に宣言できます。コンパイラは関数の実装を分析し、暗黙的にnoexceptの適用可能性を判断します。

// 明示的にnoexceptを宣言
void foo() noexcept {
  // 例外を投げない処理
}

// 例外を投げる可能性のある関数
void bar() {
  // 処理
  throw SomeException(); // コンパイラはここで例外の可能性を検知
}

// コンパイラによる例外仕様の推論
class MyClass {
public:
  // デフォルトコンストラクタがnoexceptであると推論される可能性がある
  MyClass() = default;
};

関数テンプレートの例外仕様の推論

[編集]

関数テンプレートでは、テンプレート引数の特性に基づいて例外送出の可能性を推論します。noexcept演算子を使用することで、テンプレートパラメータの例外特性を評価できます。

template <typename T>
void func() noexcept(noexcept(T())) {
  // T()の例外仕様に基づいて、この関数が例外をスローするかどうかを推論
}

// 具体的な例
template <typename T>
void safeOperation() noexcept(noexcept(std::declval<T>().method())) {
  // Tのmethod()が例外を投げない場合、この関数もnoexcept
}

推論のメカニズム

[編集]

例外仕様の推論には、以下のような重要なメカニズムがあります:

  • 静的分析:コンパイル時にコードのパスを分析
  • テンプレートメタプログラミング:テンプレートの特性を利用した例外特性の推定
  • 型特性の評価:オブジェクトの構築や操作における例外送出の可能性

注意点

[編集]
  • 例外仕様の推論は完全に静的であり、実行時の動作を保証するものではありません
  • noexcept指定された関数で例外が発生した場合、std::terminate()が呼び出されます
  • 過度にnoexceptを使用すると、例外処理の柔軟性が損なわれる可能性があります

式の例外仕様の推論

[編集]

コンパイラは、式内で例外をスローする可能性を推論することもあります。例えば、throwキーワードや特定の関数呼び出しの例外仕様を推測することがあります。

void foo() {
  int x{0};
  int y{10};

  int z = y / x; // ゼロ除算は例外をスローする可能性があるので、この式はnoexceptではないことを推測する
}

例外仕様の推論は、コンパイラがコードを解析して、関数内で例外が発生する可能性を静的に判断するプロセスです。これにより、例外安全性の向上や、例外がどのように処理されるかをプログラマが予測できるようになります。

C++の例外処理仕様の変遷
C++の例外処理仕様は時間と共に変遷を経てきました。重要な仕様の追加・変更を以下にまとめます。
C++03までの例外仕様(Dynamic Exception Specification
  • 動的例外仕様(Dynamic Exception Specification): 関数が投げる可能性のある例外を関数のシグネチャで宣言する構文が存在しました。
    void foo() throw(SomeExceptionType);
    
  • 不完全な機能: この仕様は便利なように見えましたが、実際には多くの問題を引き起こしました。関数が投げる可能性のある例外を宣言するだけで、実際にスローする例外を管理することが難しかったり、コンパイラ最適化の障害になったりしました。
C++11以降の変更
  • 動的例外仕様の非推奨化: C++11では動的例外仕様が非推奨とされ、C++17では削除されました。
  • noexcept仕様の導入: noexceptキーワードが導入され、関数が例外を投げないことを示すために使用されます。
    void foo() noexcept;
    
  • 例外指定の推論: noexceptキーワードは関数の例外仕様を明示するだけでなく、関数が例外を投げるかどうかを推論するのにも利用されます。
  • 強力なRAIIと例外安全性: C++11以降、RAII(Resource Acquisition Is Initialization)と例外安全性が重視され、リソース管理と例外処理の統合が強化されました。これにより、例外がスローされてもリソースのリークを避けることができます。
現在のC++の状況
  • 例外仕様の削除: C++17で動的例外仕様が削除されました。現在ではnoexcept仕様を使用して、例外安全性を宣言することが推奨されています。
  • 例外安全性の確保: RAII、スマートポインタ、例外仕様の推論などが、例外安全性を確保するための主要な手法となっています。

Javaのfinallyブロックの模倣

[編集]

C++にはJavaのfinallyブロックと同様の動作を実現する構文はありませんが、RAII(Resource Acquisition Is Initialization)というC++の概念があります。RAIIは、リソースの確保と解放をオブジェクトのライフサイクルに結びつける方法です。

一般的に、リソース(メモリ、ファイルハンドル、ロックなど)を取得した場合、それを解放するためにはそのスコープを抜ける時に解放処理を行う必要があります。これをRAIIを使って行うと、オブジェクトのデストラクタがリソースの解放を担当するため、スコープを抜けるときに自動的にリソースが解放されます。

例えば、ファイルハンドルを扱う場合、std::fstreamはRAIIの考え方を使用しています。ファイルを開くときにオブジェクトを作成し、そのオブジェクトがスコープを抜けるときに自動的にファイルが閉じられます。

#include <fstream>
#include <iostream>

void doSomethingWithFile() {
  std::fstream file("example.txt", std::ios::in | std::ios::out);
  if (!file.is_open()) {
    std::cerr << "Failed to open the file." << std::endl;
    return;
  }

  // ファイルを読み書きする処理

  // 関数が終了するときに、file オブジェクトがスコープを抜けるため、
  // 自動的にファイルが閉じられる(リソースの解放)
}

もし、特定の処理をtryブロック内で行い、catchブロックの後に共通のクリーンアップ処理を実行したい場合、スコープ内でRAIIを使ったオブジェクトを活用してそれを行います。

#include <iostream>

class MyResource {
public:
  MyResource() { std::cout << "Resource acquired." << std::endl; }

  ~MyResource() { std::cout << "Resource released." << std::endl; }

  void doSomething() {
    // リソースを使った処理
  }
};

void doSomethingWithResource() {
  MyResource resource;

  try {
    // リソースを使った処理
    resource.doSomething();
  } catch (const std::exception &e) {
    std::cerr << "Exception occurred: " << e.what() << std::endl;
    // 例外が発生した場合の処理
  }

  // 共通のクリーンアップ処理(finally ブロックに相当)
}

このように、C++ではRAIIを使用してリソース管理を行い、オブジェクトのデストラクタを利用してリソースの解放を確実に行います。これにより、finallyブロックのような共通の後始末処理を実現することができます。

std::shared_ptr

[編集]

finallyブロックのような振る舞いを模倣するには、std::shared_ptrを使ってリソース管理を行う方法があります。リソース管理のためにRAIIを利用し、shared_ptrのカスタムデリータ(custom deleter)を使用して特定の後始末処理を行います。

以下は、finallyブロックに相当するものを模倣するための例です。

#include <iostream>
#include <memory>

// デリータとして後始末処理を行う関数
void cleanupFunction() {
  std::cout << "Finally block equivalent cleanup." << std::endl;
  // ここに後始末処理のコードを記述する
}

auto main() -> int {
  // shared_ptrのカスタムデリータに後始末処理を行う関数を設定する
  std::shared_ptr<int> resource(nullptr, [](int *ptr) {
    if (ptr) {
      cleanupFunction();
    }
    delete ptr;
  });

  try {
    // リソースの割り当て
    int *rawPtr = new int(42);
    resource.reset(rawPtr);

    // リソースを使った処理
    std::cout << "Resource value: " << *resource << std::endl;
  } catch (const std::exception &e) {
    std::cerr << "Exception occurred: " << e.what() << std::endl;
    // 例外が発生した場合の処理
  }

  // resourceのスコープを抜ける時に、カスタムデリータが後始末処理を行う
  return 0;
}

このコードは、C++の標準ライブラリである<memory>ヘッダーを使用し、std::shared_ptrを使って後始末処理を実現する方法を示しています。

cleanupFunction()は、後始末処理を行う関数です。ここでは単純なメッセージを出力していますが、実際のアプリケーションではリソースの解放やクリーンアップなどを行うでしょう。

std::shared_ptrのカスタムデリータを使用して、リソース管理と後始末処理をカプセル化しています。このカスタムデリータは、リソースの解放とともにcleanupFunction()を呼び出して後始末処理を行います。nullptrとして初期化されたshared_ptrは、空のデリータ関数を持つことになります。

main()関数内では、リソースをint型のポインタで確保し、std::shared_ptrに割り当てています。これにより、リソースの所有権と管理がshared_ptrに移ります。

try-catchブロック内では、リソースの利用や可能な例外のキャッチが行われます。もし例外が発生した場合、catchブロックでエラーメッセージを出力することができます。

main()関数の最後で、shared_ptrのスコープを抜ける際にカスタムデリータが呼ばれ、後始末処理が実行されます。これにより、リソースが解放され、cleanupFunction()が呼ばれます。

この例では、shared_ptrのカスタムデリータを使ってRAIIの概念を応用し、リソースの自動管理と後始末処理を行っています。これにより、リソースの安全な管理と例外が発生した際の安全な後始末が実現されます。

std::unique_ptr

[編集]

ラムダ式と例外処理を組み合わせることで、柔軟で効果的なエラーハンドリングや後始末処理を行うことができます。

include <functional>
#include <iostream>

void processResource() {
  std::cout << "Processing resource." << std::endl;
  throw std::runtime_error("An error occurred while processing.");
}

auto main() -> int {
  try {
    // ラムダ式を使って後始末処理を定義
    auto cleanup = []() {
      std::cout << "Performing cleanup." << std::endl;
      // ここに後始末処理のコードを記述する
    };

    // リソース処理をラムダ式で囲む
    [&]() {
      // 後始末処理をスコープを抜ける際に実行する
      std::unique_ptr<decltype(cleanup), std::function<void()>> guard(
          &cleanup, [](auto *ptr) {
            (*ptr)(); // 後始末処理を呼び出す
          });

      // リソースの処理
      processResource();
    }();
  } catch (const std::exception &e) {
    std::cerr << "Exception caught: " << e.what() << std::endl;
  }

  return 0;
}

この例では、processResource()という関数でエラーをスローし、それをメインのtry-catchブロックでキャッチしています。さらに、ラムダ式を使用して、リソース処理中に後始末処理を実行する方法を示しています。

ラムダ式は、[&](){ /* ラムダの中身 */ }という形式で記述されます。このラムダ式は即時関数として使われ、リソース処理のスコープ内で実行されます。

ラムダ式内で、std::unique_ptrを使用してカスタムデリータを持つスマートポインタを作成しています。これにより、リソース処理スコープを抜ける際に後始末処理が呼び出されるようになります。

エラーが発生すると、catchブロックが実行されて例外がキャッチされます。この方法を使用すると、例外処理と後始末処理を効果的に組み合わせることができます。

まとめ

C++の例外処理は、プログラム実行中に発生するエラーや異常状態に対処するためのメカニズムです。以下にC++の例外処理に関するまとめを示します。

例外の基本構造

[編集]
  • 例外のスロー: throwキーワードを使用して、プログラムのある箇所で例外をスローします。
    throw SomeException("Something went wrong!");
    
  • 例外のキャッチ: try-catchブロックを使用して、例外をキャッチし処理を行います。
    try {
      // 例外が発生する可能性のあるコード
    } catch (const SomeException& e) {
      // 例外が発生した時の処理
    }
    

例外の型

[編集]
  • 標準ライブラリには、std::exceptionを基底とする様々な例外クラスが用意されています。カスタム例外クラスを定義することもできます。

RAIIと例外処理

[編集]
  • RAII(Resource Acquisition Is Initialization): リソースの解放をオブジェクトのライフサイクルに結びつけ、リソースリークを避けるための手法。
  • std::unique_ptrstd::shared_ptrなどのスマートポインタを利用して、自動的にリソースを解放することができます。

例外仕様(Exception Specification)

[編集]
  • C++において、古いバージョンの言語仕様である動的例外仕様がありましたが、C++17で非推奨とされ、C++20で削除されました。動的例外仕様では、関数が投げる可能性のある例外を指定しますが、使い勝手が悪く、推奨されなくなりました。

例外処理の注意点

[編集]
  • 例外安全性: リソースリークを避けるために、例外発生時にオブジェクトの破棄やリソースの解放を保証することが重要です。
  • スタックアンウィンド: 例外がスローされると、スタックを巻き戻して適切なハンドラに渡すプロセスが発生します。このプロセスをスタックアンウィンドと呼びます。

例外処理のベストプラクティス

[編集]
  • 関数が例外を送出する可能性がある場合には、その旨をドキュメントに記述することが重要です。
  • 複雑なリソース管理やクリーンアップを伴う場合、RAIIやスマートポインタを活用して例外安全性を確保することが望ましいです。
  • 例外を適切にキャッチして処理することで、プログラムのロバスト性を高めることができますが、過度な例外処理はコードの読みやすさを損なう可能性があります。

注意点と推奨事項

[編集]
  • 例外処理は慎重に設計し、過度な使用は避けること
  • パフォーマンスへの影響を考慮すること
  • 可能な限り具体的な例外をキャッチすること
  • リソース管理には RAII (Resource Acquisition Is Initialization) を活用すること

以上が、C++における例外処理に関する基本的なまとめです。例外処理はプログラムの信頼性と保守性を高めるために重要な概念ですが、使い方には注意が必要です。


脚註

[編集]


関連項目

[編集]