♻️

enable_shared_from_this の使い方メモ

2020/09/26に公開

https://cpprefjp.github.io/reference/memory/enable_shared_from_this.html
https://ja.cppreference.com/w/cpp/memory/enable_shared_from_this

enable_shared_from_thisshared_ptr で管理したいクラスのベースにするやつだが、これをそのまま使うのは非常に危険が危ない。

危ない理由一覧

  • 絶対shared_ptr管理するためにファクトリ関数以外から new とかされたくない
  • コピーとかもされたくない
  • 多重継承したときにやばい
  • etc...
  • C++17で多少マシにはなったけどまだまだ危険が危ない

参考資料

なので 絶対shared_ptrで管理させるマン なるいい感じのやつ作ってみた。

shared_base.hpp

shared_base.hpp
#pragma once
#include <memory>
#include <type_traits>

namespace util {
// SFINAE用
template <bool val>
using en_if = typename std::enable_if<val, std::nullptr_t>::type;

// メンバポインタのshared_ptrを取得する
template <class T, class U, class M, en_if<std::is_base_of<U, T>::value> = nullptr>
auto mem_ptr(const std::shared_ptr<T> &p, M U::*pm)
    -> std::shared_ptr<typename std::remove_reference<decltype(p.get()->*pm)>::type>
{
    return p == nullptr || pm == nullptr ? nullptr
        : decltype(mem_ptr(p, pm))(p, &(p.get()->*pm));
}

// 内部クラス
// これを直接継承しない
class virtual_shared_base: public std::enable_shared_from_this<virtual_shared_base>
{
protected:
    // 外部でインスタンス化されないようコンストラクタは全部protected
    // 派生先でもprotectedにしておくこと
    // ※ make_sharedで使うからprivateにはしない
    virtual_shared_base() = default;

public:
    // 全部shared_ptr管理したいのでコピーもムーブも禁止
    // 派生先でも定義しないこと
    virtual_shared_base(const virtual_shared_base&) = delete;    
    virtual_shared_base(virtual_shared_base&&) = delete;
    virtual_shared_base& operator = (const virtual_shared_base&) = delete;
    virtual_shared_base& operator = (virtual_shared_base&&) = delete;
    virtual ~virtual_shared_base() = default;

private:
    // SFINAE用
    template <class T>
    using base_check = en_if<std::is_base_of<virtual_shared_base, T>::value>;

protected:
    // std::make_sharedの代わり
    // 派生先のファクトリ関数から使う用
    // ※ publicコンストラクタがないとstd::make_sharedが使えないため
    template <class T, class ...Arg, base_check<T> = nullptr>
    static std::shared_ptr<T> make_shared(Arg &&...arg) {
        struct Dummy: T {
            Dummy(Arg &&...arg): T(std::forward<Arg>(arg)...) { }
        };
        return std::make_shared<Dummy>(std::forward<Arg>(arg)...);
    }

    // shared_from_this()の代わり
    // ダウンキャストして自身の型のshared_ptrを取る
    // get_shared(this) の様に使う
    template <class T, base_check<T> = nullptr>
    static std::shared_ptr<T> get_shared(T *self) {
        return self == nullptr ? nullptr
            : std::shared_ptr<T>(self->virtual_shared_base::shared_from_this(), self);
    }

    // メンバポインタの取得
    // mem_ptr(this, &TypeName::memberName) の様に使う
    template <class T, class U, class M, base_check<T> = nullptr>
    static auto mem_ptr(T *self, M U::*pm)
        -> decltype(util::mem_ptr(get_shared(self), pm))
    {
        return util::mem_ptr(get_shared(self), pm);
    }
};

// enable_sahred_from_thisをいい感じにラップしたやつ
// enable_sahred_from_thisの代わりにこれを継承する
template <class T>
class shared_base: virtual public virtual_shared_base
{
protected:
    shared_base() = default;

public:
    std::shared_ptr<T> shared_from_this() {
        return get_shared(static_cast<T*>(this));
    }
    std::shared_ptr<const T> shared_from_this() const {
        return get_shared(static_cast<const T*>(this));
    }
};
} // namespace util

使い方

  • enable_sahred_from_this と同じように shared_basepublic 継承する
  • コンストラクタを必ず protected にする
  • 静的メンバ関数とかフレンド関数とかのファクトリメソッドから make_shared 使って生成する
  • thisポインタは get_shared(this), メンバのポインタは mem_ptr(this, &T::mem) で取るようにして絶対に生のポインタを外に出さないこと
sample.cpp
#include <iostream>
#include <string>
#include <functional>
#include "shared_base"

class Base1: public util::shared_base<Base1>
{
public:
    int val;

    // ファクトリ関数
    static std::shared_ptr<Base1> Create(int val) {
        return make_shared<Base1>(val);
    }

protected:
    // コンストラクタはprotectedで
    Base1(int v): val(v) { }
};

class Base2: public util::shared_base<Base2>
{
// コンストラクタは必ずprotectedにすること
protected:
    Base2() = default;
};

// 多重継承してもOK
class C: public Base1, public Base2 {
public:
    std::string str;

    static std::shared_ptr<C> Create(int v, std::string s) {
        return make_shared<C>(v, s);
    }

protected:
    C(int v, std::string s): Base1(v), str(s) { }

public:
    // メンバポインタ取得
    std::shared_ptr<int> get_val_ref() {
        // &(this->val) としたくなるが絶対やらないこと
        return mem_ptr(this, &C::val);
    }

    std::function<std::shared_ptr<int>()> get_func() {
        // 絶対に生のthisポインタをbindしないこと
        return std::bind(&C::get_val_ref, get_shared(this));
    }

    std::function<std::string()> get_str_func() const {
        // 絶対にthisキャプチャしないこと
        return [self = get_shared(this)]() { return self->str; };
    }
};

int main()
{
    using namespace std;

    shared_ptr<Base1> base1 = Base1::Create(1);
    shared_ptr<C> c = C::Create(4, "hoge");
    {
        shared_ptr<const C> cc = c;
        shared_ptr<int> v = c->get_val_ref();

        // &(cc->str) みたいに生のポインタを取らないこと
        shared_ptr<const string> cstr = util::mem_ptr(cc, &C::str);

        *v += 1;

        cout << base1->val << endl; // 1
        cout << *v << endl; // 5
        cout << cc->val << endl; // 5
        cout << *cstr << endl; // hoge
        cout << c.use_count() << endl; // 4
        
        auto f = cc->get_str_func();
        cout << f() << endl; // hoge
        cout << c.use_count() << endl; // 5
    }
    cout << c.use_count() << endl; // 1
    base1 = c;
    cout << base1->val << endl; // 5
    cout << c.use_count() << endl; // 2
    cout << *c->get_func()() << endl; // 5
    cout << c.use_count() << endl; // 2

    // publicなコンストラクタは無いはずなので実体化できない
    // Base2 _b2;
    // C _c(0, "");
    
    // コピーもムーブもさせない
    // C _tmp = *c;
    // C _tmp2 = std::move(*c);
    
    // 参照は取れる
    C &tmp = *c;
    C &&tmp2 = std::move(*c);
}

Discussion