複数の値を一つの変数に纏める関数 combine とメタ関数 combineN_

様々なファイルのフォーマット解析をしていると,ファイルタイプの識別用などの理由で「マジックナンバー」が現れる事があります.これらのマジックナンバーは実は文字列である事が多く(例えば,*.bmpマジックナンバーは 0x424D == {'B', 'M'},*.gif のマジックナンバーは 0x474946 == {'G', 'I', 'F'}),マジックナンバーを直書きするよりは文字列を書いておいた方が間違えにくいなどの利点があります.

そこで(これ以外にも所用でいろいろと必要があったりしたため),複数の値を一つの変数に纏める関数を書いてみました.

関数 combine

template <class T1, class T2>
std::size_t combine(const T1& x1, const T2& x2);

template <class T1, class T2, class T3>
std::size_t combine(const T1& x1, const T2& x2, const T3& x3);

template <class T1, class T2, class T3, class T4>
std::size_t combine(const T1& x1, const T2& x2, const T3& x3, const T4& x4);

引数は 2 〜 4 個で,渡された値を(先に指定した値を上位バイトとして)連結したものを std::size_t 型として返します.例えば,0x20, 0x32b5 を指定すると 0x2032b5 と言う結果を返します.尚,引数に指定した値の合計バイト数が std::size_t よりも大きい場合は,上位ビットから消失していきます.

使用例として,例えば,*.bmp であるかどうかのチェックは以下のような形になります.

#include <iostream>
#include <fstream>
#include "clx/utility.h"

int main(int argc, char* argv[]) {
    if (argc < 2) std::exit(-1);

    std::ifstream ifs(argv[1], std::ios::binary);

    // ビットマップファイルは,最初の 2Byte が 'B', 'M'.
    unsigned short magic = 0;
    clx::get(ifs, magic, clx::endian::big);
    if (magic == clx::combine('B', 'M')) {
        std::cout << "Bitmap file." << std::endl;
    }
    else std::cout << "Unknown file." << std::endl;

    return 0;
}

メタ関数 combineN_

combine() は,switch 文の分岐には使用する事ができません.例えば,以下のコードはコンパイルエラーになります.

unsigned char sig[3];
clx::read(ifs, sig);
if (clx::combine(sig[0], sig[1], sig[2]) == clx::combine('G', 'I', 'F')) {
    unsigned char ver[3];
    clx::read(ifs, ver);
    switch (clx::combine(ver[0], ver[1], ver[2])) {
    case clx::combine('8', '7', 'a'):
        std::cout << "GIF version 87a." << std::endl;
        break;
    case clx::combine('8', '9', 'a'):
        std::cout << "GIF version 89a." << std::endl;
	break;
    default:
        std::cout << "GIF unknown version." << std::endl;
        break;
    }
}

そこで,同じ働きをするテンプレートメタ関数 combineN_ を作成してみました.こう言ったものはマクロで書かれる事が多い気がしますが,せっかくBoost.勉強会なんかもあったことですし敢えてこっちでw

template <std::size_t X1, unsigned char X2>
struct combine2_ {
    static const std::size_t value = (X1 << CHAR_BIT) | X2;
};

template <std::size_t X1, unsigned char X2, unsigned char X3>
struct combine3_ {
    static const std::size_t value = combine2_<combine2_<X1, X2>::value, X3>::value;
};

template <unsigned char X1, unsigned char X2, unsigned char X3, unsigned char X4>
struct combine4_ {
    static const std::size_t value = combine2_<combine3_<X1, X2, X3>::value, X4>::value;
};

使用方法は,以下のようになります.

#include <iostream>
#include <fstream>
#include "clx/utility.h"

int main(int argc, char* argv[]) {
    if (argc < 2) std::exit(-1);
    std::ifstream ifs(argv[1], std::ios::binary);

    /*
     * GIF ファイルは,最初の 3Byte が 'G', 'I', 'F'.
     * その次の 3Byte がバージョン情報であり,現在有効なバージョンは
     * '8', '7', 'a' と '8', '9', 'a' の 2 種類.
     */
    unsigned char sig[3];
    clx::read(ifs, sig);
    if (clx::combine(sig[0], sig[1], sig[2]) == clx::combine('G', 'I', 'F')) {
        unsigned char ver[3];
        clx::read(ifs, ver);
        switch (clx::combine(ver[0], ver[1], ver[2])) {
        case clx::combine3_<'8', '7', 'a'>::value:
            std::cout << "GIF version 87a." << std::endl;
            break;
        case clx::combine3_<'8', '9', 'a'>::value:
            std::cout << "GIF version 89a." << std::endl;
            break;
        default:
            std::cout << "GIF unknown version." << std::endl;
            break;
        }
        return 0;
    }
    
    std::cout << "Unknown file." << std::endl;
    return 0;
}

4バイト以上のマジックナンバーなども比較的よく出現するので結局その場合は配列などに格納して memcmp などをするしかなくなるのですが,ある程度は楽に(typo ミスをする事なく)書けて良いかなぁと思います.