ことの発端
非同期処理を実装していると std::enable_shared_from_this
をよく使う。
でも、std::enable_shared_from_this
から派生したオブジェクトはstd::shared_ptr
で所有するコードに縛るとなると、コードを記述する人の匙加減になってしまうため、これを制限したい。
問題点
下記のようなコードの場合、 BAZ
が std::shared_ptr
で所有されないとクラッシュします。
#include <memory>
class FOO : public std::enable_shared_from_this<FOO> {
int foo_value_;
public:
FOO(int value) : foo_value_(value) {}
void foo() {
auto instance = shared_from_this();
std::cout << instance->foo_value_ << std::endl;
}
};
int main() {
auto foo = std::make_shared<FOO>(123);
foo->foo();
auto foo2 = FOO(456);
foo2.foo(); // <-- crash!
}
とりあえずの解決方法
よくあるのが、コンストラクタをprotected
かprivate
にして、生成関数を作る方法。
#include <memory>
class FOO : public std::enable_shared_from_this<FOO> {
private:
int foo_value_;
protected:
FOO(int value) : foo_value_(value) {}
public:
template<typename ...ARGS> static auto create(ARGS&&...args) {
// std::make_shared が使えないけど、本題はココじゃないから、まぁ、いいか。。。
return std::shared_ptr<FOO>(new FOO(std::forward<ARGS>(args)...));
}
void foo() {
auto instance = shared_from_this();
std::cout << instance->foo_value_ << std::endl;
}
};
int main() {
// コンパイルエラー
// auto foo = std::make_shared<FOO>(123);
// foo->foo();
// コンパイルエラー
// auto foo2 = FOO(456);
// foo2.foo();
auto foo2 = FOO::create(456);
foo2->foo();
}
一見、これでも良いように思えるが、この FOO
を派生すると、話がややこしくなる。
#include <memory>
class FOO : public std::enable_shared_from_this<FOO> {
protected:
int foo_value_;
FOO(int value) : foo_value_(value) {}
public:
template<typename ...ARGS> static auto create(ARGS&&...args) {
return std::shared_ptr<FOO>(new FOO(std::forward<ARGS>(args)...));
}
void foo() {
auto instance = shared_from_this();
std::cout << instance->foo_value_ << std::endl;
}
};
class BAR : public FOO {
int bar_value_;
public:
BAR(int value1, int value2) : FOO(value1), bar_value_(value2) {}
};
int main() {
auto bar = BAR(1, 2);
bar.foo(); // <-- crash!
}
まぁ、BAR
にも生成関数を作れば良いのだけど、もう少し制約を設けて、設計意図を後世に伝えられないかとという課題になります。
制限と生成を一気に解決する
まず、must_be_shared
というクラスを定義します。
このクラスは _magic
とそれを初期化する _magic_seed
という空の構造体がキモとなります。
そして、must_be_shared
の生成には_magic
(これはmagic_t
で再定義され実質protected
です)が必要になりますが、_magic
のコンストラクタ引数の_magic_seed
を生成できるのは must_be_shared
のみです。
#include <memory>
template <typename T> class must_be_shared :
public std::enable_shared_from_this<T>
{
private:
struct _magic_seed {};
struct _magic { _magic(const _magic_seed&) noexcept {} };
protected:
using magic_t = typename must_be_shared<T>::_magic;
must_be_shared(const magic_t&) noexcept {}
public:
virtual ~must_be_shared() = default;
template <typename ...ARGS>
static auto create(ARGS&&...args) {
return std::make_shared<T>(_magic{_magic_seed{}}, std::forward<ARGS>(args)...);
}
template <typename Derived, typename OutType = Derived, typename ...ARGS>
static auto create(ARGS&&...args) {
return std::static_pointer_cast<OutType>(
std::make_shared<Derived>(_magic{_magic_seed{}}, std::forward<ARGS>(args)...)
);
}
template <typename X> auto shared_from_this_as() noexcept {
return std::dynamic_pointer_cast<X>(must_be_shared<T>::shared_from_this());
}
};
使い方
-
std::shared_ptr
をmust_be_shared
に置き換える。 - コンストラクタを
public
に配置する。 - コンストラクタの第1引数に
const magic_t& _
と書く。 - コンストラクタの初期化子に
must_be_shared<XXX>(_)
と書く。(XXX
はクラス名) - 生成関数
create()
にはコンストラクタの第2引数以降を設定する。
コンストラクタを public
に配置することを強要しますが、magic_t
はmust_be_shared
内部でしか生成する手段が提供されていないため、ローカルでコンストラクタを呼び出すことは不可能になります。
サンプル
class FOO : public must_be_shared<FOO> {
private:
int foo_value_;
public:
FOO(const magic_t& _, int foo_value):
must_be_shared<FOO>(_),
foo_value_(foo_value)
{}
void foo() {
auto instance = shared_from_this();
std::cout << instance->foo_value_ << std::endl;
}
};
int main() {
auto foo = FOO::create(1);
}
で、FOO
を派生してBAR
を定義する場合は、下記のようになります。
class BAR : public FOO {
private:
int bar_value_;
public:
BAR(const magic_t& _, int bar_value = 100, int foo_value = 200):
FOO(_, foo_value),
bar_value_(bar_value)
{}
void bar() {
auto instance = shared_from_this_as<BAR>();
std::cout << instance->bar_value_ << std::endl;
}
};
int main() {
// コンパイルエラー
// auto foo = FOO(FOO::magic_t{FOO::_magic_seed{}}, 1);
// 基本的な生成方法
auto foo = FOO::create(1);
foo->foo();
// 派生したクラスを生成する方法
auto bar = BAR::create<BAR>(10, 20);
bar->foo();
bar->bar();
// デフォルトパラメータも使える
auto bar_default = BAR::create<BAR>();
bar_default->foo();
bar_default->bar();
// ベースクラスで管理したい場合の生成方法
auto bar_as_foo = BAR::create<BAR, FOO>(10, 20);
bar_as_foo->foo();
// bar_as_foo->bar(); // <-- compile error
bar_as_foo->shared_from_this_as<BAR>()->bar();
}
// foo -> 1
// foo -> 20
// bar -> 10
// foo -> 200
// bar -> 100
// foo -> 20
// bar -> 10
所感
コンストラクタにお約束事が出てきますが、毎回create
みたいな生成関数を作成する必要がなくなるし、設計意図を後世に伝えるという意味でも、良いかも?
ただ、下記のコードが許される。
auto bar = BAR::create(1);
この場合、std::shared_ptr<FOO>
が出来上がってしまう。
なので、template <typename ...ARGS> static auto create(ARGS&&...args)
は削除して明示的に生成クラスを指定した方がバグが少なくなるかも知れない。
auto foo = FOO::create<FOO>(1);
auto bar = BAR::create<BAR>(1);