Siv3Dにおいて一部ビルドでアライメントの関連のランタイムエラーが出たときの備忘録
Siv3DでQuaternionを扱う際にハマった場所があったので備忘録。
問題
問題は「Siv3DにおいてQuaternionなどをメンバに持つ派生クラス
を基底型のポインタ
に突っ込むとx86ビルドで死ぬ場合がある」というものです(自分はx86、Releaseビルドでのみ発生しました)。
具体的には以下の様な例です(このコードが死ぬ保証はありません)。
#include <Siv3D.hpp> #include <memory> class A { public: virtual void draw() const = 0; }; class B : public A { public: void draw() const override { Plane(5.0, q1 * q2).draw(); // ここで死 } private: Quaternion q1 = Quaternion::Yaw(45.0_deg), q2 = Quaternion::Roll(30.0_deg); Plane p = Plane(10); }; void Main() { std::shared_ptr<A> ptr; ptr = std::make_shared<B>(); while (System::Update()) { ptr->draw(); } }
解決方法
あまりに不可解だったので、Twitterで Siv3Dの作者さん にお聞きしたところ「アライメント関連の問題だと思われるので class B に alignas(16) を付けてみて欲しい」という回答をいただき、やってみたのですが、上手く動きませんでした。
しかし、その後std::make_shared
がアライメント指定されたメモリ確保に未対応なのではと思いあたり、アロケータを自作することにしました。
ここでは、Siv3Dにはアロケータを考慮したmallocであるAlignedMalloc
関数が用意されていることを教えていただき、それを使うことで多分結構楽出来ました。
というわけで以下の様な関数を作ることで解決することが出来ました。
template <typename T, typename... Args> auto alignsafeShared(Args&&... args) { auto* p = AlignedMalloc<T>(1); if (!p) throw std::bad_alloc(); new(p) T(std::forward<Args>(args)...); return std::shared_ptr<T>(p, AlignedFree); }
(ちなみにnew(p) T
の部分を*p = T()
にしたら動かなかったので、理由が分かる方は教えて頂けると幸いです)
追記: Twitterで「Mallocだけでは仮想関数テーブルが生成されておらず、operator =
の呼び出しで不正なメモリを参照してしまうから」と教えていただきました。ありがとうございます。
というわけで、上記の例にこれを適応させた最終結果がこうなります。
#include <Siv3D.hpp> #include <memory> class A { public: virtual void draw() const = 0; }; class alignas(16) B : public A { public: void draw() const override { Plane(5.0, q1 * q2).draw(); } private: Quaternion q1 = Quaternin::Yaw(45.0_deg), q2 = Quaternion::Roll(30.0_deg); Plane p = Plane(10); } template <typename T, typename... Args> auto alignsafeShared(Args&&... args) { auto* p = AlignedMalloc<T>(1); if (!p) throw std::bad_alloc(); new(p) T(std::forward<Args>(args)...); return std::shared_ptr<T>(p, AlignedFree); } void Main() { std::shared_ptr<A> ptr; ptr = alignsafeShared<B>(); while (System::Update()) { ptr.draw(); } }
ちなみに、現在開発中の OpenSiv3D にはこのalignsafeShared
と大体同じ機能を持つMakeShared
と、その生ポインタ版であるAlignedNew
が用意される予定だそうです。