LoginSignup
0
1

More than 1 year has passed since last update.

enable_shared_from_this から派生したクラスを確実に shared_ptr で生成する

Last updated at Posted at 2023-04-18

ことの発端

非同期処理を実装していると std::enable_shared_from_this をよく使う。
でも、std::enable_shared_from_thisから派生したオブジェクトはstd::shared_ptrで所有するコードに縛るとなると、コードを記述する人の匙加減になってしまうため、これを制限したい。

問題点

下記のようなコードの場合、 BAZstd::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!
}

とりあえずの解決方法

よくあるのが、コンストラクタをprotectedprivateにして、生成関数を作る方法。

#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 のみです。

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());
  }
};

使い方

  1. std::shared_ptrmust_be_shared に置き換える。
  2. コンストラクタをpublicに配置する。
  3. コンストラクタの第1引数に const magic_t& _ と書く。
  4. コンストラクタの初期化子に must_be_shared<XXX>(_) と書く。(XXXはクラス名)
  5. 生成関数create()にはコンストラクタの第2引数以降を設定する。

コンストラクタを public に配置することを強要しますが、magic_tmust_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);

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1