型を引数に取る関数形式マクロ(macro-like function)を使ってみる
C言語の#define
でびっくりしたことがあったので紹介する.
(びっくりしたのは何年も前だが)
良くサンプルコードで以下のようなコードを見かける.
# define max(a,b) ((a) > (b) ? (a) : (b))
これはint
だろうがdouble
だろうが大きい方を返すので,柔軟性が高い云々紹介されている.
この関数形式マクロの是非は置いておいて.
実はこの関数形式マクロ,型の名前を引数に取っているかのように動作させることができるのだ.
何の意味もない例を挙げるとこんな感じ。
# define as(type, a) ((type)a)
int b = as(int, 6.0);
ではこれを使って何ができるというのか.
閑話
CからC++やpythonなど、比較的新しめの言語を触ってCに戻ると、配列操作がクソ面倒臭いことに気が付く。
最初から固定長で対応できるのであればよいが、拡張性を高めたいという欲望に負け、mallocとfreeが撒き散らされたコードができる.
リッチな環境であれば、多少のfreeのし忘れも長期間起動させるようなものでなければ,
OSがいい感じにしてくれる場合もあるそうだが、やはり気になるものだ。
最近はvalgrindを覚えてfree忘れを確かめるようにしている。
しかしながら、ソケット通信をするとメモリ開放し忘れとvalgrindに怒られるが,こっちのコードには問題はない.
いつになれば修正されるのだろうか。
まあ,そういうことでCでもどんな型でも対応できる柔軟な可変長配列が欲しくなる.
何がすごいのか
閑話で例に挙げた通り,C言語でVectorっぽいものを作ろうとすると,以下の二つの方法がまず思い付くであろう。
-
void*
を使う - マクロで型ごとにコードを自動生成する
1の手法は以下のようにしてできなくはないが,
typedef struct{
size_t size;
size_t elem_size; /* sizeof(int) などが入る */
void* ptr;
} Vector;
でもこれで要素にアクセスしようとすると,
void* vec_elem(Vector* v, size_t i){
return ((char*)v->ptr)[i*v->elem_size];
}
int a = (int*)vec_elem(v, 3);
まあ手動キャストの連発.
そもそもvoid*
に入れた時点で,型が消し飛ぶ.
したがって,コンパイラーが型チェックできなくなってしまう.
なので,安全性を取るのであれば2を使うのが一つの手だ.
こちらの場合型の名前を使って型毎に別のコードを生成するため,間でvoid*
を使いでもしない限り,
コンパイラーが正しく型の整合性をチェックできるようになる.
これはマクロが頑張ってくれるというよりも,そういう風に上手く書き変えられるようにしたという認識である.
今回はどう実装するのかについては触れないが,型を引数に取るかのように見える関数形式マクロがどういう風に使えるのかについてだけ説明する.
よくある例は以下のものだ.
# define Define_Vector(type) \
typedef struct{ \
size_t size; \
type* ptr;\
} Vector_##type;
Define_Vector(int)
実際にはこれだけではなく,使用する全ての関数を書かないといけない.
こんなイメージ.
# define Define_Vector(type) \
...
void Vector_Push_##type( Vector_##type* v, type* a ){ \
v->ptr[v->size++] = *a; \
} \
...
それでこんな感じに使う.
Define_Vector(int)
Vector_int* v;
...
int a = 3;
Vector_Push_int(v, &a );
ここまではよくあるので面白くもない.
でもここで以下のようなマクロを定義してやると
# define Vector_Push(type, v, a) Vector_Push_##type(v, a)
こんな感じに使える.
int a = 3;
Vector_Push(int, v, &a );
このコードではキャストをしていないので,型情報が上書きされない.
だからコンパイラが型チェックできる.
しかも型を引数に取っているかのように見え,適切な関数を自動で選択してくれる(厳密には型に応じた関数名に勝手に書き変えてくれる)コードに見える.
すごい!見易い!(と私は思います)
欠点としては
- よけいな関数形式マクロを一つ用意しないといけない.
- 毎回型を指定して関数形式マクロを呼び出さないといけない.
などがある.
型指定が必要な分C++のVectorより少しは劣るが,auto
ばっかりで何が何だか分からないまま使うよりはましと言えなくもない.
注意
上に挙げたコードは全てイメージを喚起するためのもので,やばいコードばかりです.
穴ぼこだらけです.
決してそのまま使ってはいけません!