C++のスマートポインタと例外処理の徹底解説: 理論と実践

C++のスマートポインタと例外処理の組み合わせは、メモリ管理とプログラムの堅牢性を向上させるために重要です。C++では、動的メモリ管理が必要な場面が多々ありますが、手動での管理はミスを引き起こしやすく、メモリリークや未定義動作の原因となります。スマートポインタは、こうした問題を回避するために導入された便利なツールです。一方で、例外処理は、予期しないエラーが発生した際にプログラムを適切に処理するための重要な機能です。本記事では、スマートポインタと例外処理の基本から、その組み合わせによる利点、具体的な使用例、さらに例外安全なコードを書くためのベストプラクティスまで、包括的に解説します。これにより、C++プログラムの安全性と効率性を向上させるための知識を深めていきます。

目次
  1. スマートポインタの基本
    1. std::unique_ptr
    2. std::shared_ptr
    3. std::weak_ptr
  2. スマートポインタの利点
    1. 自動メモリ管理
    2. 所有権の明確化
    3. 参照カウントによる共有管理
    4. 循環参照の防止
  3. 例外処理の基本
    1. 例外の投げ方
    2. 例外のキャッチ
    3. 例外の再投げ
    4. 例外の継承とカスタム例外クラス
  4. スマートポインタと例外処理の関係
    1. RAII(リソース獲得は初期化時に)
    2. 例外安全なコード
  5. std::unique_ptrと例外処理
    1. 例外処理と所有権の移動
    2. リソース管理の一貫性
    3. 例外安全なファクトリ関数
    4. 例外処理におけるmoveセマンティクス
  6. std::shared_ptrと例外処理
    1. 参照カウントによるリソース管理
    2. 循環参照の防止
    3. 例外安全な共有管理
    4. リソースの遅延解放
  7. スマートポインタのデストラクタと例外処理
    1. デストラクタの役割
    2. 例外発生時のデストラクタ呼び出し
    3. 複数のスマートポインタの連携
    4. 例外安全なデストラクタ設計
  8. 例外安全なコードを書くためのベストプラクティス
    1. リソース管理をスマートポインタに任せる
    2. 基本的な例外安全保証を提供する
    3. 強い例外安全保証を提供する
    4. 例外を捕捉して適切に処理する
    5. 不必要な例外を避ける
  9. スマートポインタと例外処理の応用例
    1. データベース接続管理
    2. ファイル操作の管理
    3. 複数のリソースを管理する場合
    4. カスタムデリータの使用
  10. 演習問題
    1. 問題1: 基本的なスマートポインタの使用
    2. 問題2: 例外処理の追加
    3. 問題3: カスタムデリータの実装
    4. 問題4: 循環参照の防止
    5. 問題5: 強い例外安全保証
  11. まとめ

スマートポインタの基本

スマートポインタは、C++11で導入された、メモリ管理を自動化するためのポインタラップ機能です。スマートポインタには主に三つの種類があります。

std::unique_ptr

std::unique_ptrは、単一の所有権を持つスマートポインタです。他のポインタに所有権を移すことはできますが、コピーはできません。これにより、所有権が明確になり、メモリの二重解放などの問題を防ぎます。

#include <memory>
int main() {
    std::unique_ptr<int> ptr = std::make_unique<int>(10);
    // ptrの所有権は一つだけで、他のポインタには所有権を移せません。
}

std::shared_ptr

std::shared_ptrは、複数の所有権を共有できるスマートポインタです。所有権を共有するため、所有するすべてのポインタがスコープから外れると、メモリが解放されます。参照カウントを使用して管理されます。

#include <memory>
int main() {
    std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
    std::shared_ptr<int> ptr2 = ptr1; // ptr1とptr2が所有権を共有します。
}

std::weak_ptr

std::weak_ptrは、所有権を持たないスマートポインタです。主に循環参照を防ぐためにstd::shared_ptrと一緒に使用されます。std::weak_ptrは参照カウントに影響を与えず、必要なときに一時的にstd::shared_ptrに変換して使用します。

#include <memory>
int main() {
    std::shared_ptr<int> sharedPtr = std::make_shared<int>(10);
    std::weak_ptr<int> weakPtr = sharedPtr; // weakPtrは所有権を持ちません。
}

これらのスマートポインタを適切に使用することで、メモリ管理が簡単になり、プログラムの安全性が向上します。次に、スマートポインタを使用する利点について詳しく説明します。

スマートポインタの利点

スマートポインタを使用することで、C++プログラムにおいていくつかの重要な利点が得られます。これにより、メモリ管理の問題を軽減し、プログラムの信頼性と安全性が向上します。

自動メモリ管理

スマートポインタは、スコープを抜けたときに自動的にメモリを解放します。これにより、手動でdeleteを呼び出す必要がなくなり、メモリリークのリスクを減らします。

#include <memory>
int main() {
    std::unique_ptr<int> ptr = std::make_unique<int>(10);
    // ptrがスコープを抜けると、自動的にメモリが解放されます。
}

所有権の明確化

スマートポインタを使用すると、メモリの所有権が明確になります。特に、std::unique_ptrを使うことで、単一の所有権を保証し、二重解放などのエラーを防ぎます。

#include <memory>
int main() {
    std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
    // std::unique_ptr<int> ptr2 = ptr1; // コピーは不可
    std::unique_ptr<int> ptr2 = std::move(ptr1); // 所有権を移動
}

参照カウントによる共有管理

std::shared_ptrは参照カウントを使用して、複数のスマートポインタ間で所有権を共有します。これにより、共有ポインタがすべてスコープを抜けたときにメモリが解放されるため、安全にメモリを共有できます。

#include <memory>
int main() {
    std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
    {
        std::shared_ptr<int> ptr2 = ptr1;
        // ptr1とptr2が所有権を共有
    }
    // ptr2がスコープを抜けても、ptr1が存在する限りメモリは解放されません。
}

循環参照の防止

std::weak_ptrは、所有権を持たないスマートポインタとして、std::shared_ptrと組み合わせて使用することで、循環参照を防ぎます。これにより、参照カウントがゼロにならないという問題を回避できます。

#include <memory>

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;
};

int main() {
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();
    node1->next = node2;
    node2->prev = node1; // 循環参照を防止
}

スマートポインタのこれらの利点を活用することで、C++プログラムのメモリ管理が容易になり、コードの安全性が大幅に向上します。次に、C++における例外処理の基本について説明します。

例外処理の基本

C++における例外処理は、プログラム中で発生する予期しないエラーや異常を適切に扱うための仕組みです。例外処理を利用することで、エラー発生時にプログラムがクラッシュするのを防ぎ、エラーハンドリングを行うことができます。

例外の投げ方

例外は、throwキーワードを使用して投げることができます。例外として投げることができるのは、任意の型のオブジェクトです。

#include <iostream>
#include <stdexcept>

void divide(int a, int b) {
    if (b == 0) {
        throw std::invalid_argument("Division by zero!");
    }
    std::cout << a / b << std::endl;
}

int main() {
    try {
        divide(10, 0);
    } catch (const std::invalid_argument& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }
    return 0;
}

例外のキャッチ

例外をキャッチするためには、tryブロックの中で例外が投げられる可能性のあるコードを記述し、catchブロックで例外を受け取ります。catchブロックは、投げられた例外の型に基づいて、適切に処理を行います。

#include <iostream>

void func() {
    throw std::runtime_error("Something went wrong!");
}

int main() {
    try {
        func();
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

例外の再投げ

例外をキャッチした後に再度投げることもできます。これにより、例外をキャッチして一部処理を行った後、さらに上位の呼び出し元に例外を伝播させることができます。

#include <iostream>

void func2() {
    try {
        throw std::runtime_error("Error in func2");
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught in func2: " << e.what() << std::endl;
        throw; // 再投げ
    }
}

void func1() {
    func2();
}

int main() {
    try {
        func1();
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught in main: " << e.what() << std::endl;
    }
    return 0;
}

例外の継承とカスタム例外クラス

C++では、標準ライブラリの例外クラスを継承して、独自の例外クラスを作成することもできます。これにより、特定のエラー条件に対応する詳細なエラーメッセージや追加の情報を提供することができます。

#include <iostream>
#include <exception>

class MyException : public std::exception {
public:
    const char* what() const noexcept override {
        return "My custom exception occurred";
    }
};

int main() {
    try {
        throw MyException();
    } catch (const MyException& e) {
        std::cerr << "Caught MyException: " << e.what() << std::endl;
    }
    return 0;
}

例外処理の基本を理解することで、予期しないエラーに対して適切な対策を講じ、プログラムの安定性を高めることができます。次に、スマートポインタと例外処理の関係について詳しく説明します。

スマートポインタと例外処理の関係

スマートポインタと例外処理の組み合わせは、C++プログラムの安全性と堅牢性を大幅に向上させます。スマートポインタが例外処理とどのように連携するかを理解することで、メモリ管理が自動化され、例外が発生した場合でもリソースリークを防ぐことができます。

RAII(リソース獲得は初期化時に)

スマートポインタはRAII(Resource Acquisition Is Initialization)という重要な原則に基づいています。RAIIとは、オブジェクトのライフタイムがリソースの獲得と解放を管理するという考え方です。スマートポインタは、スコープを抜けると自動的にリソースを解放するため、例外が発生した場合でもメモリリークを防ぎます。

#include <memory>
#include <iostream>

void func() {
    std::unique_ptr<int> ptr = std::make_unique<int>(10);
    throw std::runtime_error("Error occurred!");
    // 例外が投げられても、ptrはスコープを抜けると自動的にメモリを解放します。
}

int main() {
    try {
        func();
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

例外安全なコード

スマートポインタを使用することで、例外安全なコードを書くことが容易になります。例外安全なコードとは、例外が発生してもプログラムの整合性が保たれるコードを指します。スマートポインタは、例外が発生した場合でも自動的にメモリを解放するため、メモリリークや未定義動作を防ぎます。

基本保証

スマートポインタを使うことで、例外が発生してもプログラムがクラッシュせず、リソースが適切に解放されることを保証できます。

#include <memory>
#include <iostream>

void process(std::unique_ptr<int> ptr) {
    if (*ptr < 0) {
        throw std::runtime_error("Negative value error");
    }
    std::cout << "Processing value: " << *ptr << std::endl;
}

int main() {
    try {
        auto ptr = std::make_unique<int>(-1);
        process(std::move(ptr));
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

強い保証

例外が発生しても、プログラムの状態が変更されないことを保証することも可能です。スマートポインタの所有権の移動を活用して、例外が発生する前の状態に戻すことができます。

#include <memory>
#include <iostream>

void safe_process(std::unique_ptr<int>& ptr) {
    auto backup = std::make_unique<int>(*ptr); // 現在の値をバックアップ
    if (*ptr < 0) {
        throw std::runtime_error("Negative value error");
    }
    *ptr *= 2; // 正常処理
}

int main() {
    try {
        auto ptr = std::make_unique<int>(-1);
        safe_process(ptr);
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

スマートポインタと例外処理の組み合わせにより、C++プログラムの安全性と堅牢性が飛躍的に向上します。次に、具体的なスマートポインタの種類と例外処理の方法について詳しく説明します。

std::unique_ptrと例外処理

std::unique_ptrは、C++11で導入されたスマートポインタで、単一の所有権を持つことを保証します。この特性により、例外処理との組み合わせで強力なメモリ管理を実現できます。

例外処理と所有権の移動

std::unique_ptrは所有権の移動をサポートします。これにより、例外が発生してもメモリリークを防ぐことができます。所有権の移動を活用することで、例外安全なコードを書くことが容易になります。

#include <memory>
#include <iostream>

void process(std::unique_ptr<int> ptr) {
    if (*ptr < 0) {
        throw std::runtime_error("Negative value error");
    }
    std::cout << "Processing value: " << *ptr << std::endl;
}

int main() {
    try {
        auto ptr = std::make_unique<int>(10);
        process(std::move(ptr));
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

リソース管理の一貫性

std::unique_ptrはRAIIの原則に基づいており、スコープを抜けると自動的にリソースを解放します。これにより、例外が発生した場合でも確実にメモリが解放されるため、リソース管理の一貫性が保たれます。

#include <memory>
#include <iostream>

class Resource {
public:
    Resource() { std::cout << "Resource acquired" << std::endl; }
    ~Resource() { std::cout << "Resource released" << std::endl; }
};

void func() {
    std::unique_ptr<Resource> res = std::make_unique<Resource>();
    throw std::runtime_error("Error occurred!");
}

int main() {
    try {
        func();
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

例外安全なファクトリ関数

std::unique_ptrを使用したファクトリ関数は、例外が発生しても安全です。ファクトリ関数内でリソースを確保し、その後std::unique_ptrに渡すことで、リソースリークを防ぐことができます。

#include <memory>
#include <iostream>

std::unique_ptr<int> create_resource(int value) {
    if (value < 0) {
        throw std::invalid_argument("Negative value not allowed");
    }
    return std::make_unique<int>(value);
}

int main() {
    try {
        auto ptr = create_resource(10);
        std::cout << "Resource value: " << *ptr << std::endl;
    } catch (const std::invalid_argument& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

例外処理におけるmoveセマンティクス

std::unique_ptrはmoveセマンティクスをサポートするため、例外が発生した場合でも安全に所有権を移動できます。これにより、例外発生時に所有権が適切に管理されるため、メモリリークを防ぎます。

#include <memory>
#include <iostream>

void process(std::unique_ptr<int> ptr) {
    if (*ptr == 0) {
        throw std::runtime_error("Zero value error");
    }
    std::cout << "Processed value: " << *ptr << std::endl;
}

int main() {
    try {
        auto ptr = std::make_unique<int>(0);
        process(std::move(ptr));
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

これらの例から、std::unique_ptrと例外処理を組み合わせることで、C++プログラムのメモリ管理がどれほど効率的かつ安全になるかがわかります。次に、std::shared_ptrと例外処理の関係について詳しく説明します。

std::shared_ptrと例外処理

std::shared_ptrは、複数の所有権を共有できるスマートポインタです。参照カウントを使ってリソース管理を行い、すべての所有者がスコープを抜けたときにリソースを解放します。これにより、例外処理と組み合わせて安全で効率的なメモリ管理が可能です。

参照カウントによるリソース管理

std::shared_ptrは、参照カウントを使ってリソースを管理します。例外が発生しても、参照カウントがゼロになるまでリソースは解放されないため、安全にメモリを共有できます。

#include <memory>
#include <iostream>

void process(std::shared_ptr<int> ptr) {
    if (*ptr < 0) {
        throw std::runtime_error("Negative value error");
    }
    std::cout << "Processing value: " << *ptr << std::endl;
}

int main() {
    try {
        auto ptr1 = std::make_shared<int>(10);
        auto ptr2 = ptr1;
        process(ptr1);
        // 例外が発生しても、ptr2が存在する限りメモリは解放されません。
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

循環参照の防止

std::shared_ptrを使う際の注意点として、循環参照の問題があります。これを防ぐために、std::weak_ptrと組み合わせて使用します。std::weak_ptrは所有権を持たないため、循環参照を解消する役割を果たします。

#include <memory>
#include <iostream>

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;
    Node() { std::cout << "Node created" << std::endl; }
    ~Node() { std::cout << "Node destroyed" << std::endl; }
};

int main() {
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();
    node1->next = node2;
    node2->prev = node1; // weak_ptrを使用して循環参照を防ぐ
    return 0;
}

例外安全な共有管理

std::shared_ptrを使用することで、例外が発生してもリソース管理が確実に行われます。例外が発生しても参照カウントが適切に管理されるため、リソースリークの心配がありません。

#include <memory>
#include <iostream>

void func() {
    auto ptr = std::make_shared<int>(10);
    throw std::runtime_error("Error occurred!");
    // 例外が発生しても、shared_ptrがスコープを抜けると自動的にメモリが解放されます。
}

int main() {
    try {
        func();
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

リソースの遅延解放

std::shared_ptrは、すべての参照がスコープを抜けたときにリソースを解放するため、リソースの遅延解放が可能です。これにより、リソースの有効期間を柔軟に管理できます。

#include <memory>
#include <iostream>

class Resource {
public:
    Resource() { std::cout << "Resource acquired" << std::endl; }
    ~Resource() { std::cout << "Resource released" << std::endl; }
};

void create_shared_resource() {
    auto ptr = std::make_shared<Resource>();
    // ptrの参照がある限り、リソースは解放されません。
}

int main() {
    create_shared_resource();
    std::cout << "Function exited" << std::endl;
    // リソースはここで解放されます。
    return 0;
}

これらの例から、std::shared_ptrと例外処理を組み合わせることで、C++プログラムの安全性と効率性が向上することがわかります。次に、スマートポインタのデストラクタと例外処理について詳しく説明します。

スマートポインタのデストラクタと例外処理

スマートポインタのデストラクタは、リソースの自動解放を行う重要な役割を果たします。例外処理と組み合わせることで、メモリリークやリソースリークを防ぐことができ、プログラムの安全性をさらに高めることができます。

デストラクタの役割

スマートポインタのデストラクタは、その所有するリソースを解放する役割を持ちます。スコープを抜けるときに自動的に呼び出され、メモリを解放するため、手動でdeleteを呼び出す必要がなくなります。

#include <memory>
#include <iostream>

class Resource {
public:
    Resource() { std::cout << "Resource acquired" << std::endl; }
    ~Resource() { std::cout << "Resource released" << std::endl; }
};

void func() {
    std::unique_ptr<Resource> res = std::make_unique<Resource>();
    // デストラクタは、resがスコープを抜けると自動的に呼び出されます。
}

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

例外発生時のデストラクタ呼び出し

例外が発生した場合でも、スマートポインタのデストラクタは確実に呼び出されます。これにより、リソースが適切に解放され、メモリリークを防ぐことができます。

#include <memory>
#include <iostream>

class Resource {
public:
    Resource() { std::cout << "Resource acquired" << std::endl; }
    ~Resource() { std::cout << "Resource released" << std::endl; }
};

void func() {
    std::unique_ptr<Resource> res = std::make_unique<Resource>();
    throw std::runtime_error("Error occurred!");
    // 例外が発生しても、resのデストラクタが呼び出され、メモリが解放されます。
}

int main() {
    try {
        func();
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

複数のスマートポインタの連携

複数のスマートポインタを使用する場合、それぞれのデストラクタが適切に呼び出されることで、リソース管理が自動化されます。これにより、例外が発生しても、すべてのリソースが確実に解放されます。

#include <memory>
#include <iostream>

class Resource {
public:
    Resource() { std::cout << "Resource acquired" << std::endl; }
    ~Resource() { std::cout << "Resource released" << std::endl; }
};

void func() {
    auto res1 = std::make_unique<Resource>();
    auto res2 = std::make_unique<Resource>();
    throw std::runtime_error("Error occurred!");
    // 例外が発生しても、res1とres2のデストラクタが呼び出されます。
}

int main() {
    try {
        func();
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

例外安全なデストラクタ設計

デストラクタ自体が例外を投げることは避けるべきです。デストラクタが例外を投げると、例外が二重に発生し、プログラムの予測不可能な動作を引き起こす可能性があります。デストラクタ内で例外が発生する可能性がある場合は、例外をキャッチして適切に処理することが重要です。

#include <iostream>

class Resource {
public:
    Resource() { std::cout << "Resource acquired" << std::endl; }
    ~Resource() {
        try {
            // 例外を投げる可能性のあるコード
            throw std::runtime_error("Error in destructor");
        } catch (const std::exception& e) {
            std::cerr << "Exception caught in destructor: " << e.what() << std::endl;
            // 例外を再投げしない
        }
        std::cout << "Resource released" << std::endl;
    }
};

int main() {
    try {
        Resource res;
        throw std::runtime_error("Error occurred!");
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

これらの例から、スマートポインタのデストラクタと例外処理の組み合わせにより、C++プログラムのリソース管理がどれほど効率的かつ安全になるかがわかります。次に、例外安全なコードを書くためのベストプラクティスについて詳しく説明します。

例外安全なコードを書くためのベストプラクティス

例外安全なコードを書くことは、C++プログラムの信頼性と保守性を向上させるために重要です。ここでは、例外安全なコードを書くための具体的なベストプラクティスを紹介します。

リソース管理をスマートポインタに任せる

動的メモリやリソースの管理を手動で行うと、例外が発生したときにリソースリークの原因になります。スマートポインタ(std::unique_ptrstd::shared_ptr)を使用することで、リソース管理を自動化し、例外が発生しても確実にリソースが解放されるようにします。

#include <memory>
#include <iostream>

void process() {
    std::unique_ptr<int> ptr = std::make_unique<int>(10);
    // 例外が発生しても、ptrがスコープを抜けると自動的にメモリが解放されます。
    throw std::runtime_error("Error occurred!");
}

int main() {
    try {
        process();
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

基本的な例外安全保証を提供する

基本的な例外安全保証とは、例外が発生してもリソースがリークせず、プログラムの状態が一貫していることを意味します。これを実現するために、リソースの確保と解放を適切に行うことが重要です。

#include <memory>
#include <iostream>

class Resource {
public:
    Resource() { std::cout << "Resource acquired" << std::endl; }
    ~Resource() { std::cout << "Resource released" << std::endl; }
};

void process() {
    auto res = std::make_unique<Resource>();
    // 例外が発生しても、resがスコープを抜けると自動的にリソースが解放されます。
    throw std::runtime_error("Error occurred!");
}

int main() {
    try {
        process();
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

強い例外安全保証を提供する

強い例外安全保証とは、例外が発生してもプログラムの状態が変更されないことを意味します。これを実現するためには、操作を行う前にバックアップを作成し、操作が成功した場合にのみ状態を更新する方法が有効です。

#include <memory>
#include <iostream>
#include <vector>

void safe_insert(std::vector<int>& vec, int value) {
    std::vector<int> backup = vec; // 現在の状態をバックアップ
    vec.push_back(value);
    if (value < 0) {
        vec = backup; // 例外発生時に状態を元に戻す
        throw std::runtime_error("Negative value error");
    }
}

int main() {
    std::vector<int> numbers;
    try {
        safe_insert(numbers, 10);
        safe_insert(numbers, -5);
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    for (int num : numbers) {
        std::cout << num << " ";
    }
    return 0;
}

例外を捕捉して適切に処理する

例外を捕捉して適切に処理することで、プログラムのクラッシュを防ぎ、ユーザーに適切なフィードバックを提供できます。特定の例外を捕捉することが重要です。

#include <iostream>
#include <stdexcept>

void func() {
    throw std::runtime_error("Runtime error occurred");
}

int main() {
    try {
        func();
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught a runtime error: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

不必要な例外を避ける

例外の発生を最小限に抑えるために、事前条件のチェックを行い、不必要な例外を避けることが重要です。

#include <iostream>
#include <stdexcept>

void divide(int a, int b) {
    if (b == 0) {
        throw std::invalid_argument("Division by zero");
    }
    std::cout << "Result: " << a / b << std::endl;
}

int main() {
    try {
        divide(10, 0);
    } catch (const std::invalid_argument& e) {
        std::cerr << "Caught an invalid argument: " << e.what() << std::endl;
    }
    return 0;
}

これらのベストプラクティスを実践することで、例外安全なコードを書き、C++プログラムの信頼性と保守性を向上させることができます。次に、スマートポインタと例外処理の応用例を紹介します。

スマートポインタと例外処理の応用例

ここでは、スマートポインタと例外処理を組み合わせた実際のコード例を通じて、その応用方法を紹介します。これにより、スマートポインタと例外処理の有効な使用方法を具体的に理解できます。

データベース接続管理

スマートポインタと例外処理を使ってデータベース接続を管理する例です。例外が発生しても、確実に接続が閉じられるようにします。

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

// データベース接続を管理するクラス
class DatabaseConnection {
public:
    DatabaseConnection() {
        std::cout << "Database connected" << std::endl;
    }
    ~DatabaseConnection() {
        std::cout << "Database disconnected" << std::endl;
    }
    void executeQuery(const std::string& query) {
        if (query.empty()) {
            throw std::invalid_argument("Query is empty");
        }
        std::cout << "Executing query: " << query << std::endl;
    }
};

void performDatabaseOperations() {
    std::unique_ptr<DatabaseConnection> dbConn = std::make_unique<DatabaseConnection>();
    dbConn->executeQuery("SELECT * FROM users");
    dbConn->executeQuery(""); // 例外が発生
}

int main() {
    try {
        performDatabaseOperations();
    } catch (const std::exception& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

ファイル操作の管理

ファイル操作をスマートポインタと例外処理で管理する例です。例外が発生してもファイルが確実に閉じられるようにします。

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

// ファイルを管理するクラス
class FileHandler {
public:
    FileHandler(const std::string& filename) : file(std::fopen(filename.c_str(), "r")) {
        if (!file) {
            throw std::runtime_error("Could not open file");
        }
        std::cout << "File opened: " << filename << std::endl;
    }
    ~FileHandler() {
        if (file) {
            std::fclose(file);
            std::cout << "File closed" << std::endl;
        }
    }
    void readFile() {
        if (!file) {
            throw std::runtime_error("File not open");
        }
        // ファイル読み取り操作を実行
        std::cout << "Reading file..." << std::endl;
    }
private:
    FILE* file;
};

void performFileOperations(const std::string& filename) {
    std::unique_ptr<FileHandler> fileHandler = std::make_unique<FileHandler>(filename);
    fileHandler->readFile();
}

int main() {
    try {
        performFileOperations("example.txt");
    } catch (const std::exception& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

複数のリソースを管理する場合

複数のリソースをスマートポインタと例外処理で管理する例です。リソースが確実に解放されることを保証します。

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

// リソースAを管理するクラス
class ResourceA {
public:
    ResourceA() {
        std::cout << "ResourceA acquired" << std::endl;
    }
    ~ResourceA() {
        std::cout << "ResourceA released" << std::endl;
    }
};

// リソースBを管理するクラス
class ResourceB {
public:
    ResourceB() {
        std::cout << "ResourceB acquired" << std::endl;
    }
    ~ResourceB() {
        std::cout << "ResourceB released" << std::endl;
    }
};

void performOperations() {
    auto resA = std::make_unique<ResourceA>();
    auto resB = std::make_unique<ResourceB>();
    // リソースを使用する操作
    throw std::runtime_error("Error during operations");
}

int main() {
    try {
        performOperations();
    } catch (const std::exception& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

カスタムデリータの使用

スマートポインタにカスタムデリータを設定して、特殊なリソースの管理を行う例です。

#include <memory>
#include <iostream>

// カスタムデリータ
struct CustomDeleter {
    void operator()(int* ptr) const {
        std::cout << "Custom deleting resource" << std::endl;
        delete ptr;
    }
};

void performCustomDeletion() {
    std::unique_ptr<int, CustomDeleter> ptr(new int(42), CustomDeleter());
    // 例外が発生してもカスタムデリータが呼ばれます
    throw std::runtime_error("Error occurred");
}

int main() {
    try {
        performCustomDeletion();
    } catch (const std::exception& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

これらの応用例を通じて、スマートポインタと例外処理の有効な使用方法が具体的に理解できるでしょう。最後に、スマートポインタと例外処理に関する演習問題を紹介します。

演習問題

ここでは、スマートポインタと例外処理に関する理解を深めるための演習問題を紹介します。これらの問題を通じて、実践的なスキルを身につけましょう。

問題1: 基本的なスマートポインタの使用

以下のコードを完成させて、std::unique_ptrを使ってメモリリークを防いでください。

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass acquired" << std::endl; }
    ~MyClass() { std::cout << "MyClass released" << std::endl; }
    void doSomething() { std::cout << "Doing something" << std::endl; }
};

void process() {
    // ここでstd::unique_ptrを使ってMyClassのインスタンスを管理する
    // std::unique_ptr<MyClass> ptr = ???
}

int main() {
    try {
        process();
    } catch (const std::exception& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

問題2: 例外処理の追加

以下のコードに例外処理を追加し、ファイルが開けない場合に適切なメッセージを表示してください。

#include <iostream>
#include <fstream>
#include <memory>

class FileHandler {
public:
    FileHandler(const std::string& filename) {
        file = std::fopen(filename.c_str(), "r");
        if (!file) {
            // ここで例外を投げる
        }
    }
    ~FileHandler() {
        if (file) {
            std::fclose(file);
        }
    }
    void readFile() {
        // ファイル読み取り操作
    }
private:
    FILE* file;
};

void performFileOperations(const std::string& filename) {
    std::unique_ptr<FileHandler> fileHandler;
    // ここで例外処理を追加してfileHandlerを初期化
}

int main() {
    try {
        performFileOperations("example.txt");
    } catch (const std::exception& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

問題3: カスタムデリータの実装

カスタムデリータを使って、動的に確保されたメモリを適切に解放するコードを作成してください。

#include <iostream>
#include <memory>

struct CustomDeleter {
    void operator()(int* ptr) const {
        // カスタムデリータの実装
    }
};

void customDeletionExample() {
    // カスタムデリータを使ってメモリを管理するstd::unique_ptrを作成
    // std::unique_ptr<int, CustomDeleter> ptr = ???
}

int main() {
    try {
        customDeletionExample();
    } catch (const std::exception& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    return 0;
}

問題4: 循環参照の防止

std::shared_ptrstd::weak_ptrを使って、循環参照を防ぐコードを作成してください。

#include <iostream>
#include <memory>

class Node {
public:
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;
    Node() { std::cout << "Node created" << std::endl; }
    ~Node() { std::cout << "Node destroyed" << std::endl; }
};

void createCycle() {
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();
    node1->next = node2;
    node2->prev = node1; // 循環参照を防ぐ
}

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

問題5: 強い例外安全保証

以下のコードを修正して、強い例外安全保証を提供するようにしてください。

#include <iostream>
#include <vector>

void safeInsert(std::vector<int>& vec, int value) {
    // ここでバックアップを作成し、操作が成功した場合にのみ状態を更新
    vec.push_back(value);
    if (value < 0) {
        throw std::runtime_error("Negative value error");
    }
}

int main() {
    std::vector<int> numbers;
    try {
        safeInsert(numbers, 10);
        safeInsert(numbers, -5);
    } catch (const std::exception& e) {
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    for (int num : numbers) {
        std::cout << num << " ";
    }
    return 0;
}

これらの演習問題を通じて、スマートポインタと例外処理の理解を深め、実践的なスキルを身につけてください。次に、本記事のまとめに移ります。

まとめ

本記事では、C++のスマートポインタと例外処理の基本から応用までを包括的に解説しました。スマートポインタ(std::unique_ptrstd::shared_ptrstd::weak_ptr)を使うことで、手動でのメモリ管理を自動化し、例外発生時でも確実にリソースを解放できることを学びました。また、例外安全なコードを書くためのベストプラクティスや具体的な応用例を通じて、実践的なスキルを身につけました。

スマートポインタと例外処理を適切に組み合わせることで、C++プログラムの安全性、効率性、保守性を大幅に向上させることができます。今回の演習問題を通じて、自らの手で実装し、さらに理解を深めてください。これにより、より堅牢で信頼性の高いC++プログラムを開発することができるでしょう。

コメント

コメントする

目次
  1. スマートポインタの基本
    1. std::unique_ptr
    2. std::shared_ptr
    3. std::weak_ptr
  2. スマートポインタの利点
    1. 自動メモリ管理
    2. 所有権の明確化
    3. 参照カウントによる共有管理
    4. 循環参照の防止
  3. 例外処理の基本
    1. 例外の投げ方
    2. 例外のキャッチ
    3. 例外の再投げ
    4. 例外の継承とカスタム例外クラス
  4. スマートポインタと例外処理の関係
    1. RAII(リソース獲得は初期化時に)
    2. 例外安全なコード
  5. std::unique_ptrと例外処理
    1. 例外処理と所有権の移動
    2. リソース管理の一貫性
    3. 例外安全なファクトリ関数
    4. 例外処理におけるmoveセマンティクス
  6. std::shared_ptrと例外処理
    1. 参照カウントによるリソース管理
    2. 循環参照の防止
    3. 例外安全な共有管理
    4. リソースの遅延解放
  7. スマートポインタのデストラクタと例外処理
    1. デストラクタの役割
    2. 例外発生時のデストラクタ呼び出し
    3. 複数のスマートポインタの連携
    4. 例外安全なデストラクタ設計
  8. 例外安全なコードを書くためのベストプラクティス
    1. リソース管理をスマートポインタに任せる
    2. 基本的な例外安全保証を提供する
    3. 強い例外安全保証を提供する
    4. 例外を捕捉して適切に処理する
    5. 不必要な例外を避ける
  9. スマートポインタと例外処理の応用例
    1. データベース接続管理
    2. ファイル操作の管理
    3. 複数のリソースを管理する場合
    4. カスタムデリータの使用
  10. 演習問題
    1. 問題1: 基本的なスマートポインタの使用
    2. 問題2: 例外処理の追加
    3. 問題3: カスタムデリータの実装
    4. 問題4: 循環参照の防止
    5. 問題5: 強い例外安全保証
  11. まとめ