閉じる

【C++】weak_ptr::lockの使い方とshared_ptrへの安全な変換方法

C++においてスマートポインタを使いこなすことは、メモリリークや不正アクセスを防ぐために不可欠なスキルです。

その中でも「std::weak_ptr」は、循環参照を防ぐ重要な役割を担っていますが、そのままでは中身のデータにアクセスできないという特徴があります。

そこで必要になるのがlock()メソッドです。

本記事では、weak_ptrからshared_ptrへ安全に変換する方法や、なぜexpired()よりもlock()を使うべきなのかについて、現在の最新のベストプラクティスを交えて詳しく解説します。

weak_ptrとlock()メソッドの基本概念

std::weak_ptrは、std::shared_ptrが管理するオブジェクトを「所有せずに監視する」ためのスマートポインタです。

しかし、監視しているだけでは、そのオブジェクトのメンバ変数やメソッドを利用することはできません。

そこで、一時的に所有権を共有するshared_ptrに昇格させる手続きが必要となります。

それがlock()です。

なぜweak_ptrにはlock()が必要なのか

std::shared_ptrは参照カウンタを利用して、誰も参照しなくなった瞬間にメモリを解放します。

もし、weak_ptrが直接オブジェクトにアクセスできてしまうと、アクセスしている最中に他のスレッドによってオブジェクトが破棄されるという危険性があります。

これを防ぐために、lock()メソッドを使用して「今から使うので、使い終わるまで破棄しないでください」と宣言し、一時的に参照カウンタをインクリメントしたshared_ptrを取得する必要があるのです。

lock()メソッドの戻り値と挙動

lock()メソッドを呼び出した際、対象のオブジェクトがまだ生存しているかどうかで戻り値が変わります。

オブジェクトの状態lock()の戻り値
生存している対象を指す有効なshared_ptr
すでに破棄されている空の(nullptrを指す)shared_ptr

このように、lock()は例外を投げるのではなく、ヌルポインタを返すことで安全に失敗を知らせてくれる設計になっています。

weak_ptr::lock()の具体的な使い方

実際にどのようにコードを記述するのが正解なのか、基本的なパターンを見ていきましょう。

基本的な実装パターン

以下のサンプルコードは、weak_ptrからshared_ptrを取り出し、安全にメンバ関数を呼び出す標準的な書き方です。

C++
#include <iostream>
#include <memory>

class HeavyObject {
   public:
    HeavyObject(int id) : id_(id) {
        std::cout << "オブジェクト " << id_ << " が生成されました。\n";
    }
    ~HeavyObject() {
        std::cout << "オブジェクト " << id_ << " が破棄されました。\n";
    }
    void doSomething() {
        std::cout << "オブジェクト " << id_ << " のメソッドを実行中...\n";
    }

   private:
    int id_;
};

int main() {
    // shared_ptrの生成
    std::shared_ptr<HeavyObject> sp = std::make_shared<HeavyObject>(100);

    // weak_ptrへ代入(参照カウンタは増えない)
    std::weak_ptr<HeavyObject> wp = sp;

    // --- 安全なアクセスの手順 ---

    // 1. lock()を呼び出してshared_ptrを取得
    if (auto shared = wp.lock()) {
        // 2. 取得に成功した場合のみアクセスする
        // このブロック内ではsharedが存在するため、オブジェクトは破棄されない
            shared->doSomething();
    } else {
        // 3. すでに破棄されていた場合の処理
        std::cout << "オブジェクトはすでに存在しません。\n";
    }

    // 元のshared_ptrをリセットして破棄させる
    sp.reset();

    // 再度アクセスを試みる
    if (auto shared = wp.lock()) {
        shared->doSomething();
    } else {
        // オブジェクトが破棄されているため、こちらに入る
            std::cout << "アクセス失敗:オブジェクトが消失しています。\n";
    }

    return 0;
}
実行結果
オブジェクト 100 が生成されました。
オブジェクト 100 のメソッドを実行中...
オブジェクト 100 が破棄されました。
アクセス失敗:オブジェクトが消失しています。

コードのポイント解説

この実装で最も重要なのは、if (auto shared = wp.lock())という記述です。

C++のif文の初期化式を利用することで、shared_ptrのスコープをifブロック内に限定できます。

これにより、使い終わったshared_ptrがいつまでも残り続け、メモリ解放を妨げるミスを防ぐことができます。

expired()との違いと使い分け

std::weak_ptrには、オブジェクトが破棄されているかを確認するexpired()というメソッドも存在します。

しかし、「値を利用する目的」であればexpired()ではなくlock()を使うべきです。

expired()を使う際のリスク(競合状態)

マルチスレッド環境において、expired()によるチェックと、その後のアクセスには「Time of Check to Time of Use (TOCTOU)」と呼ばれる脆弱性が潜んでいます。

lock()が推奨される理由

lock()メソッドは、オブジェクトが生存しているかのチェックと、参照カウンタのインクリメントをアトミック(不可分)に行います。

つまり、lock()が有効なshared_ptrを返した時点で、そのshared_ptrを保持している限り、他のスレッドが何をしようともオブジェクトが勝手に消えることはありません。

これが「安全な変換」と呼ばれる所以です。

一方、expired()は「今消えているか」を知るための軽量な関数であり、主に「キャッシュの掃除」や「不要なコールバックのリストからの削除」など、オブジェクトの中身に触れる必要がない場合に使用します。

weak_ptr::lock()の活用シーン

weak_ptrlock()は、特定の設計パターンにおいて非常に強力なツールとなります。

オブザーバーパターンの実装

オブザーバー(観察者)がサブジェクト(観察対象)を登録する際、shared_ptrで持ってしまうと、循環参照が発生したり、オブザーバーの存在のせいでオブジェクトが一生破棄されなかったりします。

C++
#include <iostream>
#include <vector>
#include <memory>
#include <algorithm>

class Observer {
public:
    void update(int value) {
        std::cout << "通知を受け取りました: " << value << "\n";
    }
};

class Subject {
public:
    void addObserver(std::weak_ptr<Observer> observer) {
        observers_.push_back(observer);
    }

    void notify(int value) {
        // 安全に通知を送る
        for (auto it = observers_.begin(); it != observers_.end(); ) {
            if (auto obs = it->lock()) {
                obs->update(value);
                ++it;
            } else {
                // オブジェクトが消えていればリストから削除
                std::cout << "無効なオブザーバーを削除します。\n";
                it = observers_.erase(it);
            }
        }
    }

private:
    std::vector<std::weak_ptr<Observer>> observers_;
};

int main() {
    Subject subject;
    auto obs1 = std::make_shared<Observer>();
    
    subject.addObserver(obs1);
    subject.notify(1);

    {
        auto obs2 = std::make_shared<Observer>();
        subject.addObserver(obs2);
        subject.notify(2);
        // obs2はここでスコープを抜け、破棄される
    }

    std::cout << "--- obs2破棄後の通知 ---\n";
    subject.notify(3);

    return 0;
}
実行結果
通知を受け取りました: 1
通知を受け取りました: 1
通知を受け取りました: 2
--- obs2破棄後の通知 ---
通知を受け取りました: 1
無効なオブザーバーを削除します。

キャッシュ機構(Weaked Cache)

頻繁に使用するデータをメモリ上に保持しつつ、システム全体でそのデータが不要になったら自動的にキャッシュからも消えてほしい場合に有効です。

weak_ptrをキャッシュに格納しておけば、キャッシュ自体がオブジェクトの寿命を延ばすことはありません。

パフォーマンスへの影響と注意点

lock()は非常に便利な機能ですが、内部的にはアトミックな参照カウンタの操作を行っています。

そのため、極端にパフォーマンスが要求されるループの最深部などで無闇に呼び出すのは避けるべきです。

オーバーヘッドの正体

  1. コントロールブロックへのアクセス: shared_ptrweak_ptrが共有している管理領域を見に行きます。
  2. アトミックインクリメント: 参照カウンタを安全に増やすため、CPUレベルでのロックや同期が発生します。

とはいえ、通常のアプリケーション開発においては無視できるレベルの負荷です。

安全性を犠牲にして生ポインタを使うよりも、lock()による安全なアクセスを選択するのが現在のプログラミングにおける鉄則です。

注意:lock()したshared_ptrを長時間保持しない

lock()で取得したshared_ptrを、クラスのメンバ変数などに保存して長時間保持し続けると、本来weak_ptrを使う目的であった「寿命の分離」が損なわれてしまいます。

lock()したポインタは、必要な関数内でのみローカル変数として使い、速やかに破棄されるように設計しましょう。

shared_from_thisとの関係

クラス内部で自分のweak_ptrを管理し、shared_ptrとして自身を返したい場合には、std::enable_shared_from_thisを継承します。

この内部でもweak_ptrが使われており、shared_from_this()を呼ぶことは、内部的なweak_ptrに対してlock()を行っているのと同義です。

非同期処理のコールバックにthisを渡したい場合などは、生ポインタの代わりにweak_from_this()をキャプチャし、実行時にlock()することで、自身がすでに破棄されている場合のクラッシュを確実に防ぐことができます。

まとめ

std::weak_ptr::lockは、不確実なオブジェクトの生存状態を、確実なアクセス権へと変換するための架け橋です。

マルチスレッド環境における安全性を確保しつつ、循環参照というC++の古典的な問題を解決するために欠かせない機能です。

今回のポイントを整理すると以下の通りです。

  • weak_ptrのままではデータにアクセスできず、lock()shared_ptr化する必要がある。
  • lock()はオブジェクトが消えていればnullptrを返すため、if文でのチェックが必須。
  • expired()よりもlock()の方が、スレッドセーフな生存確認として適している。
  • オブザーバーパターンやキャッシュなど、寿命を強制したくないシーンで真価を発揮する。

正しいスマートポインタの知識を身につけ、メモリ管理に悩まされない堅牢なC++コードを記述していきましょう。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!