更新情報
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();
}
template <class T, class ...Args>
void Emplace_impl(Args&& ...args)
{
assert(IsEmpty());
if constexpr (IsSmall<T>())
{
new (&mStorage.mSmall.mData) T(std::forward<Args>(args)...);
mRTFuncs = &RTFuncs_value<T>;
}
else if constexpr (AllowBigObj)
{
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;
};
using Any = Any_impl<24, true>;
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()
{
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;
Any b = std::string("abcde");
assert(b.Is<std::string>());
std::cout << b.Get<std::string>() << std::endl;
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;
}
StaticAny<8> any;
any = Derived1();
any.Cast<Base>().Func();
any = Derived2();
any.Cast<Base>().Func();
return 0;
}
この程度の機能は誰かがすでに作っているかもしれないと思って探してみたら、案の定GitHubで見つかった。……しかも中身を見てみると基本設計が私のものとほぼ同じだった。わざわざ作らなくても良かったなぁ。いや、大した手間でもなかったし、名前付けの法則などが私と異なっていて気持ち悪いので、自前で作るに越したことはないのだが。