WisdomSoft - for your serial experiences.

8.3 マクロ関数

マクロ関数は #define ディレクティブによるマクロ展開でパラメータを受け取ることができ、任意のテキストを引数にマクロ展開できます。これによって、関数のように振る舞うマクロを作成できますが、あくまでプリプロセッサによってコンパイル前に展開されるテキストなので関数呼び出しのオーバーヘッドが発生しません。簡素な計算処理の展開に適しています。

8.3.1 パラメータ付きマクロの展開

何度も使われる計算式などは、関数としてまとめることで何度も書き直す作業から解放されますが、単純な計算式の関数化は異なる問題を引きを越します。関数を呼び出すには、関数のパラメータに引数を渡すため、値をコピーしてスタックに保存するという作業が発生してしまうのです。パラメータのない関数でも、制御が呼び出し元に復帰するために、機械語レベルではアドレスの保存などが行われています。

高速なプログラムを実現するためには、こうした無駄な処理は少なくする必要があります。しかし、関数を使わないプログラムは、極めて非効率的で再利用性が失われてしまいます。単純な計算式を効率よく関数化する方法はないのでしょうか?

そこで、単純な計算式などでは、変数同様にコンパイル時に展開してしまうという手法が有効となります。つまり、プリプロセッサを使うのです。#define はトークン列に名前をつけて、コンパイル時にプリプロセッサによってこれが置き換えられるというものでした。定数ではなく、計算式に名前をつけてソース内の必要な場所で用い、コンパイル時に置き換えれば、効率的に高速なプログラムを作成することができます。

#define は、マクロ関数と呼ばれる引数を受け取ることができる置き換え処理をサポートしています。これは、利用者から見れば関数のような働きをします。#define が引数の置き換えをサポートするには、次のような構文を用います。

#define ディレクティブ(マクロ関数)
#define 識別子(引数1 , 引数2 ...) トークン列

このマクロ関数を利用する人間から見れば、識別子が関数名であり、トークン列の型が戻り値のように感じられるでしょう。しかし、マクロは本当に引数を受けるわけではありません。単純に、トークン列に対して引数を展開するだけです。例えば、次のマクロ関数を見てください。

#define ADD(a , b) a + b

これは、ADD() マクロ関数を定義しています。ADD(10 , 20) とソース内で記述すれば、その位置にプリプロセッサが 10 + 20 というテキストに置き換えます。マクロ関数の開発者は、関数が呼び出されるのではなく、単純にその位置にトークン列が展開されるだけであることを意識しなければなりません。

コード1
#include <stdio.h>
#define MUL(multiplicand , multiplier) multiplicand * multiplier

int main() {
	printf("5 * 5 = %d\n" , MUL(5 , 5));
	return 0;
}
実行結果
コード1 実行結果

コード1では乗算を行う MUL() というマクロ関数を定義しています。MUL() マクロ関数の利用者にとっては、これがマクロ関数であることを特に意識する必要はありません。マクロ関数の開発者は、このマクロ関数の仕様を他の関数と同じように定めることができます。

しかし、何度もいうようにマクロ関数の開発者は、マクロ関数はテキストレベルで置き換えられるものであり、実際の関数のように実行時に呼び出されるものではないということを意識しなければなりません。実はコード1の MUL() マクロ関数には、通常の関数では発生しない大きな問題を含んでいます。これは、次のプログラムを実行すれば理解できるでしょう。

コード2
#include <stdio.h>

#define MUL(multiplicand , multiplier) multiplicand * multiplier
int mul(int multiplicand , int multiplier) {
	return multiplicand * multiplier;
}

int main() {
	printf("マクロ関数 : MUL(3 + 2 , 5) = %d\n" , MUL(3 + 2 , 5));
	printf("通常の関数 : mul(3 + 2 , 5) = %d\n" , mul(3 + 2 , 5));
	return 0;
}
実行結果
コード2 実行結果

コード2では、 乗算を行うマクロ関数 MUL() と、同様の処理を行う通常の関数 mul() が定義されています。main() 関数内でこれら2つの関数に、同一の引数値を与えて結果を表示していますが、なんと、これらは異なる値を返します。結果は上の実行結果のようになりました。通常の関数が返した 25 という値は、開発者が意図した値でしょう。しかし、マクロ関数は 13 という奇妙な計算結果を返します。なぜでしょう?

関数は、関数が呼び出される前に引数に指定された式が評価されます。そのためコード2では 3 + 2 という式が評価され、最終的には mul(5 , 5) という形で関数が呼び出されます。

しかし、マクロ関数はテキストレベルの単純な置き換え処理なので、このような式の評価は行われません。MUL(3 + 2 , 5) というマクロの指定は、3 + 2 というテキストが multiplicand として展開されるため、コンパイル時には 3 + 2 * 5 という式に展開されているのです。そのため、演算子の優先順位に基づいて 2 * 5 が先に計算され、次に 10 + 3 が計算されます。その結果 MUL(3 + 2 , 5) は 13 を返したのです。

こうした問題を解決するためには、マクロ関数の引数には必ず ( ) を指定して優先順位を守るという方法があります。MUL() 関数は次のように定義するべきです。

#define MUL(multiplicand , multiplier) ((multiplicand) * (multiplier))

このように括弧で括ることによって、展開後の計算順序を保護することが可能です。 

最後に、代表的なマクロ関数の実践方法を示します。マクロ関数が威力を発揮するのは、複雑な計算式を簡略化する場合などです。行政手続や税制が複雑であれば住民が敬遠するように、あなたが作ったシステムの仕様がどんなに効率的なものでも、計算処理などに手間がかかるようであれば開発者はシステム・サービスを使いたいとは思わないでしょう。

例えば、32 ビットシステムにおいて、RGB 値が各要素 8 ビットずつで表されるシステムの場合、色の要素にアクセスするためにはシフト演算などを行って特定のバイトを抽出しなければなりません。こうした計算処理はマクロ関数が得意とする分野です。

コード3
#include <stdio.h>

typedef unsigned char BYTE;
#define RGB(r , g , b) ((BYTE)(r) << 16) | ((BYTE)(g) << 8) | (BYTE)(b)
#define RED(color) (BYTE)((color) >> 16)
#define GREEN(color) (BYTE)((color) >> 8)
#define BLUE(color) (BYTE)(color)

int main() {
	int color = RGB(0xFF , 0xEE , 0xAA);
	printf("R = %X : G = %X : B = %X\n" ,
		RED(color) , GREEN(color) , BLUE(color));
	return 0;
}
実行結果
コード3 実行結果

コード3では、色を示す数値を作成するためのマクロ関数 RGB() と、赤、緑、青の各要素の値を抽出するためのマクロ関数 RED()、GREEN()、BLUE() を定義しています。このプログラムは 32 ビットコンピュータ上で動作するものとし、下位から順番に青、緑、赤の要素値が8ビットごとに並ぶカラー情報を扱うことを想定しています。

単一の数値で複数の情報を保有することは珍しいことではありません。このような仕様が与えられた場合は、データの生成や単一情報の取得にビット演算が必要になります。しかし、何度もそれを記述するのは面倒であり、複雑なプログラムは単純なミスを誘うことになります。そこでコード3のように、マクロ関数を用いて問題を単純化します。

RGB() マクロは、r に赤要素を、g に緑要素を、b に青要素を、それぞれ 1 バイトで指定するものとします。マクロ関数はこれらの引数を基に、各要素を適切な位置に移動する式に展開します。RED()、GREEN()、BLUE() マクロ関数の color には、RGB() マクロ関数で作成した 32 ビットの数値型を渡します。これらのマクロ関数はカラー情報から目的の1バイトを抽出する式に展開します。

この他にも、関数の呼び出しを隠蔽するマクロ関数(マクロ関数から関数を呼び出すことによって、手続きを簡略化する)などの利用もされています。このように、マクロ関数は計算式だけで構成される比較的単純な変換処理などに多用される傾向があります。