Help us understand the problem. What is going on with this article?

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

動機

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できるようにしています(こちらを参考にさせていただきました)

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした