概要
- std::weak_ptr を介してオブジェクトを利用する際は lock() する必要がある
- lock() の返り値は std::shared_ptr なので、これをコピーしてどこかに握らせてしまうと、想定外の参照カウント増加を招きかねない
- lock() したその場のスコープでのみ利用可能なハンドルを作りたい
std::weak_ptr の正しい使い方
いつもお世話になっている、cpprefjp にバッチリ書いてあります。
https://cpprefjp.github.io/reference/memory/weak_ptr.html
std::shared_ptr<int> sp(new int(42));
std::weak_ptr<int> wp = sp;
if (std::shared_ptr<int> r = wp.lock()) {
std::cout << "get weak_ptr value : " << *r << std::endl;
}
みんながこうやって使ってくれるならこれで十分なんですが……
if (std::shared_ptr<int> r = wp.lock()) {
// やめろー!
HogeSingleton::GetInstance()->SetReference(r);
// うわあー!
HugaSingleton::GetInstance()->SetReference(r);
}
こうされると台無しです。強参照があちこちに散逸します。どうすれば防げるでしょうか?
ハンドル型を作ろう
こんな型があれば、いいかもしれません。
- 直接 std::shared_ptr を触らせない
- コピーを禁止
- メンバとして保持できない(できにくくする)
どうすれば実現できるでしょうか。
とりあえず std::shared_ptr をラップして、*
と->
演算子を定義して、コピーコンストラクタを潰せば最初の 2 点はクリアです。
メンバとして保持できなくする完全な方法はありませんが、カジュアルにできなくする方法はあります。ラムダ式の型を型引数に持たせるというものです。ラムダ式の型は decltype で取得するくらいしかコード上で記述する方法がないので、考え無しにメンバに持たせてしまう事故は大幅に減らせるでしょう。
template<typename T>
class weak_handle;
template <typename T, typename F>
class lock_handle {
friend class weak_handle<T>;
public:
constexpr lock_handle() noexcept = delete;
lock_handle(const lock_handle& r) noexcept = delete;
lock_handle& operator=(const lock_handle& r) noexcept = delete;
lock_handle(lock_handle&& r) noexcept = default;
lock_handle& operator=(lock_handle&& r) noexcept = default;
~lock_handle() = default;
T& operator*() const noexcept { return *m_sp; }
T* operator->() const noexcept { return m_sp.get(); }
//T& operator[](ptrdiff_t i) const { return m_sp[i]; }
explicit operator bool() const noexcept { return !!m_sp; }
private:
lock_handle(const F& func) noexcept : m_sp(func()) {}
std::shared_ptr<T> m_sp;
};
std::weak_ptr のかわりに weak_handle という型を使うことにし、weak_handle::lock() が返す値の型として、この lock_handle を定義してみました。
規定のコンストラクタとコピーを封じて、ムーブのみを許可します。アクセスは *
と ->
で行い、有効値を持つか否かは bool キャストオペレータで判定可能にします。
private に隔離したコンストラクタでは std::shared_ptr を返すファンクタを引数として、この型を型引数として露出させることで、この型の値は auto で受ける以外にはなさそうな空気を醸し出します。
(operator[] がコメントアウトしてありますが、色々ゴニョゴニョすれば T が配列型の時のみ定義されるようにできます。今回の本題ではないので省略します。)
template <typename T>
class weak_handle {
public:
constexpr weak_handle() noexcept = default;
weak_handle(const weak_handle& r) noexcept = default;
weak_handle& operator=(const weak_handle& r) noexcept = default;
weak_handle(weak_handle&& r) noexcept = default;
weak_handle& operator=(weak_handle&& r) noexcept = default;
template <class U>
weak_handle(const weak_handle<U>& r) noexcept : m_wp(r.m_wp) {}
template <class U>
weak_handle& operator=(const weak_handle<U>& r) noexcept {
m_wp = r.m_wp;
return *this;
}
template <class U>
weak_handle(weak_handle<U>&& r) noexcept : m_wp(std::move(r.m_wp)) {}
template <class U>
weak_handle& operator=(weak_handle<U>&& r) noexcept {
m_wp = std::move(r.m_wp);
return *this;
}
template <class U>
weak_handle(const std::shared_ptr<U>& r) noexcept : m_wp(r) {}
template <class U>
weak_handle& operator=(const std::shared_ptr<U>& r) noexcept {
m_wp = r;
return *this;
}
~weak_handle() = default;
void swap(weak_handle& r) noexcept { m_wp.swap(r.m_wp); }
void reset() noexcept { m_wp.reset(); }
long use_count() const noexcept { return m_wp.use_count(); }
bool expired() const noexcept { return m_wp.expired(); }
decltype(auto) lock() noexcept {
auto func = [this]() { return m_wp.lock(); };
return lock_handle<T, decltype(func)>(func);
}
private:
std::weak_ptr<T> m_wp;
};
weak_handle は、ほとんど std::weak_ptr と同じ API 構成でラップしているだけです。
重要なのはもちろん lock() です。この中に定義されているラムダ式をコンストラクタの引数として、生成した lock_handle を返します。
使い方
std::shared_ptr<int> sp(new int(42));
weak_handle<int> wh = sp;
if (auto r = wh.lock()) {
std::cout << "get weak_ptr value : " << *r << std::endl;
}
lock() の返り値を auto で受ける以外は std::weak_ptr とほぼ同じ使い勝手で、かつ危険な操作は簡単にはできなくなっていると思います。悪意を持ってこれを引っぺがせるような人は、恐らくスマートポインタの使い方を間違えるようなことはないでしょう、たぶん。