LoginSignup
4
0

More than 3 years have passed since last update.

勝手にnewされないことを保証するクラスの実装

Last updated at Posted at 2020-02-23

動機

C++でプログラムを書いていると、たまに自身のポインタを生のthisポインタではなく、shared_ptrで使いたいと言うことがあります。こういったとき普通はstd::enable_shared_from_thisを継承してshared_from_this()すればいいのですが、この方法だと「そのクラスがshared_ptrで管理」されていない、つまりnewで初期化されていると未定義動作を起こしてしまう(2020/2/25訂正 C++17以降では未定義動作ではなく、例外が投げられるそうです @yuki12 さんご指摘ありがとうございます)ので、知らず知らずのうちに危険なプログラムを書いてしまう可能性があります。
そこで、「派生クラスが勝手にnewされない」ことを保証するクラスを実装してみました。

訂正(2020/2/23)

よくよく考えればnewさえされなければいいのでcreateメンバ関数内のチェック処理は入りませんでした 以降その部分は気にしないでください

実装

#include <memory>
#include <type_traits>


template <typename T> 
class SharedFromThis
{
    struct CreateHelper:T
    {
        template<typename... Args>
        CreateHelper(Args&&... args):T(std::forward<Args>(args)...){}
    };
    public:
    template<typename... Args>
    void* operator new(size_t,Args&&...)
    {
        //delay evaluation until instantiation
        constexpr bool operator_new_is_not_used = []{return false;}();
        static_assert(operator_new_is_not_used,"Class that inherits from SharedFromThis cannot be instantiated with operator new.");
    }

    template<typename... Args> static std::shared_ptr<T> create(Args&&... args)
    {
        constexpr bool is_constructor_is_callable = std::is_constructible<T,Args&&...>::value;
        static_assert(
            !is_constructor_is_callable,
            "If the class inherits from SharedFromThis,the class of constructor must be protected."
        );

        auto shared_this = std::make_shared<CreateHelper>(std::forward<Args>(args)...);
        return shared_this;
    }
};

こんな感じです。
operator newをテンプレート関数でオーバーライドし、実体化の際(つまり、派生クラスがnewされた時)にstatic_assertでコンパイル時にエラーを出すようにしています。
また、createと言うstaticメンバ関数内でstd::is_constructible<T,Args&&...>::value
として派生クラスがpublicなコンストラクタの存在を調べ、勝手に初期化されては困るので存在した場合はコンパイル時エラーを出すようにしています。

用例

#include <memory>
#include <type_traits>


template <typename T> 
class SharedFromThis
{
    struct CreateHelper:T
    {
        template<typename... Args>
        CreateHelper(Args&&... args):T(std::forward<Args>(args)...){}
    };
    protected:
    std::shared_ptr<T> shared_this;
    public:
    template<typename... Args>
    void* operator new(size_t,Args&&... args)
    {
        constexpr bool operator_new_is_not_used = []{return false;}();
        static_assert(operator_new_is_not_used,"Class that inherits from SharedFromThis cannot be instantiated with operator new.");
    }
    template<typename... Args> static std::shared_ptr<T> create(Args&&... args)
    {

        constexpr bool is_constructor_is_callable = std::is_constructible<T,Args&&...>::value;
        static_assert(
            !is_constructor_is_callable,
            "If the class inherits from SharedFromThis,the class of constructor must be protected."
        );

        auto shared_this = std::make_shared<CreateHelper>(std::forward<Args>(args)...);

        return shared_this;
    }
};



class D:public SharedFromThis<D>{
protected:
D(int){}
D(float){}
};

class DD : public D,public SharedFromThis<DD>
{
    public:
    using SharedFromThis<DD>::create;
    using SharedFromThis<DD>::operator new;    
    protected:
    DD(int num):D(num){}
};

class DDD:public DD,public SharedFromThis<DDD>
{
    public:
    using SharedFromThis<DDD>::create;
    using SharedFromThis<DDD>::operator new;
    public:
    DDD(int num):DD(num){}
};

int main()
{
    auto d = D::create(10);
    auto dd = DD::create(10);
    //auto dd_error = new DD(10); newしたらコンパイルエラー
    //auto ddd = DDD::create(10); DDDはpublicなコンストラクタを持っているのでコンパイルエラー
}

こんな感じですね。危険な操作を未然に防げます。

問題点

用例でわかるように、この実装だと「SharedFromThisを継承しているクラスとSharedFromThisを多重継承したクラス」とかを作ろうとすると名前解決のためにいちいちusing宣言をしないと行けないです。(解決策があったら誰か教えてください...)

小技

今回の実装に使ったちょっとむつかしそうなC++の小技の解説です。
まず、

template<typename... Args>
    void* operator new(size_t,Args&&...)
    {
        //delay evaluation until instantiation
        constexpr bool operator_new_is_not_used = []{return false;}();
        static_assert(operator_new_is_not_used,"Class that inherits from SharedFromThis cannot be instantiated with operator new.");
    }

このコードですが、[]{return false;}();からfalseを得ると言う周りくどいことをすることでテンプレート関数の実体化までoperator_new_is_not_usedの評価を遅らせ、実際にこの関数が使用された時のみコンパイルエラーを出すようにしています(こちらを参考にさせていただきました)
そして、

template<typename... Args> static std::shared_ptr<T> create(Args&&... args)
    {

        constexpr bool is_constructor_is_callable = std::is_constructible<T,Args&&...>::value;
        static_assert(
            !is_constructor_is_callable,
            "If the class inherits from SharedFromThis,the class of constructor must be protected."
        );

        auto shared_this = std::make_shared<CreateHelper>(std::forward<Args>(args)...);

        return shared_this;
    }

このコードでは、std::is_constructibleと言うメタ関数によって、T型のコンストラクタを与えられたArgs型の引数で呼び出せるかをチェックしています。
また、CreateHelperと言うTをpublic継承したクラス内構造体を経由することで、Tがprotectedなコンストラクタを持っていてもmake_sharedできるようにしています(こちらを参考にさせていただきました)

4
0
4

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
4
0