PRE00-C. 関数形式マクロよりもインライン関数やスタティック関数を使う
マクロは危険である。本物の関数と同じように使えるが、セマンティクスが異なるからである。C99 からは、C 言語仕様にインライン関数が追加されている。インライン関数とマクロのどちらでも使える場合には、インライン関数の方を使うようにすべきである。関数をインライン関数として定義すると、例えば、通常の関数呼出しの仕組みの代わりに インライン置換 が行なわれることで、処理速度の向上も期待できる。(「PRE31-C. 安全でないマクロの引数では副作用を避ける」、「PRE01-C. マクロ定義中で参照する引数名は括弧で囲む」、および「PRE02-C. マクロ置換リストは括弧で囲む」を参照のこと。)
インライン置換は、テキストの単なる置き換えではなく、また、新しい関数を作成することもない。例えば、その関数本体内で使われるマクロの展開は、関数が呼び出される時点で行われるのではなく、(ソースコード上で)関数本体が出現する位置にもとづいて行なわれる。また、識別子で参照できる定義内容は、関数本体の出現位置において有効な範囲にあるもののみである。
関数をインライン化するかどうかはローレベルな最適化の話であり、プログラマからの入力に頼らずにコンパイラが決めるべきことである。インライン関数の使用は、(a) 対象コンパイラのインライン関数サポート状況、(b) システムのパフォーマンス特性への影響(もしあれば)、(c) 可搬性、といった観点にもとづいて見極めるべきである。なお、C でサポートされておりインライン関数と同じくらい有用なものとして、スタティック関数もある。
違反コード
以下のコード例におけるマクロ CUBE()
は、副作用のある式を引数として渡されると未定義の動作となる。
#define CUBE(X) ((X) * (X) * (X))
void func(void) {
int i = 2;
int a = 81 / CUBE(++i);
/* ... */
}
この例では、a
の初期化は次のように展開される。
int a = 81 / ((++i) * (++i) * (++i));
この動作は未定義である(「EXP30-C. 副作用の評価順序に依存しない」を参照)。
適合コード
マクロ定義をインライン関数に置き換えると、副作用は関数の呼出しの直前に一回だけ実行される。
inline int cube(int i) {
return i * i * i;
}
void func(void) {
int i = 2;
int a = 81 / cube(++i);
/* ... */
}
違反コード
以下のコード例では、プログラマは EXEC_BUMP()
というマクロを定義している。このマクロは、指定した関数を呼び出し、グローバル変数をインクリメントしている[Dewhurst 2002]。この例のように、ある関数の定義のなかでマクロが展開されると、マクロの定義本体で使われている識別子は、その関数本体のスコープにある宣言を参照することになる。その結果、aFunc
関数のなかでマクロが呼び出されたとき、うかつにもグローバル変数と同じ名称のローカル変数をインクリメントしてしまう。このコード例は、「DCL01-C. サブスコープで変数名を再利用しない」にも違反している。
size_t count = 0;
#define EXEC_BUMP(func) (func(), ++count)
void g(void) {
printf("Called g, count = %zu.\n", count);
}
void aFunc(void) {
size_t count = 0;
while (count++ < 10) {
EXEC_BUMP(g);
}
}
aFunc()
を実行したときに、以下の行が (あやまって) 5回出力される結果となる。
Called g, count = 0.
適合コード
以下の適合コードでは、EXEC_BUMP()
マクロをインライン関数 exec_bump()
に置き換えている。aFunc
呼出しは今度は count
の値を 0 から 9 まで (正しく) 出力する。
size_t count = 0;
void g(void) {
printf("Called g, count = %zu.\n", count);
}
typedef void (*exec_func)(void);
inline void exec_bump(exec_func f) {
f();
++count;
}
void aFunc(void) {
size_t count = 0;
while (count++ < 10) {
exec_bump(g);
}
}
インライン関数を使うことで、識別子 count
は関数本体がコンパイルされるときにグローバル変数にバインドされる。関数呼出し時に、同じ名前を持つ別の変数にバインドされることはない。
違反コード
関数と違ってマクロの実行はインターリーブすることがある。そのため、個々には問題ないふたつのマクロが、ひとつの式のなかで組み合わされると未定義の動作を起こすことがある。以下の例では、F()
と G()
の両方がグローバル変数 operations
をインクリメントしている。これは、ふたつのマクロが一緒に使われるとき問題をひきおこす。
int operations = 0, calls_to_F = 0, calls_to_G = 0;
#define F(x) (++operations, ++calls_to_F, 2 * x)
#define G(x) (++operations, ++calls_to_G, x + 1)
void func(int x) {
int y = F(x) + G(x);
}
変数 operations
は同じ式のなかで2回、値の読み込みと変更がなされる。例えば以下のような実行順序になった場合に、間違った値になるかもしれない。
operations を レジスタ0 に読み込む
operations を レジスタ1 に読み込む
レジスタ0 をインクリメント
レジスタ1 をインクリメント
レジスタ0 を operations にストア
レジスタ1 を operations にストア
このコード例は「EXP30-C. 副作用の評価順序に依存しない」にも違反している。
適合コード
インライン関数も含め、関数の実行はインターリーブされない。そのため、問題となる実行順序になることはない。
int operations = 0, calls_to_F = 0, calls_to_G = 0;
inline int f(int x) {
++operations;
++calls_to_F;
return 2 * x;
}
inline int g(int x) {
++operations;
++calls_to_G;
return x + 1;
}
void func(int x) {
int y = f(x) + g(x);
}
プラットフォーム固有の詳細
GNU C (とその他いくつかのコンパイラ)は、C 言語仕様に追加される前からインライン関数をサポートしており、C 言語仕様と大きく異なるセマンティクスを持つ。C99 と GNU C におけるセマンティクスの違いについては、Richard Kettlewell による優れた解説がある[Kettlewell 2003]。
例外
PRE00-C-EX1: マクロを使うと、ローカル関数(そのスコープ内の自動変数にアクセスするコード)を実装できるが、これはインライン関数では実現できない。
PRE00-C-EX2: マクロを使うと、トークンの連結や文字列化を実装できる。例えば、
enum Color { Color_Red, Color_Green, Color_Blue };
static const struct {
enum Color color;
const char *name;
} colors[] = {
#define COLOR(color) { Color_ ## color, #color }
COLOR(Red), COLOR(Green), COLOR(Blue)
};
COLOR(Red)
は {Color_Red, "Red"}
に、COLOR(Green)
は {Color_Green, "Green"}
に、COLOR(Blue)
は {Color_Blue, "Blue"}
に、それぞれ展開される。詳しくは、「PRE05-C. 字句の結合や文字列化を行う際のマクロ置換動作をよく理解する」を参照のこと。
PRE00-C-EX3: マクロを使うと、コンパイル時定数を得ることができる。これは、インライン関数では必ずしも実現できるとは限らない。
#define ADD_M(a, b) ((a) + (b))
static inline int add_f(int a, int b) {
return a + b;
}
この例では、ADD_M(3,4)
マクロ呼出しは定数式を生成するが、add_f(3,4)
関数呼出しではそうならない。
PRE00-C-EX4: マクロを使えば、C 言語では実装できない (C++ のテンプレートの機能などを使わなければ実現できないような) 型総称関数の機能を実装できる。
関数形式マクロを使って型総称関数を作る例は、「MEM02-C. メモリ割り当て関数の結果は、割り当てた型へのポインタに即座にキャストする」で紹介している。
型総称マクロを使うと例えば、ふたつの同じ型の変数の値を入れ換える操作を行なうことができる。
PRE00-C-EX5: マクロの引数は名前呼出し(call-by-name)で評価されるが、関数では値呼出し(call-by-value)で評価される。名前呼出し(call-by-name)のセマンティクスが要求されるときはマクロを使う必要がある。
リスク評価
マクロの不適切な使用は未定義の動作を引き起こす可能性がある。
レコメンデーション |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
---|---|---|---|---|---|
PRE00-C |
中 |
低 |
中 |
P4 |
L3 |
自動検出(最新の情報はこちら)
ツール | バージョン | チェッカー | 説明 |
---|---|---|---|
Astrée |
18.10
|
macro-function-like | Fully checked |
Axivion Bauhaus Suite |
6.9.0 |
CertC-PRE00 |
|
1.2
|
CC2.PRE00 |
実装済み |
|
Klocwork |
2018
|
MISRA.DEFINE.FUNC |
|
LDRA tool suite |
9.7.1
|
340 S |
Enhanced enforcement |
Parasoft C/C++test |
10.4.1
|
CERT_C-PRE00-a |
A function should be used in preference to a function-like macro |
Polyspace Bug Finder |
R2018a |
MISRA C:2012 Dir 4.9 |
A function should be used in preference to a function-like macro where they are interchangeable |
PRQA QA-C |
9.5
|
3453 | 実装済み |
RuleChecker |
18.10
|
macro-function-like | Fully checked |
SonarQube C/C++ Plugin | 3.11 | S960 |
|
関連するガイドライン
SEI CERT C++ Coding Standard | VOID PRE00-CPP. Avoid defining macros |
ISO/IEC TR 24772:2013 | Pre-processor Directives [NMP] |
MISRA C:2012 | Directive 4.9 (advisory) |
参考資料
[Dewhurst 2002] | Gotcha #26, "#define Pseudofunctions" |
[FSF 2005] | Section 5.34, "An Inline Function Is as Fast as a Macro" |
[Kettlewell 2003] |
|
[Summit 2005] | Question 10.4 |
翻訳元
これは以下のページを翻訳したものです。
PRE00-C. Prefer inline or static functions to function-like macros (revision 135)