読者です 読者をやめる 読者になる 読者になる

bitfield templateでハマった件

C++ではテンプレートを使ってビットフィールドを実現するというテクニックがあるようです(検索すると色々出てきます)。しかしこれは注意して実装しないとハマるであろう落とし穴があった(私もハマりました)のでメモしておきます(gcc 4.9.2, clang 3.5.0で検証しました)。

私もC++は詳しくないので最初から順を追って書いていきます。まず、C/C++には言語機能としてのビットフィールドがありますが:

union Regs{
    uint8_t raw;
    struct{
        uint8_t a : 2;
        uint8_t b : 3;
        uint8_t c : 3;
    } r;
};

これを使った場合、実際のビットのレイアウトは処理系定義となるようです。よってビットレイアウトが問題になる場合(その方が多いと思う)、この方法は事実上使えません。

自力でビット演算すればいいじゃないか、というのは全くその通りで、例えば16bit値のbit2-4とbit8-12をクリアしたい、という場合は

value &= 0xE0E3;

のようにすればいいわけですが、毎回こんなことをするのは頭のいい人でないと辛いかなと思います。私は頭が悪いのでこんなことしてたらハゲちゃいます^^;

そこで「ビットフィールドを自前で実装できないか」ということになります。これはC++ではテンプレートを使うとわりと短く書けて、実装も一見簡単です:

// 実は問題がある!!
template<typename T, unsigned int Offset, unsigned int Bits=1>
class BitField{
private:
    static constexpr T MASK = ((T(1)<<Bits)-1) << Offset;
    T value_;
public:
    operator T() const { return (value_ & MASK) >> Offset; }
    BitField& operator=(T rhs)
    {
        value_ = (value_ & ~MASK) | ((rhs & (MASK>>Offset)) << Offset);
        return *this;
    }
};

例えばこんな風に使えます:

template<unsigned int Offset, unsigned int Bits=1>
using BitField16 = BitField<std::uint16_t, Offset, Bits>;

int main()
{
    union Addr{
        uint16_t raw;
        BitField16<0,5> x;
        BitField16<5,5> y;
    };

    Addr v;
    v.raw = 0;       // 全bitを0に
    v.x   = 0b11111; // bit0-4へ代入
    v.y   = 0b10101; // bit5-9へ代入

    // ...
}

では何が問題かというと、このコードはビットフィールド同士で代入を行った場合に期待した動作になりません:

Addr v, t;
v.x = t.x; // 全bitが上書きされる!!

当然ここでは v の下位5bitを t の下位5bitで上書きすることを期待していますが、実際には全bitの上書きが行われます。これは BitField クラスがデフォルトの代入演算子を生成しており、そちらが使われてしまったためです。

検索すると BitField クラスの代入演算子を以下のようにメンバテンプレートにしている例もあります:

// このテンプレートがあってもデフォルトの代入演算子が使われる
template<typename T2>
BitField& operator=(T2 rhs)
{
    value_ = (value_ & ~MASK) | ((rhs & (MASK>>Offset)) << Offset);
    return *this;
}

しかし、これでも BitField 同士の代入ではクラスが生成したデフォルトの代入演算子が使われてしまうようで、やはり全bitが上書きされてしまいます。ネット上で見かけるコードは大体この罠にハマってしまっているようです。

ビットフィールド同士の代入も正しく処理するには、自分で代入演算子を定義すればいいようです(もっといい方法があるかもしれませんが):

BitField& operator=(const BitField& rhs)
{
    return *this = T(rhs);
}

ただし、これを定義すると今度は union 同士での代入がコンパイルエラーになります。エラーメッセージを読む限り、union のメンバが non-trivial なコピー代入演算子を持っているとき、その union の代入演算子は生成されないということのようです。よって、union 側にも自分で代入演算子を定義してやる必要があります:

union Addr{
    uint16_t raw;
    Addr& operator=(const Addr& rhs) { raw = rhs.raw; return *this; }
    // ...
};

一応ダメな例修正例の全コードを置いておきます。

実はNESエミュレータを自作(他の人の真似してるだけ)していて、このときbitfield templateが非常に便利だったのですが、この罠にハマったためにレジスタ間のコピーで値がおかしくなってBGが描画されず、丸一日悩んでました^^;
とりあえずSMB1の画面が出るところまではできましたが、APUについては知識が皆無なため音を出すまでの道のりが長そう…。