C++のshared_ptrをマスターする7つの方法

C++のshared_ptrを徹底解説するイメージC++
この記事は約18分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

C++において、メモリ管理は重要な役割を果たします。

特にスマートポインターの一種であるshared_ptrは、その使い方を理解し、適切に活用することで、メモリリークやその他のメモリ関連の問題を防ぐ助けとなります。

この記事を読むことで、初心者から上級者まで、C++のshared_ptrを深く理解し、効果的に使用する方法を学ぶことができます。

まずは、shared_ptrの基礎から始めましょう。

●C++とshared_ptrの基本

C++プログラミングにおけるメモリ管理は、プログラムのパフォーマンスと安定性に直接影響します。

C++では、開発者が直接メモリを管理するため、その負担と複雑さは他の言語よりも大きいと言えます。

ここで、スマートポインターの役割が重要になります。

スマートポインターは、ポインターのライフサイクルを自動で管理し、メモリリークや野良ポインターの問題を防ぐのに役立ちます。

○C++におけるメモリ管理の基礎

C++では、動的に確保されたメモリ(ヒープメモリ)の管理が必要です。

newdeleteは、それぞれメモリの確保と解放を行うために使用されますが、これらを適切に管理しないとメモリリークなどの問題が生じます。

スマートポインターは、これらの問題を自動的に管理するメカニズムを提供します。

○shared_ptrとは何か?

shared_ptrは、C++11から導入されたスマートポインターの一種です。

複数のshared_ptrインスタンスが同じリソースを共有し、最後のshared_ptrがスコープ外に出ると自動的にリソースが解放されます。

これにより、手動でのメモリ解放の必要がなくなり、メモリ管理の負担が軽減されます。

○shared_ptrの基本的な使い方

shared_ptrの基本的な使い方は、リソースの確保と共有に関連しています。

std::make_shared関数を用いてshared_ptrを生成すると、指定した型のオブジェクトが動的に確保され、そのポインターを管理するshared_ptrが返されます。

shared_ptrはコピー可能であり、コピーするごとに内部の参照カウントが増加します。

参照カウントがゼロになると、自動的にメモリが解放されます。

●shared_ptrの詳細な使い方

shared_ptrは、その柔軟性と安全性の高さから、C++のメモリ管理において非常に重要な役割を担います。

ここでは、shared_ptrのより詳細な使い方について、実際のサンプルコードを交えながら解説します。

これにより、shared_ptrの多様な活用方法や、それに伴う注意点を理解することができます。

○サンプルコード1:基本的なshared_ptrの初期化と利用

最初のサンプルコードでは、shared_ptrの基本的な初期化と利用方法を紹介します。

shared_ptrを使用して動的に確保したオブジェクトを管理する方法を確認しましょう。

#include <memory>
#include <iostream>

int main() {
    std::shared_ptr<int> ptr = std::make_shared<int>(10);
    std::cout << "ptrの値: " << *ptr << std::endl;
    return 0;
}

このコードでは、std::make_shared<int>(10)を使用して、整数値10を持つint型のオブジェクトを動的に確保し、そのshared_ptrをptrに格納しています。

*ptrを出力することで、ptrが指すオブジェクトの値を確認できます。

このコードを実行すると、コンソールには「ptrの値: 10」と表示されます。

○サンプルコード2:shared_ptrでのオブジェクトの共有

shared_ptrの強みの一つは、複数のshared_ptrインスタンス間でオブジェクトを安全に共有できることです。

下記のサンプルコードでは、2つのshared_ptrが同じオブジェクトを共有している様子を表しています。

#include <memory>
#include <iostream>

int main() {
    std::shared_ptr<int> ptr1 = std::make_shared<int>(20);
    std::shared_ptr<int> ptr2 = ptr1; // ptr1と同じオブジェクトを共有

    std::cout << "ptr1の値: " << *ptr1 << ", ptr2の値: " << *ptr2 << std::endl;
    std::cout << "ptr1とptr2の参照カウント: " << ptr1.use_count() << std::endl;
    return 0;
}

このコードでは、ptr1ptr2が同じオブジェクトを指していることがわかります。

また、ptr1.use_count()を使うことで、共有されているオブジェクトの参照カウント(この場合は2)を出力できます。

○サンプルコード3:shared_ptrとカスタムデリーター

shared_ptrでは、オブジェクトの解放時に特定の処理を行うためのカスタムデリーターを指定することができます。

下記のサンプルでは、カスタムデリーターの使用例を表しています。

#include <memory>
#include <iostream>

void customDeleter(int* p) {
    std::cout << "カスタムデリーターによる解放: " << *p << std::endl;
    delete p;
}

int main() {
    std::shared_ptr<int> ptr(new int(30), customDeleter);
    // ここでは何もしない
    return 0; // ptrがスコープを抜けるときにcustomDeleterが呼ばれる
}

このコードでは、new int(30)により確保された整数値30のメモリをptrで管理し、ptrがスコープを抜ける際にcustomDeleter関数が呼ばれます。

これにより、メモリ解放時に特定のログを出力するなどの追加処理を行うことが可能です。

○サンプルコード4:shared_ptrの配列利用

shared_ptrは、単一のオブジェクトだけでなく、配列の管理にも使用できます。

配列を用いた場合のshared_ptrの使用方法を次のサンプルで確認しましょう。

#include <memory>
#include <iostream>

int main() {
    std::shared_ptr<int[]> ptr = std::shared_ptr<int[]>(new int[3]{40, 50, 60}, std::default_delete<int[]>());
    for (int i = 0; i < 3; ++i) {
        std::cout << "ptr[" << i << "]: " << ptr[i] << std::endl;
    }
    return 0; // ptrがスコープを抜けるときに配列が自動的に解放される
}

このコードでは、整数値の配列を動的に確保し、そのshared_ptrをptrに格納しています。

ループを使用して配列の各要素にアクセスし、値を出力しています。

ここでも、shared_ptrがスコープを抜ける際に、配列が自動的に解放される点に注意してください。

●shared_ptrの応用例

shared_ptrは、基本的な使い方を理解した上で、様々な応用シナリオにおいてその力を発揮します。

ここでは、特に実用的ないくつかの応用例をサンプルコードを交えて解説します。

これらの例は、shared_ptrの柔軟性と効率性を理解するのに役立ちます。

○サンプルコード5:shared_ptrを使用したリソースの管理

shared_ptrは、複数のリソースを管理する際にも有効です。

下記のコードは、shared_ptrを使用して複数のオブジェクトを管理する方法を表しています。

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

class Resource {
public:
    Resource() { std::cout << "リソース生成\n"; }
    ~Resource() { std::cout << "リソース破棄\n"; }
};

int main() {
    std::vector<std::shared_ptr<Resource>> resources;
    resources.push_back(std::make_shared<Resource>());
    resources.push_back(std::make_shared<Resource>());
    resources.push_back(std::make_shared<Resource>());

    std::cout << "リソースの数: " << resources.size() << std::endl;
    // リソースの利用
    // ...

    return 0; // ベクタがスコープを抜けると、全てのリソースが自動的に破棄される
}

このコードでは、Resourceクラスのインスタンスを管理するために、std::vectorstd::shared_ptr<Resource>を格納しています。

ベクタのスコープを抜ける際に、これらのリソースは自動的に解放されます。

○サンプルコード6:shared_ptrと多重継承

多重継承を使用する場合、shared_ptrは基底クラスへのポインタとして安全に扱うことができます。

下記の例では、多重継承されたクラスに対するshared_ptrの使用方法を表しています。

#include <memory>
#include <iostream>

class Base1 {
public:
    virtual void func1() { std::cout << "Base1のfunc1\n"; }
    virtual ~Base1() { std::cout << "Base1破棄\n"; }
};

class Base2 {
public:
    virtual void func2() { std::cout << "Base2のfunc2\n"; }
    virtual ~Base2() { std::cout << "Base2破棄\n"; }
};

class Derived : public Base1, public Base2 {
public:
    void func1() override { std::cout << "Derivedのfunc1\n"; }
    void func2() override { std::cout << "Derivedのfunc2\n"; }
};

int main() {
    std::shared_ptr<Base1> ptr = std::make_shared<Derived>();
    ptr->func1();
    // ptr->func2(); これはエラーになる

    return 0; // ptrがスコープを抜けると、Derivedオブジェクトが自動的に破棄される
}

このコードでは、DerivedクラスがBase1Base2から継承されています。

shared_ptrはBase1型としてDerivedオブジェクトを指しています。

○サンプルコード7:shared_ptrとスレッドセーフ

shared_ptrは、マルチスレッド環境での使用においても、その参照カウントの操作がスレッドセーフであるため安全です。

下記のコードは、マルチスレッド環境でshared_ptrを使用する一例を表しています。

#include <memory>
#include <thread>
#include <iostream>

void threadFunc(std::shared_ptr<int> ptr) {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "スレッドでのptrの値: " << *ptr << std::endl;
}

int main() {
    std::shared_ptr<int> ptr = std::make_shared<int>(100);
    std::thread t1(threadFunc, ptr);
    std::thread t2(threadFunc, ptr);

    t1.join();
    t2.join();

    return 0; // スレッドが終了した後、ptrがスコープを抜けるとメモリが解放される
}

このコードでは、std::threadを使用して二つのスレッドを作成し、それぞれにshared_ptrを渡しています。

各スレッドは安全にshared_ptrを使用し、スレッドの終了後にはメモリが自動的に解放されます。

●shared_ptrの注意点と対処法

shared_ptrを使用する際には、いくつかの注意点があります。

これらを理解し、適切に対処することで、shared_ptrをより安全かつ効率的に利用することができます。

ここでは、特に重要な注意点とその対処法について説明します。

○循環参照とその解決方法

shared_ptrの最も一般的な問題の一つは循環参照です。

循環参照は、2つ以上のshared_ptrが互いに参照し合っている状態を指し、これにより参照カウントがゼロにならず、メモリリークを引き起こす可能性があります。

この問題を解決するためには、一方の参照をstd::weak_ptrに変更することが一般的です。

std::weak_ptrは参照カウントを増やさず、循環参照を防ぐことができます。

#include <memory>
#include <iostream>

class B; // 前方宣言

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A破棄\n"; }
};

class B {
public:
    std::shared_ptr<A> a_ptr;
    ~B() { std::cout << "B破棄\n"; }
};

int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b_ptr = b;
    b->a_ptr = a; // 循環参照が発生

    return 0; // メモリリークが発生
}

この問題を解決するためには、下記のようにstd::weak_ptrを使用します。

class B;

class A {
public:
    std::weak_ptr<B> b_ptr; // weak_ptrを使用
    ~A() { std::cout << "A破棄\n"; }
};

// Bクラスの定義は同じ

int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();
    a->b_ptr = b;
    b->a_ptr = a; // 循環参照を防止

    return 0; // 正常にメモリが解放される
}

○パフォーマンスに関する考慮事項

shared_ptrは便利ですが、無闇に使用するとパフォーマンスの低下を招くことがあります。

特に、shared_ptrのコピー操作は参照カウントの更新が伴うため、コピーのコストが高くなります。

また、不必要にshared_ptrを使用することは、メモリ使用量の増加にもつながります。

パフォーマンスを重視する場合は、shared_ptrの使用を最小限に抑え、必要な場面でのみ使用することが推奨されます。

また、可能な限りstd::make_sharedを使用して、メモリ確保の効率化を図ることも有効です。

●shared_ptrのカスタマイズ方法

shared_ptrは、その機能をカスタマイズすることで、さらに多様な用途に対応することができます。

ここでは、shared_ptrのカスタマイズ方法として、特にカスタムアロケーターの使用とshared_ptrの拡張について詳しく解説します。

○カスタムアロケーターの使用

shared_ptrでは、デフォルトのメモリアロケーターの代わりにカスタムアロケーターを使用することができます。

これにより、メモリ確保の挙動を制御し、特定のメモリプールからオブジェクトを確保するなどのカスタマイズが可能になります。

下記のコードは、カスタムアロケーターを使用したshared_ptrの例を表しています。

#include <memory>
#include <iostream>

template <typename T>
struct CustomAllocator {
    using value_type = T;

    CustomAllocator() = default;

    template <class U> constexpr CustomAllocator(const CustomAllocator<U>&) noexcept {}

    [[nodiscard]] T* allocate(std::size_t n) {
        std::cout << "カスタムアロケーターでメモリ確保\n";
        return static_cast<T*>(::operator new(n * sizeof(T)));
    }

    void deallocate(T* p, std::size_t) noexcept {
        std::cout << "カスタムアロケーターでメモリ解放\n";
        ::operator delete(p);
    }
};

int main() {
    std::allocator<int> alloc;
    std::shared_ptr<int> ptr = std::allocate_shared<int, CustomAllocator<int>>(alloc, 10);

    std::cout << "ptrの値: " << *ptr << std::endl;
    return 0;
}

このコードでは、CustomAllocatorを定義し、std::allocate_sharedを使用してそのアロケーターでメモリを確保しています。

この方法を用いることで、メモリ確保の挙動をカスタマイズすることが可能です。

○shared_ptrの拡張

shared_ptrのもう一つのカスタマイズ方法として、拡張機能を利用することがあります。

たとえば、shared_ptrに追加情報を格納するためのカスタム構造体を使用したり、特定の処理をラップする関数を提供したりすることができます。

下記の例では、shared_ptrを拡張して追加情報を保持する方法を表しています。

#include <memory>
#include <iostream>
#include <string>

template <typename T>
struct EnhancedSharedPtr {
    std::shared_ptr<T> ptr;
    std::string additionalInfo;

    EnhancedSharedPtr(std::shared_ptr<T> p, std::string info)
        : ptr(std::move(p)), additionalInfo(std::move(info)) {}

    T& operator*() const { return *ptr; }
    T* operator->() const { return ptr.operator->(); }
};

int main() {
    auto rawPtr = new int(20);
    auto sharedPtr = std::shared_ptr<int>(rawPtr);
    EnhancedSharedPtr<int> enhancedPtr(sharedPtr, "追加情報");

    std::cout << "値: " << *enhancedPtr << ", 追加情報: " << enhancedPtr.additionalInfo << std::endl;
    return 0;
}

このコードでは、EnhancedSharedPtrという構造体を定義し、shared_ptrに追加情報を付加しています。

これにより、shared_ptrの基本的な機能はそのままに、ユーザー定義の追加情報を保持することができます。

まとめ

本記事では、C++のshared_ptrの基本的な使い方から応用技術、注意点、カスタマイズ方法までを詳細に解説しました。

shared_ptrを適切に使いこなすことで、メモリ管理の効率性と安全性を大幅に向上させることができます。

循環参照の回避やパフォーマンスへの影響など、注意すべき点を理解することが重要です。

また、カスタムアロケーターや拡張機能の利用により、より柔軟なプログラミングが可能になります。

この知識を活用して、より効果的なC++プログラミングを実現しましょう。