Home > ラーニング > セキュアコーディング > C セキュアコーディングスタンダード > 01. プリプロセッサ (PRE)
C言語でマクロ置換がどのように動作するか理解することは必須である。特に、##演算子を使って字句を結合したり、# 演算子を使ってマクロの引数を文字列に変換したりするコンテクストでは重要になってくる。
## 前処理演算子を使い、マクロ展開時に二つの字句を一つに結合することができる。これは字句貼り付け(token pasting)や字句連結と呼ばれる。マクロが展開されるとき、各## 演算子の両側にある二つの字句は一つの字句に結合され、## および二つのオリジナルの字句を置き換える。[FSF 05]
字句貼り付けが最も便利なのは、字句のいっぽうもしくは両方がマクロ引数によって与えられる場合である。## に並ぶ字句が引数名であるなら、引数名を実引数に置き換え、その後に ## が実行される。実引数が最初にマクロ展開されるわけではない。
文字列定数の中にある引数は置換されないが、# 前処理演算子を使うことで置換することができる。マクロ引数の先頭に # をつけて使用すると、プリプロセッサはそれを実引数の定数テキストと置き換え、文字列定数に変換する。[FSF05]
「DCL03-C. 定数式の値をテストするには静的アサートを使う」からとってきた以下の定義は、JOIN() マクロを使って字句 assertion_failed_at_line_ を __LINE__ の値に置き換えている。
#define static_assert(e) \
typedef char JOIN(assertion_failed_at_line_, __LINE__) \
[(e) ? 1 : -1]
__LINE__ は予め定義されたマクロ名であり、現在のソースファイル中の推定される行番号を表す整数定数に展開される。[ISO/IEC 9899:1999]
プログラマの意図が __LINE__ マクロを展開することであるなら(ここではそうである可能性が高いが)、以下のような JOIN() の定義は違反コードである。
#define JOIN(x, y) x ## y
__LINE__ は展開されず、文字配列は結果的に assertion_failed_at_line___LINE__ という名前になる。
マクロを展開するには、以下の解決法に示すように、二段階の回り道が必要である。
#define JOIN(x, y) JOIN_AGAIN(x, y) #define JOIN_AGAIN(x, y) x ## y
JOIN(x, y) は JOIN_AGAIN(x, y) を呼び出す。したがって、x や y がマクロである場合、## 演算子がこれらをひとつに貼りつけるまえに展開される。
## 演算子を使って字句を連結する場合、# 演算子を使ってマクロ引数を文字列に変換する場合、隣接する文字列リテラルを結合する場合には、マクロ引数を個々に括弧で囲むことはできないことに注意すること。これは、「PRE01-C. マクロ内の引数名は括弧で囲む」の例外 PRE01-EX2 に該当する。
プログラマの意図が、文字列化を行う前にマクロを展開するのであったならば、これは違反コードである。
#define str(s) #s #define foo 4 str(foo)
マクロ呼び出し str(foo) は foo に展開される。
マクロ引数の展開結果を文字列化するには、二段階のマクロを使用しなければならない。
#define xstr(s) str(s) #define str(s) #s #define foo 4
マクロ呼び出し xstr(foo) は 4 に展開される。どういうことかというと、xstr() の引数 s は普通の引数なので、まず xstr() がマクロ展開され、その後に s の部分が完全にマクロ展開される。str() の引数 s は文字列化されるため、s 自体はマクロ展開されないが、str() の引数が処理される時点ではその引数はすでに 4 にマクロ展開されているのである。
| レコメンデーション | 深刻度 | 可能性 | 修正コスト | 優先度Priority | レベル |
|---|---|---|---|---|---|
| PRE05-C | 低 | 低 | 中 | P2 | L3 |
PRE05-C. Understand macro replacement when concatenating tokens or performing stringification