Home / C++ template

C++ template テンプレート

2011-2-5 少し整形
2013-11-18 少し追記

再帰

テンプレートのマッチを応用した再帰の例は良く見かけるものの、これと言った実用性は無いように思っていた。 が、「コンパイル時に文字列リテラルに指定のアルゴリズムによる計算を施し、整数に変換するには?」 との記事を見かけ、「おぉースゲー」。

そこで応用として 2進表記の文字列("1010" とか)を整数へ変換するテンプレート関数を考えてみた。

#include <cstddef>

template <std::size_t N>
int binstr(const char (&str)[N], int m = 0)
{
    return
    binstr(reinterpret_cast<const char (&)[N-1]>(str), m + 1)
    + ((str[N-2] == '1' ? 1 : 0) << m);
}

template <>
int binstr<1>(const char (&)[1], int)
{
    return 0;
}

#include <stdio.h>

const int v = binstr("0001" "0101");  // 4bit ずつ離しているだけ。

int main(int, char*[])
{
    return printf("%d 0x%08X\n", v, v);
}

何の捻りも無く、シフト量を引数に持った処なんかショボイ訳だけど、こんな程度でもハマったのはリテラル文字列の参照が終端文字も含んで一致するって事。
例えば "1010" ならば N = 5 に一致する。

そう言う訳で右端文字を [N-2] で参照し、また特殊化の <1> は空文字列に相当する。

ところで、上の例ではグローバルの const int へ代入(宣言)しているけど、関数内で binstr を呼んだ場合に gdb で見ると、嬉しい事に再帰する様子が見える。嬉しいと言うよりも、このテンプレートってコンパイル時計算になってないような気もするんだけど… デバッグモードのせいなのか?
(2013-11-11) そう、コンパイル時計算(定数)にはならない。デバッグモードなんかのせいではなく、関数形式だからと思う。オリジナル記事は即値を求める見出しだったが、自分でやった限りは即値にならない。

さて、特殊化した関数ではデフォルト引数を指定していないが、指定するとコンパイルエラーとなる。では特殊化した関数にマッチするような関数呼び出しを書く場合に引数を省略できないのかと思うが、そうではなく、特殊化でない関数のデフォルト引数が採用される。…なるほどね、デフォルト引数は所詮、コンパイラのデフォルト処理(フォ−ルト潰しの意)に過ぎない訳だから、その指定がコロコロ変わるのでは使う側にとっても良くない訳です。
合理的な仕様だと感心。

(2013-11-07)

GCC 4.3以降?なら、2進表記を直接書けるそうで、上の例は徒労に終わる。orz

int main(int, char*[])
{
    return printf("%d", 0b00010101);
}

が、これにも落ちがあって、32bit環境なせいか不明だが、64bit整数は定義出来なかった。警告も出なかったので非常に不満だ。シフトして和を採れば済むが、それを言い出すと...

再帰:もう少し実践向け

さて、もう少し実践向けに、HOGE 構造体ってものがあって、その配列中からメンバ v の最大値を探して返す例を考えてみました。普通なら実行時に探す訳ですが…

struct HOGE {
    int v;
};

HOGE hoge[] = {
    { 1025 }, { 65535 }, … 
};

template <size_t N>
inline int get_max(const HOGE (&arr)[N])
{
    int tmp = 
    get_max(reinterpret_cast<const HOGE (&)[N-1]>(arr));

    return    arr[N-1].v > tmp    // 大きい方
            ? arr[N-1].v : tmp;
}

template <>
inline int get_max<1>(const HOGE (&arr)[1])
{
    return arr[0].v;
}

何番目の添え字が最大だったかなんてのは無視して単に最大値を返すです。重要なのは特殊化の方で、[1] は1個の配列参照であり、配列の先頭を指します。そしてこれが最初に実行(コンパイル中に)される訳です。だからこの場合は単純に自身の値を返すだけになってますです。

もうちょっと汎用性を高めたいですが、HOGE そのものは再帰の要ですし、何しろ純粋な既存のデータ扱いにしたいので別クラスに任せましょう。

再帰:さらに…

と言う事で HOGE はそのままとし、適当なクラスへまとめたかったのだけれど、クラスメンバ関数ではコンパイル出来なかったので適当な namespace で括って、比較関数 comp と値参照の value を追加・分離。HOGE も class T へパラメタ化し、再帰関数名も recursion へ改名。 う〜ん、何やら qsort や bsearch な雰囲気。 ま、そういう事なんだけどね。

struct HOGE {
    int v;
};

HOGE hoge[] = {
    { 1025 }, { 65535 }, … 
};

namespace max_in_hoge
{
    inline int comp(const HOGE& ref, int tmp)
    {
        return 
          ref.v > tmp    // 大きい方
        ? ref.v : tmp;
    }

    inline int value(const HOGE& ref)
    {
        return ref.v;
    }

    template <class T, size_t N>
    inline int recursion(const T (&arr)[N])
    {
        return comp(arr[N-1], 
            recursion(reinterpret_cast<const T (&)[N-1]>(arr)));
    }

    template <>
    inline int recursion<HOGE, 1>(const HOGE (&arr)[1])
    {
        return value(arr[0]);
    }
};

呼び出しはこんな感じ。

const int GMAX = max_in_hoge::recursion(hoge);
int main(int argc, char* argv[])
{
    printf("%d\n", GMAX);
    return 0;
}

まぁ、やってる事が単純なので コンパイル時に 実行時定数的に答えが得られても旨みは少ない。


template で配列サイズ

C/C++ でサイズ固定な配列要素数を得るには

sizeof (array)/sizeof (array[0])

とするのが常套 テンプレートなら関数化してもインスタンス化する時に解決してくれるんじゃないか、などとつい期待してしまう。 短ければマクロでも構わないが、まとまった行になる程、関数化したい。 VC++で使いたく、その辺に問題があったのを思い出した。 (←何の事書いていたか忘れてしまった)

で、MFC や VC8 以降?では stdlib.h に _countof(_Array) と定義されている。

一方、C++ では template で得る方法になっていて

template <typename __CountofType, size_t _SizeOfArray>
char (*__countof_helper(UNALIGNED _CountofType (&_Array)[_SizeOfArray]))[_SizeOfArray];
#define _countof(_Array) sizeof(*__countof_helper(_Array))

もう1つ、IPヘルパー系のヘッダー(wspiapi.h)に見られるものは

template <typename __CountofType, size_t _N>
char (&__wspiapi_countof_helper(__CountofType (&_Array)[_N]))[_N];
#define _WSPIAPI_COUNTOF(_Array) sizeof(__wspiapi_countof_helper(_Array))

下のマクロで sizeof とあるので、配列宣言された要素数を得ているのかと思うが、これは関数の宣言。因みに関数本体は無い。

引数は初めに書いたように配列の参照である事に加え、sizeof が働くように関数の戻りも配列の参照になっている処がミソ。詳しくは色んな人が解説しているので、そちらを参照して頂くとして、要は配列を素で渡すとポインタへ変換されるので、参照渡しにして、サイズもテンプレートパラメタにする、と言うもの。余り見かけない形式だけどこんな感じ。

template <typename T, size_t N>
void tag(T (&x)[N])
{ ... }

結構昔の記事だが、この辺の話は今でも上位にヒットする テンプレート引数の一致に判り易く解説されている。

なお、色々試されているが VC6 ではコンパイル出来ない。参照渡し自体がおかしい訳ではなく、テンプレート機能がおかしい。VC6 での新規開発は殆ど無いと思うが、既存の改修は十分有り得る。そのような場合、VC7 以降にリリースされた Platform SDK を導入してコンパイルしようとすると、IPヘルパー辺りで _WSPIAPI_COUNTOF のコンパイルエラーを食らう(wspiapi.h)。 回避は簡単だけど、意地悪ですね。VC6 なら sizeof/sizeof を使うように書いておけばいいものを...

ところで用途はともかく、配列の参照を返す関数が VC6 でイケるかどうかは試しておこう。

char ac[3] = { 'a','b','c' };
int  ai[3] = { 1,2,3 };

char (&f_char())[3] { return ac; }
int  (&f_int())[3]  { return ai; }
int main()
{
    char (&x)[3] = f_char();
    int  (&y)[3] = f_int();
    return 0;
}

ちゃんと x, y は参照になっている。

(2014-2-25)配列の参照宣言は滅多に使わないが、概念はちゃんと把握しておいた方が良い。
別途、書こうと思っているが、昨今の buffer overflow 検出がサイズ的要素でもって判定されているのは間違いないと思う一方、言語仕様を制限するような検出方法が採られている、と見做せなくもない風潮が感じられる。
そんな時、コンパイラ屋の考えと立ち向かうだけの概念は身に付けておくべきだと思う。

コンパイル時定数へのトライ

事の起こりはクラス型で enum に値を保持するものであった。

template <int N>
struct hoge {
    enum { value = ... };
};

template <>
struct hoge<0> {
    enum { ... };
};

ここでテンプレート引数に文字列を食わせられないかと...

template <const char (&STR)[5]>
struct hoge {...};

extern char const foo[] = "1001";
hoge<foo>* tmp;

これで一応はコンパイル通るが(extern が必要なんかは後回し)、文字列の中身を覗いて定数化しようとすると拒否される。(GCC 4.3.3)

template <const char (&STR)[5]>
struct hoge { enum { v = STR[0] }; };

error: template parameter ‘STR’ of type ‘const char (&)[5]’ is not allowed in an integral constant expression because it is not of integral or enumeration type

←この辺は初心者が驚く事の1つとある。う〜む、配列と言えども const って言うとるのだから定数と認めてもいいんじゃないか? と今だけは思うが...

と言う訳で、テンプレート(引数)が外界と通信できる生の型は今の処、整数のみなのであった。関数ポインタも可能らしいが、それもまぁ数値の範囲だ。浮動小数点については技術的には問題無いらしい...



Home / C++ template

© 2008 usskim    http://usskim.web.fc2.com/
inserted by FC2 system