わさっきhb

大学(教育研究)とか ,親馬鹿とか,和歌山とか,とか,とか.

関数形式マクロを使って,式を定義するとき

関数形式マクロを使って,1個以上の値から1個の値を求めるような式を定義するとき,置換内容には,それぞれの引数と,全体にカッコをつけるのが原則です*1
この原則を知れば,

#define multiply_bad(x,y) x*y

とするのはよくないと言えます.というのも,これを使って

int a = multiply_bad(3+3,2+2);

とすると,関数形式マクロは単純な置き換えですから,

int a = 3+3*2+2;

となり,これは3プラス6プラス2で11です.一方,(3プラス3)かける(2プラス2)は,9×4で36です.ぜんぜん違う値ですね.
このとき,対策として

int a = multiply_bad((3+3),(2+2));

のように,使用する*2側のそれぞれの引数にカッコをつけると,どうなるでしょうか.これなら

int a = (3+3)*(2+2);

です.関数形式マクロによる置き換えで,一つ目の引数は「(3+3)」と内側のカッコまで含みます.2番目は「(2+2)」です.
じゃあこのほうがいいのでは…と言いたくなるところですが,これは「問題を,どのレベルで解決しようとするか」ということに関わってきます.ここでの「問題」は,「掛け算をする関数形式マクロをどのように定義すればよいか」です.
今とった方法をもう一度言いますと,「使用する側の引数にカッコをつける」ということです.2引数ならすでに書いたとおりです.1引数の場合は,そうですね,絶対値の関数形式マクロを出しましょうか.

#define abs_bad(x) x>=0?x:-x

これは,三項演算子を使って,xが0以上ならxそのものを,xがマイナスなら,-x,すなわち符号反転したxの値を,評価結果にするというものです*3.これに対して,そうですね,

double b = abs_bad(-1.2);

コンパイルエラーです*4.実際,こんなコードを書いて,

/* abs_bad.c */
#include <stdio.h>

#define abs_bad(x) x>=0?x:-x

int main(void)
{
  double b = abs_bad(-1.2);
  printf("b = %g\n", b);
  return 0;
}

コンパイルすると…あれ,おかしいなあ.エラーにならなかったですね.「-1.2>=0?-1.2:--1.2」となってほしかったのですが.念のため実行してみると,「1.2」.期待通りに絶対値が求められました.
gccでは-Eオプションを与えれば,前処理後のソースコードが標準出力に出てきますので,確認しますと,

$ gcc -E abs_bad.c
(略)
int main(void)
{
  double b = -1.2>=0?-1.2:- -1.2;
  printf("b = %g\n", b);
  return 0;
}

…あ,そうでした.abs_badの中の「-x」は「-」と「x」の2つのトークンで構成されていて,関数形式マクロによる置き換えで,このマイナス記号と,引数の「-1.2」のマイナス記号がくっついて「--」になるなんてことは,ないのでした*5.失礼しました.
では別の例を考えましょう…abs_badの定義はそのままで,

double b = abs_bad(0.1-0.2);

というのだと,これは

double b = 0.1-0.2>=0?0.1-0.2:-0.1-0.2;

となります.意味を変えることなく,カッコや空白を入れて分かりやすくすると,

double b = ((0.1-0.2>=0) ? (0.1-0.2) : (-0.1-0.2));

です.最初の「0.1 - 0.2 >= 0」は,「-0.1 >= 0」となって偽ですので,「-0.1-0.2」が評価結果となります.中学1年の数学の式でも出てきたように,これは-0.3です.変数bには-0.3が代入されます.
しかしabs_badを用いたbの定義式を見直すと,これは,-0.1の絶対値,すなわち0.1をbに格納したかったはずです.
この原因は,関数形式マクロまで立ち返ると,「-x」のところが,展開によって「-0.1-0.2」となっている点です.
どえらい回り道をしましたが,やっと,0.1をbに格納するために,どうすればいいかを考えることができます.あらかじめ予防線を張らせてもらいますが,「double b = 0.1;」という答えを望んでいるのではありません.絶対値を求める関数形式マクロを自作して,かつ複雑な式からなる引数を指定しても,意図どおりの結果になるには,どうすればよいかを考えたいのであり,「0.1-0.2」は例の一つにすぎません.
方法は二つです.一つは,使用するときに,引数にカッコをつけること.

double b = abs_bad((0.1-0.2));

もう一つは,定義のときに,引数と,全体にカッコをつけること.

#define abs_good(x) ((x)>=0?(x):-(x))

double b = abs_good(0.1-0.2);

今,画面を注意深く見ていた人は,「abs_g」のあとに「abs_good」になったのに気づいたと思います.これは,Emacsの動的略語展開というもので*6,Altを押しながら/(スラッシュ)を押しました.これで,Emacsの内部では,abs_gから始まる単語を探しまして,このケースではabs_goodひとつしかないので,書き出してくれます.「補完」とも言います.関数形式マクロに限らず,自作関数の呼び出しでも,変数の補完にも使えます*7.余談でした.
さて…「使用するときに,カッコをつけること」と「定義のとき,引数と,全体にカッコをつけること」だと,どっちもどっちに見えますが,ここでもう一つ,条件を追加します.といっても,通常使う場合には当たり前のことです.プログラムの中で,“その関数形式マクロは,何回も使用される”というものです.
そうすると,前者は,「使用するときに,そのたび,引数にカッコをつけること」となります.後者は「定義のときに,引数と,全体にカッコをつけるが,使用するときには,引数にカッコをつけなくてもよい」ということです.なお,abs_goodでもabs_badでも,いちばん外側のカッコは,関数形式マクロを使用するため構文上必要なカッコなので,ここで考えているカッコとは別物と考えてください.
こう比較すると,定義の段階で手間をかけることのメリットがはっきりしますね.呼び出すときに「abs((0.1-0.2))」だとか「multiply((3+3),(2+2))」だとか書くのは,タイプ数もソースファイルのサイズも多くなりますし…まあそれは現在のコンピュータ環境では微々たるものですが…それよりも,こんなにカッコを多用したプログラムは,Cらしくありません.
これが,問題を「根本」で解決しようとするか,それとも「表面的なところ」で解決しようとするかの違いです.ぜひ,「根本」を見つけ,将来にわたって利用可能な解決法を考え,与えていきながら,そんな経験を通じて,あなたなりの方法論を身につけていってください.
ここでは,「#define multiply_bad(x,y) x*y」や「#define abs_bad(x) x>=0?x:-x」と定義すると,期待通りに掛け算や絶対値にならない場合がある,という問題から始まって,2通りの解決方法の比較をしましたが,世の中の問題は…プログラミングに限らず…最初から「_bad」と,解決しないといけないことが暗に示されているとは限らないというのは,注意しないといけませんね.
もう一つ,補足があります.「#define multiply_bad(x,y) x*y」を変えてはいけないという制約があったときには,残念ながら,根本だと思った箇所に対して解決を図ることはできません*8.そういうときは「使用するときに,そのたび,引数にカッコをつけること」になるでしょう.一時期,このような種類の対処法は「バッドノウハウ」と呼ばれていましたが,今は下火ですかね.死語かもしれません.まあそんなフレーズも知っといてください.

*1:制御文などを組み入れた複雑な場合は別です.

*2:授業では「関数の呼び出し」と対比して「関数形式マクロの使用」と表現し,「関数形式マクロの呼び出し」と言わないようにしましたが,何をもって「呼び出す(invoke)」と言うのかという問題なんだと気づくと,不適切とは言えないようですね.教科書の一つ『C言語によるプログラミング スーパーリファレンス編』でも使用していますし.

*3:xに型がないのは,関数形式マクロならではです.ところで最近知ったのですが,int型を引数・戻り値とするライブラリ関数absはstdlib.hで,double型を引数・戻り値とするfabsはmath.hでそれぞれ定義されているのですね.

*4:以下を読む前に,このあとを「え? そんなことないよ」と思った人へ:はい,問題ありません.意図的な誤解です.

*5:「- -1.2」について,右側のマイナスは「-1.2」という数値リテラルの一部で,左側のマイナスは,「-1.2」をオペランドにとり符号反転したものを評価値とする単項演算子です.Cでは,カッコなしでも正しい式です.

*6:22.6 動的略語展開.なお,Emacs の略語展開のメモ — ありえるえりあも興味深いですが,授業では演習室の「素の」Emacsで利用可能な操作に限定したいため,他のelispの導入は考えていません.

*7:Emacsが単語と判定するものを探しますので,ダブルクォートでくくった文字列の中に単語があっても…Cのマクロと異なり…見つけて補完してくれます.

*8:「コードを書かないことが根本的な解決法だ」というのは,行きすぎです.とさせてください.