[C++]std::anyとは異なる、動的メモリ確保を行わない動的型を作る。

更新情報

2021年3月18日
色々と設計に問題があったので全面的に作り直した。

動機

std::anyはどのような型のインスタンスでも格納でき、かつデストラクタを呼ぶ必要のない便利な機能だ。ただしインスタンスを格納するメモリは動的に確保される場合があり、しかも実装によって動的メモリ確保を回避できる最大サイズが異なっている。私が確認した限り、64bitのMSVCだと56または48バイトで、GCCのlibstdc++だとおそらく8バイト、Clangのlibc++は24バイトだと思われる。このあたりの詳しい説明はこちらの記事を参照されたい

しかし実装によって動的メモリ確保が起きたり起きなかったりするとか、最大サイズを指定できないなどの仕様は鬱陶しい。出来ることなら、例えば動的メモリ確保を起こさない最大サイズを明示的に指定できたり、動的メモリ確保が起きる大きさのインスタンスの格納は禁止したりできるとより便利だ。
もしそれが実現するのなら、例えば派生クラスのインスタンスを静的なストレージに格納しておいて、基底クラスにキャストしつつ使う、といった方法も可能だ。通常は派生クラスを基底クラスのポインタなどに格納する場合は動的メモリ確保が必要になるが、このコストを帳消しにできるのは大きい。

というわけでちょっと作ってみた。

template <size_t StrgSize, bool AllowBigObj>
class Any_impl
{
    template <class T>
    static constexpr bool IsAny()
    {
        return std::is_same_v<std::decay_t<T>, Any_impl>;
    }

    template <class T>
    static constexpr bool IsSmall()
    {
        return
            sizeof(T) <= StrgSize &&
            std::is_nothrow_move_constructible_v<T>;
    }

    struct RTFuncs
    {
        template <class T, bool Small>
        static void Copy(Any_impl& to, const Any_impl& from)
        {
            if constexpr (Small)
            {
                if constexpr (!std::is_copy_constructible_v<T>) throw std::exception("T is not copy constructible");
                else
                {
                    new (&to.mStorage.mSmall.mData) T(from.Get_unsafe<T>());
                }
            }
            else if constexpr (AllowBigObj)
            {
                if constexpr (!std::is_copy_constructible_v<T>) throw std::exception("T is not copy constructible");
                else to.mStorage.mBig.mPtr = new T(from.Get_unsafe<T>());
            }
            else
                static_assert([]() { return false; }(), "size of the template argument is greater than Any's storage.");
        }
        template <class T, bool Small>
        static void Move(Any_impl& to, Any_impl&& from) noexcept
        {
            if constexpr (Small)
            {
                auto& t = *std::launder(reinterpret_cast<T&>(to.mStorage.mSmall.mData));
                auto& f = *std::launder(reinterpret_cast<T&>(from.mStorage.mSmall.mData));
                new (&t) T(std::move(f));
                f.~T();
            }
            else if constexpr (AllowBigObj)
            {
                to.mStorage.mBig.mPtr = from.mStorage.mBig.mPtr;
                from.mStorage.mBig.mPtr = nullptr;
            }
            else
                static_assert([]() { return false; }(), "size of the template argument is greater than Any's storage.");
        }
        template <class T, bool Small>
        static void Destroy(Any_impl& to) noexcept
        {
            if constexpr (Small)
            {
                auto& t = *std::launder(reinterpret_cast<T&>(to.mStorage.mSmall.mData));
                t.~T();
                to.mStorage.mSmall = SmallStorage{};
            }
            else if constexpr (AllowBigObj)
            {
                auto* t = *std::launder(reinterpret_cast<T*>(to.mStorage.mBig.mPtr));
                delete t;
                to.mStorage.mBig = BigStorage{};
            }
            else
                static_assert([]() { return false; }(), "size of the template argument is greater than Any's storage.");
        }
        template <class T>
        static const std::type_info& TypeInfo()
        {
            return typeid(T);
        }
        using CopyF = void(*)(Any_impl&, const Any_impl&);
        using MoveF = void(*)(Any_impl&, Any_impl&&);
        using DestroyF = void(*)(Any_impl&);
        using TypeInfoF = const std::type_info& (*)(void);

        CopyF mCopy = nullptr;
        MoveF mMove = nullptr;
        DestroyF mDestroy = nullptr;
        TypeInfoF mTypeInfo = nullptr;
    };
    template <class T, bool Small = IsSmall<T>()>
    inline static constexpr RTFuncs RTFuncs_value =
    { &RTFuncs::template Copy<T, Small>, &RTFuncs::template Move<T, Small>,
      &RTFuncs::template Destroy<T, Small>, &RTFuncs::template TypeInfo<T> };

public:

    struct NullType {};

    Any_impl() : mRTFuncs(nullptr) {}
    template <class T, std::enable_if_t<!std::is_same_v<std::decay_t<T>, Any_impl>, std::nullptr_t> = nullptr>
    Any_impl(T&& v)
        : mRTFuncs(nullptr)
    {
        Emplace_impl<std::decay_t<T>>(std::forward<T>(v));
    }
    Any_impl(const Any_impl& a)
        : mRTFuncs(nullptr)
    {
        if (!a.IsEmpty()) Copy(a);
    }
    Any_impl(Any_impl&& a) noexcept
        : mRTFuncs(nullptr)
    {
        if (!a.IsEmpty()) Move(std::move(a));
    }

    template <class T, std::enable_if_t<!std::is_same_v<std::decay_t<T>, Any_impl>, std::nullptr_t> = nullptr>
    Any_impl& operator=(T&& v)
    {
        Emplace<std::decay_t<T>>(std::forward<T>(v));
        return *this;
    }

    Any_impl& operator=(const Any_impl& a)
    {
        if (!IsEmpty()) Destroy();
        if (!a.IsEmpty()) Copy(a);
        return *this;
    }
    Any_impl& operator=(Any_impl&& a) noexcept
    {
        if (!IsEmpty()) Destroy();
        if (!a.IsEmpty()) Move(std::move(a));
        return *this;
    }
    ~Any_impl()
    {
        if (!IsEmpty()) Destroy();
    }

    template <class T, class ...Args>
    void Emplace(Args&& ...args)
    {
        if (!IsEmpty()) Destroy();
        Emplace_impl<T>(std::forward<Args>(args)...);
    }

    bool IsEmpty() const { return mRTFuncs == nullptr; }
    operator bool() const { return !IsEmpty(); }
    template <class T>
    bool Is() const
    {
        return !IsEmpty() && typeid(T) == TypeInfo();
    }
    const std::type_info& GetType() const
    {
        if (IsEmpty()) return typeid(NullType);
        return TypeInfo();
    }

    template <class T>
    const T& Get() const&
    {
        if (!Is<T>()) throw std::exception("bad cast of Any");
        return Get_unsafe<T>();
    }
    template <class T>
    T& Get()&
    {
        if (!Is<T>()) throw std::exception("bad cast of Any");
        return Get_unsafe<T>();
    }
    template <class T>
    T&& Get()&&
    {
        if (!Is<T>()) throw std::exception("bad cast of Any");
        return Get_unsafe<T>();
    }

    template <class T>
    const T& Get_unsafe() const&
    {
        assert(!IsEmpty());
        if constexpr (IsSmall<T>())
            return *std::launder(reinterpret_cast<const T*>(&mStorage.mSmall.mData));
        else if constexpr (AllowBigObj)
            return *std::launder(reinterpret_cast<const T*>(mStorage.mBig.mPtr));
        else
            static_assert([]() { return false; }(), "size of the template argument is greater than Any's storage.");
    }
    template <class T>
    T& Get_unsafe()&
    {
        assert(!IsEmpty());
        if constexpr (IsSmall<T>())
            return *std::launder(reinterpret_cast<T*>(&mStorage.mSmall.mData));
        else if constexpr (AllowBigObj)
            return *std::launder(reinterpret_cast<T*>(mStorage.mBig.mPtr));
        else
            static_assert([]() { return false; }(), "size of the template argument is greater than Any's storage.");
    }
    template <class T>
    T&& Get_unsafe()&&
    {
        assert(!IsEmpty());
        if constexpr (IsSmall<T>())
            return std::move(*reinterpret_cast<T*>(&mStorage.mSmall.mData));
        else if constexpr (AllowBigObj)
            return std::move(*reinterpret_cast<T*>(mStorage.mBig.mPtr));
        else
            static_assert([]() { return false; }(), "size of the template argument is greater than Any's storage.");
    }

    template <class T, bool B = AllowBigObj, std::enable_if_t<!B, std::nullptr_t> = nullptr>
    T& Cast()&
    {
        return *std::launder(reinterpret_cast<T&>(mStorage.mSmall.mData));
    }
    template <class T, bool B = AllowBigObj, std::enable_if_t<!B, std::nullptr_t> = nullptr>
    const T& Cast() const&
    {
        return *std::launder(reinterpret_cast<const T&>(mStorage.mSmall.mData));
    }

private:

    void Copy(const Any_impl& from)
    {
        assert(IsEmpty());
        mRTFuncs = from.mRTFuncs;
        mRTFuncs->mCopy(*this, from);
    }
    void Move(Any_impl&& from) noexcept
    {
        assert(IsEmpty());
        mRTFuncs = from.mRTFuncs;
        mRTFuncs->mMove(*this, std::move(from));
        from.mRTFuncs = nullptr;
    }
    void Destroy() noexcept
    {
        assert(!IsEmpty());
        auto* d = mRTFuncs->mDestroy;
        mRTFuncs = nullptr;
        d(*this);
    }
    const std::type_info& TypeInfo() const
    {
        assert(!IsEmpty());
        return mRTFuncs->mTypeInfo();
    }

    //Tはdecayされているものとする。
    template <class T, class ...Args>
    void Emplace_impl(Args&& ...args)
    {
        assert(IsEmpty());
        if constexpr (IsSmall<T>())
        {
            //small
            new (&mStorage.mSmall.mData) T(std::forward<Args>(args)...);
            mRTFuncs = &RTFuncs_value<T>;
        }
        else if constexpr (AllowBigObj)
        {
            //big
            mStorage.mBig.mPtr = new T(std::forward<Args>(args)...);
            mRTFuncs = &RTFuncs_value<T>;
        }
        else
            static_assert([]() { return false; }, "size of the template argument is greater than Any's storage.");
    }

    struct BigStorage
    {
        char Padding[StrgSize - sizeof(void*)];
        void* mPtr;
    };
    struct SmallStorage
    {
        std::aligned_storage_t<StrgSize, alignof(std::max_align_t)> mData;
    };
    template <bool, class = void>
    union Storage
    {
        BigStorage mBig;
        SmallStorage mSmall;
    };
    template <class Dummy>
    union Storage<false, Dummy>
    {
        SmallStorage mSmall;
    };

    Storage<AllowBigObj> mStorage;
    const RTFuncs* mRTFuncs;
};

//Any_implのテンプレート引数にはストレージの大きさ、動的メモリ確保を許すか否かを与える。

//Any_impl<Size, true>とした場合、
// * 代入するオブジェクトがSizeバイト以下かつnothrow move constructibleならAny_impl内のストレージにplacement newで構築される。
// * Sizeバイトより大きいなら、動的メモリ確保を行いそこにオブジェクトを格納する。Any_implはそこへのポインタを保有する。
using Any = Any_impl<24, true>;

//Any_impl<Size, false>とした場合、
// * 代入するオブジェクトがSizeバイト以下かつnothrow move constructibleならAny_impl<Size, true>に同じ。
// * Sizeバイトより大きいオブジェクトは禁止され、格納しようとするとコンパイルエラーになる。
template <size_t Size>
using StaticAny = Any_impl<Size, false>;

ここで定義しているAny_impl<Size, AllowBigObj>は、Size + sizeof(void*)の大きさを持ち、Sizeバイトまでなら動的メモリ確保なしに格納することが出来る(ただしnothrow move constructibleでなければならない)。AllowBigObjをtrueにした場合、Sizeバイトを超えるオブジェクトも動的メモリ確保によって格納することが出来るが、falseを与えた場合はそれも禁止される。

この機能を作った目的の一つは派生クラスの動的メモリ確保を回避することだった。AllowBigObj == falseとした場合に限り、これを可能にするCast関数を用意した。trueの場合は原理的に実現不可能である。

struct Base
{
    virtual void Func() const = 0;
};
struct Derived1 : public Base { virtual void Func() const { std::cout << "Derived1" << std::endl; } };
struct Derived2 : public Base { virtual void Func() const { std::cout << "Derived2" << std::endl; } };

int main()
{
    //32バイト以下のオブジェクトを格納できる。
    StaticAny<32> a = std::vector<double>{ 1, 2, 3, 4, 5 };
    assert(a.Is<std::vector<double>>());
    for (auto d : a.Get<std::vector<double>>()) std::cout << d;
    std::cout << std::endl;
    //33バイト以上のオブジェクトは格納できない。
    //StaticAny<32> b = std::array<double, 5>{ 1, 2, 3, 4, 5 };コンパイルエラー。

    //25バイト以上のオブジェクトは動的メモリ確保を伴って格納される。
    Any b = std::string("abcde");
    assert(b.Is<std::string>());
    std::cout << b.Get<std::string>() << std::endl;

    //std::anyはnon-copyableなオブジェクトを格納できないが、Any_implは可能。
    //コピーしようとすると例外を投げる。
    StaticAny<8> c = std::make_unique<int>(10);
    std::cout << *c.Get<std::unique_ptr<int>>() << std::endl;
    try
    {
        StaticAny<8> d = c;
    }
    catch (const std::exception& e)
    {
        std::cout << e.what() << std::endl;
    }

    //AllowBigObj == falseの場合は、格納されている派生クラスのオブジェクトを基底クラスにキャストする、なんて使い方もできる。
    //動的メモリ確保を行うことなく派生クラスを扱うことも可能。
    //Baseクラスの型情報では格納先がAny_impl内ストレージか動的確保されたメモリかがわからないため、AllowBigObj == trueの場合はこの使い方は許されない。
    StaticAny<8> any;
    any = Derived1();
    any.Cast<Base>().Func();//Castは型チェックなど諸々省くので非常に危険だが、仮想基底クラスなどへもキャストできる。
    any = Derived2();
    any.Cast<Base>().Func();
    return 0;
}

 この程度の機能は誰かがすでに作っているかもしれないと思って探してみたら、案の定GitHubで見つかった。……しかも中身を見てみると基本設計が私のものとほぼ同じだった。わざわざ作らなくても良かったなぁ。いや、大した手間でもなかったし、名前付けの法則などが私と異なっていて気持ち悪いので、自前で作るに越したことはないのだが。