進捗置き場というわけでもない場所

プログラミングしてる中で見つけたこととか

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();
    }
}

解決方法

あまりに不可解だったので、TwitterSiv3Dの作者さん にお聞きしたところ「アライメント関連の問題だと思われるので 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が用意される予定だそうです。