C言語の高度なマクロ機能を活用したプログラミング
本稿ではC言語のマクロ機能について、高度な使い方をご紹介します。はじめに、関数形式マクロについて、ありがちなミスの回避方法に焦点を当てながら、説明します。次に、#および##演算子がどのように解釈されるかを示します。そして、do{}while(0)というトリッキーな記述についてもご紹介します。最後に、#ifまたは#ifdefのどちらが条件付きコンパイルのために好ましいか、という話題に触れます。
関数形式マクロの問題
関数形式マクロは、一見シンプルで単純な構造に見えます。しかし、実際に使ってみると、たくさんの欠点に気づくでしょう。ここで、いくつかの具体例と、その例が示す問題点、それらの解決策を示します。
マクロのパラメータをいつもカッコでくくる
まずシンプルなマクロを見てみましょう。
#define TIMES_TWO(x) x * 2
単純な使用においては、これで問題ないです。例えば、TIMES_TWO(4)と記述すれば、それは4*2に展開され、結果として8となります。一方、TIMES_TWO(4+5)は18になるでしょうか?なりません。“4 + 5 * 2”と展開され、結果14と解釈されるからです。
解決策は常にパラメータにカッコを付けることです。例えば以下です。
#define TIMES_TWO(x) (x) * 2
マクロ(式の結果)はカッコでくくる
次に下記のマクロがあるとします。
#define PLUS1(x) (x) + 1
ここで、パラメータxの周りにはカッコが正しく付いています。このマクロは何通りかの場合には、うまく動作します。例えば、次の例は11を出力します。
printf("%d\n", PLUS1(10));
しかし、次の場合は22ではなく21を出力します。
printf("%d\n", 2 * PLUS1(10));
何が起きたのでしょう?おさらいしますが、プリプロセッサはマクロを単純に展開するのみです。このため、以下のように展開されます。
printf("%d\n", 2 * (10) + 1);
明らかに、これは期待する展開ではありません。解決策は、マクロの定義全体もカッコでくくることです。
#define PLUS1(x) ((x) + 1)
こうすれば、プリプロセッサは下記のようにマクロを展開し、期待どおり22が出力されます。
printf("%d\n", 2 * ((10) + 1));
副作用を含むパラメータを持つマクロ
以下のマクロと典型的な使用例を見てみましょう。
#define SQUARE(x) ((x) * (x))
printf("%d\n", SQUARE(++i));
この記述をしたユーザは恐らくiを1インクリメントして、二乗した値を出力することを期待しているでしょう。しかし、実際には以下のように展開されます。
printf("%d\n", ((++i) * (++i)));
問題は、パラメータが使用される箇所ごとに副作用が働く点です。このため、経験則として、各パラメータは一度だけ評価されるようにすべきです。それが難しければ、注意書きをすることで、ユーザが驚くようなことは回避できるでしょう。
マクロの各パラメータが一度だけ使用されるように記述できないなら、何ができるでしょうか?直接的な回答としては、マクロの使用を避けるということです。C++ではインライン関数がサポートされていますし、最近のC言語でも広くサポートされています。インライン関数はマクロ形式関数が持つ、パラメータの副作用に関する懸念を含まず、同様の動作をしてくれます。加えて、コンパイラにとって、インライン関数は型情報を含まないマクロより警告を出すのが容易です。
マクロだけの特別な仕様
文字列化演算子#
#は関数形式マクロでだけ使用可能な演算子で、パラメータを文字列に変換します。はじめは単純に思えるのですが、深く考えずに使用すると驚くことになります。
以下の例は期待どおり動きます。
#define NAIVE_STR(x) #x
puts(NAIVE_STR(10)); /* This will print "10". */
期待したように動かない例は以下のような記述です。
#define NAME Anders
printf("%s", NAIVE_STR(NAME)); /* Will print NAME. */
2つ目の例はNAMEを出力し、NAMEがマクロで定義するAndersではありません。この場合はどうすれば良いでしょう?一般的な解決策は次になります。
#define STR_HELPER(x) #x
#define STR(x) STR_HELPER(x)
この奇妙な構造の裏には、STR(NAME)の展開時に、STR_HELPER(NAME)の中に展開されるということがあります。そして、STR_HELPERが展開される前、NAMEがまず展開されます。関数形式マクロSTR_HELPERがパラメータとともに使用されるとき、パラメータはAndersとして渡されます。
トークン連結演算子##
##はマクロで使用できる演算子で、対象を連結することができます。例えば、以下のトークンがあるとします。
MinTime, MaxTime, TimeCount.
MinSpeed, MaxSpeed, SpeedCount.
AVERAGEというマクロを定義してみましょう。このマクロには1つパラメータを取り、平均時間、平均速度などを返すとします。単純な方法として、以下のようになります。
#define NAIVE_AVERAGE(x)
(((Max##x) - (Min##x)) / (x##Count))
こちらは概ね上手く動作します。
NAIVE_AVERAGE(Time);
例えば、上記は次のように展開されます。
return (((MaxTime) - (MinTime)) / (TimeCount));
しかし、#の例と同様に、以下の記述では期待どおり動きません。
#define TIME Time
NAIVE_AVERAGE(TIME)
残念ながら以下のように展開されるからです。
return (((MaxTIME) - (MinTIME)) / (TIMECount));
解決策はSTRの例と同様です。2段階にマクロを展開しましょう。典型的にはこうした連結をするマクロを定義するなら以下のようになります。
#define GLUE_HELPER(x, y) x##y
#define GLUE(x, y) GLUE_HELPER(x, y)
これでAVERAGEマクロが使用可能になります。
#define AVERAGE(x)
(((GLUE(Max,x)) - (GLUE(Min,x))) / (GLUE(x,Count)))
do {} while(0)形式のトリッキーな記述方法
Cのソースコードのようにマクロを実装するのは便利です。最初の手順としては、定数に対してのみマクロを使うことです。関数形式マクロは、文章で表現できるものや時間とともに変化する式に対して使いましょう。
見た目をそのままに、関数形式マクロの後にセミコロンを書くことを推奨します。この時、プリプロセッサは、マクロをソースコードの中で展開するだけなので、結果として生成されるプログラムが関数式マクロに続くセミコロンで驚かないようにしましょう。
例えば、
void test()
{
a_function(); /* The semicolon is not part of
A_MACRO(); the macro substitution. */
}
1つの文で構成するマクロなら単純に後続するセミコロンを省略できます。
#define DO_ONE() a_function(1,2,3)
しかし、マクロが2以上の文で構成するならどうなるでしょう?例えば、2つの関数呼び出しをしていたら?なぜ、下記の記述は悪い例なのでしょう?
#define DO_TWO() first_function(); second_function()
たしかに単純な記述であれば意図したとおり動きます。
DO_TWO();
以下のように展開されるからです。
first_function(); second_function();
しかし、1つの文を想定する記述の中で、このマクロを使用するとどうなるでしょう?
if (... test something ...)
DO_TWO();
残念ながら、以下のように展開されます。
if (... test something ...)
first_function();
second_function();
問題は、first_functionだけが、if文の結果に応じて呼び出される点です。second_functionはif文の結果とは無関係に呼び出されます。このため、2つの関数呼び出しをカッコでくくってみるとどうなるでしょう?
#define DO_TWO() \
{ first_function(); second_function(); }
残念ながらこの場合もまだうまく動作しません。その理由を見てみましょう。
以下の例を考えます。
if (... test something ...)
DO_TWO();
else
...
これは下記のように展開されます。閉じカッコの後ろにセミコロンがあります。
if (... test something ...)
{ first_function(); second_function(); };
else
...
この形式はC言語の仕様として正しくありません。if節とelse節の間には、1つのブロックまたは文しか許されないからです。そこで昔ながらのトリックである記述方法が登場します。
#define DO_TWO() \
do { first_function(); second_function(); } while(0)
上記のdo-while構文は、記述の最後にセミコロンを必要とするので、期待した形になります。do-while構文なのでループしてしまうように思えるかもしれませんが、繰り返し条件を0に設定しているので、ループ本体は1度しか実行されません。
このマクロの記述方法は、通常の関数のような見た目です。そして、if-else節の間で使用する場合、以下のようなC言語の文法として正しい形式に展開されます。
if (... test something ...)
do { first_function(); second_function(); } while(0);
else
...
まとめると、do{}while(0)のトリックは、関数形式マクロを記述する上で便利です。欠点は、マクロの定義が直観的でないことです。このため、do{}while(0)のマクロ定義を記述する際は、その目的をコメントに残すことで、この形式を目にしたことがない人でも理解できるようにすることが大事です。
#ifdefではなく#ifを使うべき理由
たいていのアプリケーションは、実行環境などによって、ソースコードの一部をコンパイル対象外とする必要があります。例えば、異なる実行環境向けのライブラリを開発する際、特定のOSやプロセッサを想定したり、テスト用のソースコードを含んだりということがあります。
#ifも#ifdefも両方ともコンパイルからソースコードの一部を対象外とするために使用可能です。
#ifdef
#ifdefを使ったソースコードは以下のような構成となります。#ifdefを使ったアプリケーションは設定用のシンボルに特別な処理を入れる必要はありません。
#ifdef MY_COOL_FEATURE
... included if "my cool feature" is used ...
#endif
#ifndef MY_COOL_FEATURE
... excluded if "my cool feature" is used ...
#endif
#if
#ifを使うとき、評価対象のシンボルは通常定義されている。参照されるシンボルは、真または偽のどちらかで、整数の1か0に対応する。
#if MY_COOL_FEATURE
... included if "my cool feature" is used ...
#endif
#if !MY_COOL_FEATURE
... excluded if "my cool feature" is used ...
#endif
もちろん、1と0以外の値もとりうる。例えば、
#if INTERFACE_VERSION == 0
printf("Hello\n");
#elif INTERFACE_VERSION == 1
print_in_color("Hello\n", RED);
#elif INTERFACE_VERSION == 2
open_box("Hello\n");
#else
#error "Unknown INTERFACE_VERSION"
#endif
上記の形式のアプリケーションは、構成を変えるためのシンボルがデフォルトの値を持つことを強制します。その場合、”defaults.h”といったヘッダファイルで対応します。アプリケーションをこのようにして設定する時、構成を変えるためのシンボルは、コマンドラインか”config.h”といったヘッダファイルで指定されます。こうしたヘッダファイルはデフォルトの構成が推奨される場合は、空のこともあります。
defaults.hの例を示します。
/* defaults.h for the application. */
#include "config.h"
/*
* MY_COOL_FEATURE -- True, if my cool feature
* should be used.
*/
#ifndef MY_COOL_FEATURE
#define
#ifと#ifdefのどちらを使うべきでしょう?
これまでのところ、どちらも同等に見えます。実際のアプリケーションを見ても、どちらも広く使われているでしょう。最初は#ifdefの方が扱いやすく見えますが、経験的には#ifの方が推奨できます。
それは#ifdefがタイプミスからユーザを守ってくれないからです。#ifdefは、シンボル名に対して、タイプミスかどうかを判別することができません。指定されたシンボルが定義済かのみ判別できます。
例えば、以下の記述はコンパイル時には何も問題を検出しません。
#ifdef MY_COOL_FUTURE /* Should be "FEATURE". */
... Do something important ...
#endif
一方で、ほとんどのコンパイラで、#ifを使えば定義されていないシンボル(タイプミス)を検出できます。また、C標準では、シンボルは値0を持つべきとされています。
#ifdefが将来問題となることがあります
#ifdefを使って設定を切り分けるアプリケーションを開発しているとしましょう。例えば、将来的にデフォルト値が変更されてもカラーをサポートするなど、あるプロパティが特定の方法で構成されることを保証したい場合はどうすればよいでしょうか?残念ながらそれはできません。
一方で、#ifを使って設定を切り分ける場合、設定に使うシンボルを特定の値にすることで、将来的なデフォルト値の変更にも対応できます。
#ifdefはデフォルト値が名前を示します
#ifdefがアプリケーションの設定を切り分けるために使用される場合、デフォルトの設定は余計なシンボルを指定しません。機能追加のためには、この方法は直感的で、例えば単純にMY_COOL_FEATUREを定義します。しかし、機能が削除されると、しばしばシンボルはDONT_USE_COLORSになります。二重否定は本当の意味でポジティブではないですよね?
二重否定を入れるとソースコードの可読性が下がります。例えば、カラーをサポートするためにincludeすべき実装が存在したとします。
#ifndef DONT_USE_COLORS
... do something ...
#endif
ささいなことに聞こえるかもしれませんが、大規模なソースコードを読む場合、きっと混乱することでしょう。このため以下の実装をおすすめします。
#if USE_COLORS
... do something ...
#endif
ソースコードを記述する際、デフォルト値を知っていなければならない
もう1つの欠点は、実装しようとする機能はデフォルトで有効かどうかを、把握し(または調べ)なければならいことです。併せて、タイプミスへの対策が#ifdefにはないことも忘れてはいけません。
#ifdefでデフォルト値を変えることはほぼ不可能
しかし、最大の欠点は、#ifdefを使用する際、アプリケーション上の全ての#ifdefを変更することなしに、デフォルト値を変更できないということです。#ifなら、デフォルト値の変更は些細なことです。デフォルト値を含むファイルを修正するだけです。
#ifdefから#ifへのマイグレーション
それでは、すでに#ifdefで実装したアプリケーションをどのようにマイグレーションすれば良いでしょうか?#ifdefから#ifへのマイグレーションはそこまで難しくありません。加えて、使用していた設定用のシンボルに下位互換を持たせることもできます。
始めに、設定用のシンボル名を決めましょう。否定語が入った名前のシンボル(例えばDONT_USE_COLORS)は、否定語が入らない形(例えばUSE_COLORS)にしましょう。それ以外のシンボルは、そのまま、または改善余地があれば軽微な修正にとどめましょう。
設定用のシンボル名をそのままにするなら、そしてユーザが値を空にするなら(例えば"#define MY_COOL_FEATURE")、コンパイルエラーが起きます。そして、ユーザに1に定義せよとメッセージを出します。
defaults.hというヘッダファイルを作り、全てのソースコードがこのファイルをインクルードするようにしましょう。ヘッダファイルの先頭で、古い#ifdefのシンボル名を新しい名前とマッピングするのも良いでしょう。
/* Old configuration variables, ensure that they still work. */
#ifdef DONT_USE_COLORS
#define USE_COLORS 0
#endif
/* Set the default. */
#ifndef USE_COLORS
#define USE_COLORS 1
#endif
そして、以下のとおり実際に参照している#ifdefの箇所を#ifに置換します。
From: | To: |
#ifdef MY_COOL_FEATURE | #if MY_COOL_FEATURE |
#ifndef MY_COOL_FEATURE | #if !MY_COOL_FEATURE |
#ifdef DONT_USE_COLORS | #if !USE_COLORS |
#ifndef DONT_USE_COLORS | #if USE_COLORS |
結果として、全ての設定用のシンボルがヘッダファイルに集約され、デフォルト値を持つようになりました。
ウェビナー(オンラインセミナー)
オンデマンドウェビナー