2012/10/27

ビットの位置(≒2の冪指数)を静的算出するtemplateメタプログラム

ビットフラグを使用していると、
たまに「そのフラグが立てているビットの位置」を知りたいというときがあります。


結論から言って、
以下のテンプレートクラス(構造体ですが、、)を書きました。
==================================
typedef unsigned int U32;

// ビット位置の静的算出
// 値Nについて、最大のビット位置を求める
//(例:N:127→7, N:128→8
template<U32 N> struct BitPos
{
 enum {
  pos = BitPos<N/2>::pos + 1,
 };
};
template<> struct BitPos<1> { enum { pos = 0 }; };  // 終了条件1
template<> struct BitPos<0> { enum { pos = -1 }; }; // 終了条件2
==================================


これはテンプレートメタプログラミングと呼ばれる、
C++のテクニックとしては古いものです。

このテクニックを利用すると、入力値に対しコンパイル時に値が算出されるため、
実行時の演算コストがなくなります。
コンパイル時に時間的なコストがあるといえばありますが。

動作的原理については、Wikipediaに詳しく書かれています。

このテンプレートは定数posの値を、
コンパイル時の再帰呼び出しによって決定します。

たとえば以下のように使用します。
==================================
static const U32 BIT_FLG = 0x04000000; // 例えばこんな定数がある

std::cout << BitPos<BIT_FLG>::pos << std::endl;


// 結果は26となる
// 0x04000000 => 0000 0100 0000 0000 0000 0000 0000 0000

==================================
 
大まかには以上です。 

※ 終了条件2について、-1としていますが、
この辺りは人によって意見が分かれるかもしれません。
個人的には、ビットフラグに0というのはフラグとして成り立っていないと考え、
設定値として無効な値であるから-1というエラー値を設定しています。
これが正しいというには難しいため、好みにより-1でも0でも良いと思います。


〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
以下は説明と見せかけて駄文です。
興味のある方だけお読みください。


例えば、
==================================
typedef unsigned short U16;
static const U16 BIT_FLG_A = 0x1000;
static const U16 BIT_FLG_B = 0x2000;
static const U16 BIT_FLG_C = 0x4000;
static const U16 BIT_FLG_D = 0x8000;
==================================

という定義があったとして、
ある変数について、このビットフラグを扱うとき、
【0x0000】または【0x0001】という結果を、
演算で求めたい場合があるとします。

そのとき場合のコードはこうなると思います。
==================================
// 入力値 U16 hoge = 0x2018 から0x2000を得て、
// 0x0000 or 0x0001を得る

U16 ret = 0x0000;
ret = (hoge & BIT_FLG_B) >> 13;  // ここにリテラルが発生する
==================================

この例題だと正直、素直にif文でもよいですが、
ここではそういう話はしません。

コメントに記したように、
0x0001を得るために13bit右シフトを意味するリテラルを書くわけですが、
この13というリテラルは気持ち悪いと感じる事もあるでしょう。
だからこの13に対して BIT_FLG_Bの対となる定数をまた用意したりして、
==================================
// BIT_FLG_Bでマスクした結果から 
// 0x0000 or 0x0001を得るための定数値
static const U16 FLG_B_SHIFT_NUM = 13;


// 入力値 U16 hoge = 0x2018
U16 ret = 0x0000;
ret = (hoge & BIT_FLG_B) >> FLG_B_SHIFT_NUM;  // 定数に変更
==================================

とかやってはみるものの、
非常に無駄な作業をしている気がしてならなくなってくるわけです。

もう面倒だから13でいいやとか、投げやりにもなりたくなります。

更に、データフォーマットが変わってしまって、
型がU16からU8(unsigned charとします)に変えなければならない時、

この【FLG_B_SHIFT】の型、定数値も変えてやらなければならないという事態になります。
==================================
static const U8 BIT_FLG_A = 0x10;
static const U8 BIT_FLG_B = 0x20;
static const U8 BIT_FLG_C = 0x40;
static const U8 BIT_FLG_D = 0x80;

// BIT_FLG_Bでマスクした結果から 
// 0x00 or 0x01を得るための定数値
static const U8 FLG_B_SHIFT_NUM = 5;

// 入力値 U8 hoge = 0x24
U8 ret = 0x00;
ret = (hoge & BIT_FLG_B) >> FLG_B_SHIFT_NUM;
==================================
↑こんなふうに。



ではもっと、効率的で意味を可視化できる方法がないでしょうか?


ここで、0x20と5との関係性を考えたとき、
2^5 = 32 (=0x20)であるということがあります。
これは逆に32を2で5回割れば1となるというとです。

と言うことで、
冒頭のtemplateでは、1 < Nの関係が成り立つ間、
N÷2を再帰的に繰り返せばいいじゃないという発想のもと、ビット位置を算出しています。

前述のソースに適用すると、

==================================
static const U8 BIT_FLG_A = 0x10;
static const U8 BIT_FLG_B = 0x20;
static const U8 BIT_FLG_C = 0x40;
static const U8 BIT_FLG_D = 0x80;

// 入力値 U8 hoge = 0x24
U8 ret = 0x00;
ret = (hoge & BIT_FLG_B) >> BitPos<BIT_FLG_B>::pos;
==================================
となり、BIT_FLG_xがどんな値であるかを意識する必要がなくなります。

ただし、一つ難点があるといえば、
BitPos<xxx>::posとかいってますが、一体どこのposなのか判らないということです。

例えば0x24(0010 0100)という2つのフラグがある場合、
これに対してはどこのbit位置なのか?


そこで、冒頭のテンプレートを
以下のように変更することで、より意味を明確にしました。

==================================
// ビット位置の静的算出
// 値Nについて、最大のビット位置と最小のビット位置を求める
//(例:124→most:7, least:2、 128→most:8, least:8
template<U32 N> struct BitPos
{
 enum {
  most = BitPos<N/2>::most + 1,    // 最大位置
  least = ((N & 1) == 0) ? BitPos<N / 2>::least + 1 : 0 // 最小位置
 };
};
template<> struct BitPos<1> { enum { most = 0, least = 0 }; };
template<> struct BitPos<0> { enum { most = -1, least = -1 }; };
==================================
leastはNのLSBが0である間だけbit位置をカウントします。
LSBが1になれば、つまり最初の最小のビット位置を発見したということなので、
再帰呼び出しを終了します。
また、そもそも最初の入力値Nが0というのはビットフラグとして成り立っていないため、
強制的にカウントを-1として終了します。

N=1のときは、LSBが立っていることが明らかであるため、
結果を0ビット目とします。


以上、
長々とお付き合い頂き、ありがとうございました。

0 件のコメント:

コメントを投稿