PRE31-C. 安全でないマクロの引数では副作用を避ける
「安全でない関数形式のマクロ」とは、展開されると1つの引数を2回以上評価するか、まったく評価しないようなものを指す。代入、インクリメント、デクリメント、volatile アクセス、入出力、その他副作用 (副作用を引き起こす可能性のある関数呼出しを含む) を持つ式を使って、安全でないマクロを呼び出してはならない。
安全でないマクロ定義を提供する場合、副作用を伴う引数を使った呼出しが危険であることを明記しておくべきであるが、マクロを安全に呼び出す責任はマクロを使うプログラマにある。このように使用にリスクが伴うことを考えると、安全でない関数形式マクロの作成自体を避けるべきだ(「PRE00-C. 関数形式マクロよりもインライン関数やスタティック関数を使う」を参照)。
これは「EXP44-C. sizeof、_Alignof、_Generic 演算子のオペランドには副作用を持たせない」に類似のルールである。
違反コード
安全でないマクロの問題の1つは、以下の違反コード例に示すように、引数における副作用である。
#define ABS(x) (((x) < 0) ? -(x) : (x))
void func(int n) {
/* n が想定範囲内の値であることを確認 */
int m = ABS(++n);
/* ... */
}
ABS()
マクロ呼び出しは次のように展開される。
m = (((++n) < 0) ? -(++n) : (++n));
展開後のコードは言語仕様に沿った正しいコードにはなっているが、n
は 1回ではなく 2回インクリメントされる。
適合コード
以下の適合コードでは、安全でないマクロを呼び出す前にインクリメント演算 ++n
を実行している。
#define ABS(x) (((x) < 0) ? -(x) : (x)) /* 安全でないマクロ定義 */
void func(int n) {
/* n が想定範囲内の値であることを確認 */
++n;
int m = ABS(n);
/* ... */
}
マクロが安全でないことを警告するコメントがあることに注意。マクロが安全でないことを明確にするため、マクロ名を ABS_UNSAFE()
とする方法もある。他の適合コードと同様、この適合コードにおいても、ABS()
の引数が符号付き整数型の最小値(負の数として 0 から最も遠い値)である場合には未定義の動作となる(「INT32-C. 符号付き整数演算がオーバーフローを引き起こさないことを保証する」を参照)。
適合コード
この適合コードでは、「PRE00-C. 関数形式マクロよりもインライン関数やスタティック関数を使う」にしたがって、ABS()
マクロの代わりにインライン関数 iabs()
を定義している。ABS()
マクロではどのような型のオペランドでもそのまま展開されるが、iabs()
関数に int
型より大きい型の引数を与えた場合、関数本体の処理の前に(int
型に収まるように)切り捨てが発生する可能性がある。
#include <complex.h>
#include <math.h>
static inline int iabs(int x) {
return (((x) < 0) ? -(x) : (x));
}
void func(int n) {
/* n が想定範囲内の値であることを確認 */
int m = iabs(++n);
/* ... */
}
適合コード
さらに柔軟性があるコードにするには、_Generic
選択を使う形で ABS()
マクロを宣言する。全ての算術データ型をサポートするために、インライン関数も使って絶対値の計算をしている(「PRE00-C. 関数形式マクロよりもインライン関数やスタティック関数を使う」および「PRE12-C. 安全でないマクロを定義しない」を参照)。
C言語規格 [ISO/IEC 9899:2011]、6.5.1.1 のパラグラフ3には次のような記述がある。
総称選択(generic selection)の制御式は評価されない。制御式の型に適合する型名を持つ総称関連(generic association)がある場合、総称選択の結果の式は、該当する総称関連における式となる。さもなければ、総称選択の結果の式は、
default
総称関連における式となる。その他の総称関連の式はいずれも評価されない。
総称選択において式の評価は行われないため、この適合コードにおいてマクロ引数 v
は一度だけ評価されることが保証されている。
#include <complex.h>
#include <math.h>
static inline long long llabs(long long v) {
return v < 0 ? -v : v;
}
static inline long labs(long v) {
return v < 0 ? -v : v;
}
static inline int iabs(int v) {
return v < 0 ? -v : v;
}
static inline int sabs(short v) {
return v < 0 ? -v : v;
}
static inline int scabs(signed char v) {
return v < 0 ? -v : v;
}
#define ABS(v) _Generic(v, signed char : scabs, \
short : sabs, \
int : iabs, \
long : labs, \
long long : llabs, \
float : fabsf, \
double : fabs, \
long double : fabsl, \
double complex : cabs, \
float complex : cabsf, \
long double complex : cabsl)(v)
void func(int n) {
/* nが想定範囲内の値であることを確認 */
int m = ABS(++n);
/* ... */
}
総称選択は C11 で導入されたもので、C99 およびそれ以前の規格では利用できない。
適合コード(GCC)
GCC の __typeof
拡張を利用すると、引数に対して同じ型の一時変数を宣言し引数の値を使った計算を行うマクロを宣言することができ、引数が一度だけ評価されることを保証できる。また、別の GCC 拡張 statement expression を使うと、式が期待される場所にブロック文を置くことができる。
#define ABS(x) __extension__ ({ __typeof (x) tmp = x; \
tmp < 0 ? -tmp : tmp; })
このような拡張機能を使うと可搬性が損われ、「MSC14-C. 必要もなくコードをプラットフォーム依存にしない」に違反することに注意。
違反コード(assert()
)
assert()
マクロはコードに診断テストの機能を取り込むための便利な機能である(「MSC11-C. アサートを使って診断テストを組み込む」を参照)。assert()
マクロの引数には、副作用を持つ式を使用すべきではない。NDEBUG
マクロの定義に依存する。NDEBUG
マクロが定義されていない場合、assert()
マクロは引数式を評価し、その結果が 0 と等しければ abort()
関数を呼び出す。一方、NDEBUG
が定義されている場合には、assert()
は ((void)0)
に展開される。この場合 assert
中の式は評価されないため、副作用を持っている式だったとしても、デバッグ以外の実行中に発生したはずの副作用は発生しない。
次の違反コード例では、副作用を持つ式(index++
)が assert()
マクロの引数に使われている。
#include <assert.h>
#include <stddef.h>
void process(size_t index) {
assert(index++ > 0); /* 副作用 */
/* ... */
}
適合コード(assert()
)
この適合コードでは、副作用を持つ式を assert()
の外に移動し、assert()
マクロの評価時に副作用が発生する可能性を回避している。
#include <assert.h>
#include <stddef.h>
void process(size_t index) {
assert(index > 0); /* 副作用なし */
++index;
/* ... */
}
例外
PRE31-C-EX1: 副作用を持たない関数については、その関数呼出しを行う式を安全でないマクロの引数として使ってもよい。しかし、ソースコードが公開されていないライブラリ関数など、関数が持っているかもしれない副作用は容易に見逃しうる。たとえば、errno
の変更も副作用として考えなければならない。開発者自身が書いた関数であって、他の関数を呼び出さずに計算を行ってその結果を返すような場合でなければ、開発者の多くは副作用が存在することを見落してしまうだろう。したがって、この例外の適用は十分に注意して行われなければならない。
リスク評価
副作用を持つ引数を使って安全でないマクロを呼び出すと、マクロの評価にしたがって副作用が何度も発生し、予期しないプログラム動作になる可能性がある。
ルール |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
---|---|---|---|---|---|
PRE31-C |
低 |
低 |
低 |
P3 |
L3 |
自動検出(最新の情報はこちら)
ツール |
バージョン |
チェッカー |
説明 |
---|---|---|---|
Axivion Bauhaus Suite |
6.9.0 |
CertC-PRE31 | 実装済み |
2017.07
|
ASSERT_SIDE_EFFECTS |
一部実装済み 副作用がある演算や関数呼出しを含むアサーションを検出できる |
|
ECLAIR |
1.2
|
CC2.EXP31 CC2.PRE31 |
実装済み |
LDRA tool suite |
9.7.1
|
9 S, 562 S, 572 S, 35 D, 1 Q |
実装済み |
Parasoft C/C++test |
10.4.1
|
CERT_C-PRE31-a |
A full expression containing an increment (++) or decrement (--) operator should have no other potential side effects |
Polyspace Bug Finder |
R2018a |
MISRA C:2012 Rule 13.2 |
The value of an expression and its persistent side effects shall be the same under all permitted evaluation orders |
PRQA QA-C |
9.5
|
3462, 3463, 3464, 3465, 3466, 3467 |
実装済み |
PRQA QA-C++ |
4.3 |
3225, 3226, 3227, 3228, 3229 |
|
関連するガイドライン
Taxonomy |
Taxonomy item |
Relationship |
---|
参考資料
[Dewhurst 2002] | Gotcha #28, "Side Effects in Assertions" |
[ISO/IEC 9899:2011] | Subclause 6.5.1.1, "Generic Selection" |
[Plum 1985] | Rule 1-11 |
翻訳元
これは以下のページを翻訳したものです。
PRE31-C. Avoid side effects in arguments to unsafe macros (revision 158)