C++の例外を使ったリソース管理とクリーンアップを完全解説

C++プログラミングにおいて、例外を使ったリソース管理とクリーンアップは非常に重要です。リソースリークを防ぎ、安定したアプリケーションを開発するための基本的な考え方から、具体的な実装方法までを詳しく解説します。本記事では、例外処理の基本概念から始まり、RAII(リソース取得は初期化)の原則、スマートポインタの利用方法、例外安全なコードの書き方など、実際のコード例を交えながらわかりやすく説明します。これにより、例外を効果的に活用して、堅牢なC++プログラムを作成する方法を理解できるでしょう。

目次

例外処理の基本概念

例外処理は、プログラムの実行中に発生する予期しないエラーや異常状態を扱うための重要なメカニズムです。C++では、例外を使ってエラーをキャッチし、適切に処理することで、プログラムのクラッシュを防ぎ、信頼性を高めることができます。

例外とは何か

例外とは、通常のプログラムの流れを中断し、特別な処理を行うための手段です。これにより、エラーが発生した際に、そのエラーを適切に処理し、プログラムの継続を試みることができます。

例外の基本的な仕組み

C++では、例外処理のためにtry, throw, catchという3つのキーワードを使用します。

tryブロック

エラーが発生する可能性があるコードをtryブロックで囲みます。

try {
    // エラーが発生する可能性のあるコード
}

throwキーワード

エラーが発生した場合、throwキーワードを使って例外を発生させます。

if (エラー条件) {
    throw エラー情報;
}

catchブロック

発生した例外をキャッチし、処理するためにcatchブロックを使用します。

catch (例外型 変数名) {
    // 例外を処理するコード
}

例外の例

次に、例外処理の具体例を示します。

#include <iostream>
#include <stdexcept>

void mayThrow() {
    throw std::runtime_error("エラーが発生しました");
}

int main() {
    try {
        mayThrow();
    } catch (const std::runtime_error& e) {
        std::cout << "例外をキャッチしました: " << e.what() << std::endl;
    }
    return 0;
}

このコードでは、mayThrow関数内で例外が発生し、それがmain関数内でキャッチされ、エラーメッセージが表示されます。

RAII(リソース取得は初期化)の概念

RAII(Resource Acquisition Is Initialization)は、C++におけるリソース管理の基本的な原則です。この原則に従うことで、リソースの漏れを防ぎ、例外安全なコードを実現することができます。

RAIIの基本原則

RAIIの基本原則は、「リソースの取得はその初期化時に行う」というものです。リソースの取得と解放をオブジェクトのライフサイクルに組み込むことで、自動的にリソース管理が行われます。

コンストラクタでのリソース取得

オブジェクトのコンストラクタでリソースを取得し、初期化します。これにより、オブジェクトが生成された時点でリソースが確保されます。

class Resource {
public:
    Resource() {
        // リソースの取得
        std::cout << "リソースを取得しました" << std::endl;
    }

    ~Resource() {
        // リソースの解放
        std::cout << "リソースを解放しました" << std::endl;
    }
};

デストラクタでのリソース解放

オブジェクトのデストラクタでリソースを解放します。これにより、オブジェクトが破棄されるときに自動的にリソースが解放されます。

{
    Resource res;
    // リソースが使用される
}
// ここでresのデストラクタが呼ばれ、リソースが解放される

RAIIの利点

RAIIを利用することで、以下の利点があります:

リソースリークの防止

RAIIを使用することで、リソースの取得と解放が確実に行われるため、リソースリークを防止できます。

例外安全性の向上

例外が発生した場合でも、デストラクタが確実に呼ばれるため、リソースの解放が漏れなく行われ、例外安全性が向上します。

RAIIの具体例

次に、RAIIを利用した具体的な例を示します。

#include <iostream>

class FileHandle {
public:
    FileHandle(const std::string& filename) {
        file = fopen(filename.c_str(), "r");
        if (!file) {
            throw std::runtime_error("ファイルを開けませんでした");
        }
        std::cout << "ファイルを開きました" << std::endl;
    }

    ~FileHandle() {
        if (file) {
            fclose(file);
            std::cout << "ファイルを閉じました" << std::endl;
        }
    }

private:
    FILE* file;
};

int main() {
    try {
        FileHandle fh("example.txt");
        // ファイルを使用する処理
    } catch (const std::exception& e) {
        std::cout << "例外をキャッチしました: " << e.what() << std::endl;
    }
    return 0;
}

このコードでは、FileHandleクラスがファイルを開き、オブジェクトが破棄されるときに自動的にファイルを閉じます。例外が発生した場合でも、確実にファイルが閉じられます。

スマートポインタの利用方法

C++11以降、スマートポインタはリソース管理を簡単にし、メモリリークを防ぐための強力なツールとして提供されています。代表的なスマートポインタにはunique_ptrshared_ptrがあります。

unique_ptrの利用方法

unique_ptrは所有権の唯一性を保証するスマートポインタです。所有権は一つのポインタのみが持つため、所有権の移動(ムーブセマンティクス)以外でコピーすることはできません。

unique_ptrの基本的な使い方

unique_ptrを使うことで、動的メモリの管理が自動的に行われます。

#include <iostream>
#include <memory>

void useUniquePtr() {
    std::unique_ptr<int> ptr = std::make_unique<int>(10);
    std::cout << "unique_ptrの値: " << *ptr << std::endl;
} // ここでptrのデストラクタが呼ばれ、メモリが解放される

所有権の移動

所有権はstd::moveを使って移動できます。これにより、リソースを新しいunique_ptrに移すことができます。

void transferOwnership() {
    std::unique_ptr<int> ptr1 = std::make_unique<int>(20);
    std::unique_ptr<int> ptr2 = std::move(ptr1); // ptr1からptr2に所有権を移動
    if (!ptr1) {
        std::cout << "ptr1は所有権を失いました" << std::endl;
    }
}

shared_ptrの利用方法

shared_ptrは所有権の共有を可能にするスマートポインタです。複数のshared_ptrが同じリソースを指すことができ、最後の一つが破棄されたときにリソースが解放されます。

shared_ptrの基本的な使い方

shared_ptrを使うことで、複数箇所で安全にリソースを共有できます。

#include <iostream>
#include <memory>

void useSharedPtr() {
    std::shared_ptr<int> ptr1 = std::make_shared<int>(30);
    std::shared_ptr<int> ptr2 = ptr1; // ptr1とptr2は同じリソースを指す
    std::cout << "shared_ptrの値: " << *ptr1 << std::endl;
    std::cout << "参照カウント: " << ptr1.use_count() << std::endl; // 参照カウントは2
}

weak_ptrの活用

weak_ptrshared_ptrによって管理されるリソースへの弱い参照を提供します。循環参照を防ぐために使用されます。

#include <iostream>
#include <memory>

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev; // 循環参照を防ぐためにweak_ptrを使用
};

void createLinkedList() {
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();
    node1->next = node2;
    node2->prev = node1; // weak_ptrによる参照
}

これらのスマートポインタを使うことで、C++でのリソース管理は格段に容易になり、メモリリークやバグを防ぐことができます。

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

例外安全性とは、例外が発生した場合でもプログラムが予測可能な状態に留まり、リソースリークを防ぐことを指します。例外安全なコードを書くためのテクニックを学びましょう。

例外安全なコードの基本原則

例外安全なコードを書くための基本原則は、リソース管理を自動化し、例外が発生した場合でも確実にリソースを解放することです。以下のテクニックを活用します。

RAIIの徹底

前述のRAIIの原則を徹底することで、リソース管理を確実に行い、例外発生時のリソースリークを防ぎます。

スマートポインタの使用

unique_ptrshared_ptrを使用することで、自動的にメモリ管理を行い、例外発生時でもメモリリークを防ぎます。

基本的な例外安全のレベル

例外安全には、以下の3つの基本的なレベルがあります:

基本保証

例外が発生しても、プログラムは一貫した状態を保ち、リソースリークは起こりません。ただし、操作の成功は保証されません。

void basicGuaranteeExample() {
    std::unique_ptr<int> ptr = std::make_unique<int>(10);
    try {
        // 例外が発生する可能性のある操作
        throw std::runtime_error("エラー発生");
    } catch (...) {
        // 例外処理
    }
    // ptrのデストラクタが呼ばれ、メモリが解放される
}

強い保証

例外が発生した場合でも、プログラムの状態は例外発生前と同じになります。操作は成功するか、完全に行われないかのどちらかです。

#include <vector>

void strongGuaranteeExample() {
    std::vector<int> vec = {1, 2, 3};
    std::vector<int> temp = vec;
    try {
        // 例外が発生する可能性のある操作
        temp.push_back(4);
    } catch (...) {
        // 例外処理
    }
    vec = std::move(temp); // 操作が成功した場合のみ変更を適用
}

例外なし保証

操作が例外を投げないことを保証します。これは、最も強力な保証ですが、実現が難しい場合があります。

void noThrowGuaranteeExample() noexcept {
    int* ptr = new int(10);
    delete ptr; // 例外は発生しない
}

例外安全な関数設計

例外安全な関数を設計する際には、次のポイントに注意します:

ステートメントの順序

例外が発生する可能性のある操作は、先に行います。リソースの取得や状態変更は、後で行います。

void orderOfOperationsExample() {
    std::unique_ptr<int> ptr;
    try {
        ptr = std::make_unique<int>(10); // 例外が発生する可能性がある操作
    } catch (...) {
        // 例外処理
    }
    // 以降の操作は安全
}

ロールバック機構の実装

例外が発生した場合に、状態を元に戻すためのロールバック機構を実装します。

#include <vector>

void rollbackExample() {
    std::vector<int> vec = {1, 2, 3};
    std::vector<int> backup = vec;
    try {
        // 例外が発生する可能性のある操作
        vec.push_back(4);
        throw std::runtime_error("エラー発生");
    } catch (...) {
        vec = backup; // ロールバック
    }
}

これらのテクニックを用いることで、例外安全なコードを実現し、安定したプログラムを作成することができます。

try-catchブロックの使い方

例外を捕捉し、適切に処理するための基本的な構文は、try-catchブロックです。この節では、try-catchブロックの使い方と、その注意点について説明します。

try-catchブロックの基本的な構文

tryブロック内に例外が発生する可能性のあるコードを配置し、catchブロック内で例外を捕捉して処理します。

try {
    // 例外が発生する可能性のあるコード
} catch (例外型 変数名) {
    // 例外を処理するコード
}

基本的な例

次に、基本的なtry-catchブロックの例を示します。

#include <iostream>
#include <stdexcept>

void mightThrow() {
    throw std::runtime_error("エラーが発生しました");
}

int main() {
    try {
        mightThrow();
    } catch (const std::runtime_error& e) {
        std::cout << "例外をキャッチしました: " << e.what() << std::endl;
    }
    return 0;
}

この例では、mightThrow関数内で例外が発生し、それがmain関数内でキャッチされ、エラーメッセージが表示されます。

複数のcatchブロック

異なる種類の例外を捕捉するために、複数のcatchブロックを使用することができます。

try {
    // 例外が発生する可能性のあるコード
} catch (const std::runtime_error& e) {
    // runtime_errorを処理
} catch (const std::exception& e) {
    // その他のstd::exceptionを処理
} catch (...) {
    // その他の例外を処理
}

例外の再スロー

キャッチした例外を再度スローすることで、上位のハンドラで処理することができます。

void function1() {
    try {
        throw std::runtime_error("エラー発生");
    } catch (const std::runtime_error& e) {
        std::cout << "function1で例外をキャッチ: " << e.what() << std::endl;
        throw; // 例外を再スロー
    }
}

void function2() {
    try {
        function1();
    } catch (const std::runtime_error& e) {
        std::cout << "function2で例外をキャッチ: " << e.what() << std::endl;
    }
}

int main() {
    function2();
    return 0;
}

この例では、function1でキャッチした例外を再スローし、function2で再度キャッチしています。

catchブロック内での注意点

catchブロック内で次の点に注意します:

リソースの解放

例外が発生した場合でも、リソースが確実に解放されるようにします。RAIIやスマートポインタを活用することで、これを自動化できます。

例外の多重捕捉を避ける

一つの例外が複数のcatchブロックで捕捉されることを避けます。例外の再スローや異なる種類の例外を捕捉する場合は、適切に処理する必要があります。

catchの順序

catchブロックは、より具体的な例外型から順に並べる必要があります。一般的な例外型を先に書いてしまうと、より具体的な例外が捕捉されません。

try {
    // 例外が発生する可能性のあるコード
} catch (const std::overflow_error& e) {
    // 具体的な例外型を先に捕捉
} catch (const std::exception& e) {
    // より一般的な例外型を後に捕捉
}

これらの基本的な使い方と注意点を押さえることで、try-catchブロックを効果的に利用し、例外処理を正確に行うことができます。

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

標準ライブラリの例外クラスだけでなく、独自のカスタム例外クラスを作成することで、より細かいエラー管理が可能になります。ここでは、カスタム例外クラスの作成方法とその活用法について解説します。

カスタム例外クラスの基本

カスタム例外クラスは、標準ライブラリのstd::exceptionクラスを継承して作成します。これにより、標準的な例外処理機構を利用しながら、独自のエラーメッセージやエラーハンドリングを実装できます。

基本的なカスタム例外クラス

まず、基本的なカスタム例外クラスを作成します。

#include <iostream>
#include <exception>
#include <string>

class MyException : public std::exception {
public:
    MyException(const std::string& message) : msg_(message) {}

    virtual const char* what() const noexcept override {
        return msg_.c_str();
    }

private:
    std::string msg_;
};

このクラスは、std::exceptionを継承し、エラーメッセージを受け取って格納するコンストラクタを持っています。whatメソッドをオーバーライドして、エラーメッセージを返すようにしています。

カスタム例外クラスの利用

次に、カスタム例外クラスを利用する方法を示します。

例外のスローとキャッチ

作成したカスタム例外クラスをスローし、キャッチします。

void functionThatThrows() {
    throw MyException("カスタム例外が発生しました");
}

int main() {
    try {
        functionThatThrows();
    } catch (const MyException& e) {
        std::cout << "例外をキャッチしました: " << e.what() << std::endl;
    }
    return 0;
}

この例では、functionThatThrows関数でMyExceptionをスローし、main関数でそれをキャッチしてメッセージを表示します。

派生カスタム例外クラス

さらに、カスタム例外クラスを派生させることで、異なる種類のエラーを管理できます。

class FileException : public MyException {
public:
    FileException(const std::string& message) : MyException("File error: " + message) {}
};

class NetworkException : public MyException {
public:
    NetworkException(const std::string& message) : MyException("Network error: " + message) {}
};

これらの派生クラスは、特定の種類のエラー(ファイルエラーやネットワークエラー)を示すために使用します。

派生カスタム例外クラスの利用

void functionThatThrowsFileError() {
    throw FileException("ファイルが見つかりません");
}

void functionThatThrowsNetworkError() {
    throw NetworkException("接続に失敗しました");
}

int main() {
    try {
        functionThatThrowsFileError();
    } catch (const FileException& e) {
        std::cout << "ファイル例外をキャッチしました: " << e.what() << std::endl;
    } catch (const NetworkException& e) {
        std::cout << "ネットワーク例外をキャッチしました: " << e.what() << std::endl;
    } catch (const MyException& e) {
        std::cout << "一般的な例外をキャッチしました: " << e.what() << std::endl;
    }
    return 0;
}

このコードでは、特定のエラーに対して適切なキャッチブロックを用意し、異なる種類のエラーに対する処理を明確にしています。

これにより、プログラムの異常状態をより詳細に管理し、適切なエラーハンドリングを実現できます。

ファイル操作の例外処理

ファイル操作は、失敗する可能性が高いため、例外処理が非常に重要です。このセクションでは、ファイル操作における例外処理の方法と、よくあるエラーの対処法について解説します。

ファイルを開く際の例外処理

ファイルを開く際には、ファイルが存在しない、読み取り権限がないなどの理由で失敗することがあります。これらのエラーをキャッチして適切に処理します。

#include <iostream>
#include <fstream>
#include <stdexcept>

void readFile(const std::string& filename) {
    std::ifstream file(filename);
    if (!file) {
        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::runtime_error& e) {
        std::cout << "例外をキャッチしました: " << e.what() << std::endl;
    }
    return 0;
}

このコードでは、readFile関数でファイルを開き、失敗した場合には例外をスローします。main関数でこの例外をキャッチし、エラーメッセージを表示します。

ファイル書き込みの例外処理

ファイルへの書き込みも、ディスクスペースの不足や書き込み権限の問題で失敗することがあります。

#include <iostream>
#include <fstream>
#include <stdexcept>

void writeFile(const std::string& filename, const std::string& content) {
    std::ofstream file(filename);
    if (!file) {
        throw std::runtime_error("ファイルを開くことができません: " + filename);
    }

    file << content;
    if (!file) {
        throw std::runtime_error("ファイルに書き込むことができません: " + filename);
    }
}

int main() {
    try {
        writeFile("example.txt", "Hello, World!");
    } catch (const std::runtime_error& e) {
        std::cout << "例外をキャッチしました: " << e.what() << std::endl;
    }
    return 0;
}

この例では、writeFile関数でファイルを開き、書き込みに失敗した場合には例外をスローします。

ファイル操作中の例外安全性

ファイル操作中に例外が発生しても、リソースが確実に解放されるようにするためにRAIIを活用します。以下は、ファイルを安全に操作するためのクラスの例です。

#include <iostream>
#include <fstream>
#include <stdexcept>

class SafeFile {
public:
    SafeFile(const std::string& filename, std::ios_base::openmode mode)
        : file_(filename, mode) {
        if (!file_) {
            throw std::runtime_error("ファイルを開くことができません: " + filename);
        }
    }

    ~SafeFile() {
        if (file_.is_open()) {
            file_.close();
        }
    }

    std::ofstream& get() {
        return file_;
    }

private:
    std::ofstream file_;
};

void safeWrite(const std::string& filename, const std::string& content) {
    SafeFile file(filename, std::ios::out);
    file.get() << content;
    if (!file.get()) {
        throw std::runtime_error("ファイルに書き込むことができません: " + filename);
    }
}

int main() {
    try {
        safeWrite("example.txt", "Hello, Safe World!");
    } catch (const std::runtime_error& e) {
        std::cout << "例外をキャッチしました: " << e.what() << std::endl;
    }
    return 0;
}

このコードでは、SafeFileクラスがファイル操作を安全に行うためのRAIIを提供し、例外が発生した場合でも確実にファイルが閉じられます。

ネットワークリソースの管理

ネットワークリソースの管理も、ファイル操作と同様に例外処理が重要です。ネットワーク接続の確立やデータの送受信は、さまざまな理由で失敗する可能性があります。このセクションでは、ネットワークリソースの例外処理について解説します。

ネットワーク接続の例外処理

ネットワーク接続の確立は、タイムアウトや接続拒否などの理由で失敗することがあります。これらのエラーを適切に処理する必要があります。

#include <iostream>
#include <stdexcept>
#include <boost/asio.hpp>

void connectToServer(const std::string& host, const std::string& service) {
    boost::asio::io_context io_context;
    boost::asio::ip::tcp::resolver resolver(io_context);
    boost::asio::ip::tcp::resolver::results_type endpoints = resolver.resolve(host, service);

    boost::asio::ip::tcp::socket socket(io_context);
    boost::system::error_code error;

    boost::asio::connect(socket, endpoints, error);

    if (error) {
        throw std::runtime_error("接続に失敗しました: " + error.message());
    }

    std::cout << "サーバーに接続しました" << std::endl;
}

int main() {
    try {
        connectToServer("example.com", "http");
    } catch (const std::runtime_error& e) {
        std::cout << "例外をキャッチしました: " << e.what() << std::endl;
    }
    return 0;
}

このコードでは、Boost.Asioライブラリを使用してサーバーへの接続を試み、接続失敗時に例外をスローします。

データ送受信の例外処理

データの送受信中に、接続が切断される、データが破損するなどのエラーが発生することがあります。これらのエラーも例外として処理します。

#include <iostream>
#include <stdexcept>
#include <boost/asio.hpp>

void sendData(boost::asio::ip::tcp::socket& socket, const std::string& message) {
    boost::system::error_code error;
    boost::asio::write(socket, boost::asio::buffer(message), error);

    if (error) {
        throw std::runtime_error("データ送信に失敗しました: " + error.message());
    }
}

void receiveData(boost::asio::ip::tcp::socket& socket) {
    char buf[512];
    boost::system::error_code error;
    size_t len = socket.read_some(boost::asio::buffer(buf), error);

    if (error == boost::asio::error::eof) {
        std::cout << "接続が閉じられました" << std::endl;
    } else if (error) {
        throw std::runtime_error("データ受信に失敗しました: " + error.message());
    } else {
        std::cout << "受信データ: " << std::string(buf, len) << std::endl;
    }
}

int main() {
    try {
        boost::asio::io_context io_context;
        boost::asio::ip::tcp::resolver resolver(io_context);
        boost::asio::ip::tcp::resolver::results_type endpoints = resolver.resolve("example.com", "http");

        boost::asio::ip::tcp::socket socket(io_context);
        boost::asio::connect(socket, endpoints);

        sendData(socket, "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n");
        receiveData(socket);
    } catch (const std::runtime_error& e) {
        std::cout << "例外をキャッチしました: " << e.what() << std::endl;
    }
    return 0;
}

この例では、サーバーへのデータ送信と受信を試み、エラーが発生した場合に例外をスローします。

RAIIを活用したネットワークリソース管理

ネットワークリソースの管理にはRAIIを活用することで、リソースリークを防ぎ、例外安全なコードを実現できます。

#include <iostream>
#include <stdexcept>
#include <boost/asio.hpp>

class NetworkConnection {
public:
    NetworkConnection(const std::string& host, const std::string& service)
        : socket_(io_context_) {
        boost::asio::ip::tcp::resolver resolver(io_context_);
        boost::asio::ip::tcp::resolver::results_type endpoints = resolver.resolve(host, service);

        boost::asio::connect(socket_, endpoints);
    }

    ~NetworkConnection() {
        socket_.close();
    }

    void send(const std::string& message) {
        boost::system::error_code error;
        boost::asio::write(socket_, boost::asio::buffer(message), error);

        if (error) {
            throw std::runtime_error("データ送信に失敗しました: " + error.message());
        }
    }

    std::string receive() {
        char buf[512];
        boost::system::error_code error;
        size_t len = socket_.read_some(boost::asio::buffer(buf), error);

        if (error == boost::asio::error::eof) {
            throw std::runtime_error("接続が閉じられました");
        } else if (error) {
            throw std::runtime_error("データ受信に失敗しました: " + error.message());
        }

        return std::string(buf, len);
    }

private:
    boost::asio::io_context io_context_;
    boost::asio::ip::tcp::socket socket_;
};

int main() {
    try {
        NetworkConnection conn("example.com", "http");
        conn.send("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n");
        std::cout << "受信データ: " << conn.receive() << std::endl;
    } catch (const std::runtime_error& e) {
        std::cout << "例外をキャッチしました: " << e.what() << std::endl;
    }
    return 0;
}

このコードでは、NetworkConnectionクラスを使用してネットワークリソースを管理し、例外発生時にも確実にリソースが解放されるようにしています。

演習問題と解答例

これまで学んだ内容を確認するために、いくつかの演習問題を用意しました。実際にコードを書いてみて、例外処理とリソース管理の理解を深めましょう。

演習問題1: ファイル読み込みの例外処理

以下の指示に従って、ファイル読み込み時の例外処理を行う関数を作成してください。

問題

  1. readFile関数を作成し、引数としてファイル名を受け取ります。
  2. ファイルを開き、失敗した場合には例外をスローします。
  3. ファイルの内容を1行ずつ読み込み、標準出力に表示します。
  4. 例外が発生した場合には、エラーメッセージを表示します。

解答例

#include <iostream>
#include <fstream>
#include <stdexcept>

void readFile(const std::string& filename) {
    std::ifstream file(filename);
    if (!file) {
        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::runtime_error& e) {
        std::cout << "例外をキャッチしました: " << e.what() << std::endl;
    }
    return 0;
}

演習問題2: カスタム例外クラスの作成

カスタム例外クラスを作成し、特定のエラーを処理する関数を実装します。

問題

  1. FileNotFoundExceptionというカスタム例外クラスを作成します。このクラスはstd::exceptionを継承し、エラーメッセージを受け取ります。
  2. ファイルを開く関数openFileを作成し、ファイルが見つからない場合にFileNotFoundExceptionをスローします。
  3. main関数でこの例外をキャッチし、エラーメッセージを表示します。

解答例

#include <iostream>
#include <fstream>
#include <stdexcept>

class FileNotFoundException : public std::exception {
public:
    FileNotFoundException(const std::string& filename)
        : msg_("ファイルが見つかりません: " + filename) {}

    virtual const char* what() const noexcept override {
        return msg_.c_str();
    }

private:
    std::string msg_;
};

void openFile(const std::string& filename) {
    std::ifstream file(filename);
    if (!file) {
        throw FileNotFoundException(filename);
    }
}

int main() {
    try {
        openFile("nonexistent.txt");
    } catch (const FileNotFoundException& e) {
        std::cout << "例外をキャッチしました: " << e.what() << std::endl;
    }
    return 0;
}

演習問題3: スマートポインタと例外安全性

スマートポインタを使用して、例外安全なリソース管理を実装します。

問題

  1. unique_ptrを使用して動的メモリを管理する関数processDataを作成します。
  2. 動的メモリを割り当て、例外が発生する可能性のある操作を行います。
  3. 例外が発生した場合にメモリリークが発生しないことを確認します。

解答例

#include <iostream>
#include <memory>
#include <stdexcept>

void processData() {
    std::unique_ptr<int> data = std::make_unique<int>(42);

    if (*data == 42) {
        throw std::runtime_error("例外が発生しました");
    }

    std::cout << "データ: " << *data << std::endl;
}

int main() {
    try {
        processData();
    } catch (const std::runtime_error& e) {
        std::cout << "例外をキャッチしました: " << e.what() << std::endl;
    }
    return 0;
}

これらの演習問題を通して、例外処理とリソース管理の基本を実践的に理解できるようになります。

まとめ

この記事では、C++の例外を使ったリソース管理とクリーンアップの重要性と方法について詳しく解説しました。まず、例外処理の基本概念から始め、RAIIの原則、スマートポインタの利用、例外安全なコードの書き方について説明しました。さらに、try-catchブロックの使い方やカスタム例外クラスの作成、ファイル操作およびネットワークリソースの管理における例外処理の実例を示しました。

これらの知識を応用して、C++プログラムの信頼性と安全性を向上させることができます。特に、RAIIやスマートポインタの活用は、リソースリークを防ぎ、例外安全性を確保する上で非常に有効です。今後の開発においても、これらのテクニックを積極的に取り入れていきましょう。

コメント

コメントする

目次