Section:1 2 3 4 5 6 7 8 9 10 11 12 13 14 A B C D E

11 Macros(マクロ)

マクロは一般的に、定数を実装するため、あるいは、 関数呼び出しのオーバーヘッド無しの関数を擬似するためにC言語プログラムで使用されています。 関数を実装するために使用されているマクロはC言語プログラムのバグの頑固な 原因です。なぜなら、それらは特定の引数で呼び出されたときや 特定の文法のコンテキストで使用されたときに、意図した関数のように動作しない 可能性があるためです。

Splintは危険な実装と危険なマクロ呼び出しによって マクロを検出することによって潜在的な問題のほとんどを排除します。 マクロ定義がチェックされているか、あるいは、正常に展開されているかは、 フラグの設定と制御コメントに依存します(Section 11.3を参照)。 定型化されたマクロは多くの値の最初から最後までを繰り返すための 制御構造を定義するためにも使用されます(Section 11.4を参照)。

11.1 Constant Macros(定数マクロ)

マクロは定数を実装するために使用できます。 定数マクロに対して型チェックを得るには、constantアノテーションを使用してください。 例えば、

/*@constant null char *mstring_undefined@*/

宣言された定数は展開されず、宣言に従ってチェックされます。 nullアノテーション付きの定数は only記憶領域として使用することも可能です。

11.2 Function-like Macros(関数形式のマクロ)

関数を模倣するためにマクロを使用することは、悪名が知れわたるほどに、 危険です。 数字を2乗するこの正常に機能しないマクロを考えて見ましょう。

# define square(x) x * x

これはsquare(i)のような単純な呼び出しに対しては 正常に動作します。 しかし、副作用を持つ引数でインスタンスを生成した場合、予期しない動作をします。 例えば、square(i++)i++ * i++へ展開します。 これは誤った結果を与えるだけではなく、オペランドが評価される順番が 定義されていないので、未定義の動作となります。 (未定義の評価順序の動作を示す式がSplintによって検出される方法の詳細については Section 8.2を参照してください) 問題を修正するためには、その引数が厳密に1度だけ評価されるよう マクロを書き直すか、副作用のある引数でのマクロの呼び出しからクライアントを 防止する必要があります。

マクロによるもう1つの可能性のある問題は、 演算子の優先順位のルールによって予期しない結果が生じる可能性があることです。 インスタンス化、square(i+1)は、 i+1*i+1と等しく、 i+1の2乗の代わりに i+i+1が評価されます。 期待された動作を確実にするためには、マクロの引数は マクロの本体で使用される場所で括弧で囲まれている必要があります。

構文的に表現に等しくない場合もまた、マクロは未定義の動作をすることがあります。 次のマクロの定義を考えて見ましょう。

# define incCounts() ntotal++; ncurrent++;

1つの文として使用されない限り、これは正しく動作します。 例えば、

if (x < 3) incCounts();

は、x < 3のとき、ntotalの 値を+1しますが、しかし、ncurrentは常に+1されます。

1つの解決策はマクロを定義するためにカンマ演算子を 使用することです。

# define incCounts() (ntotal++, ncurrent++)

より複雑なマクロはdo … while構文を使用して書くことが出来ます。

# define incCounts() \
 do { ntotal++; ncurrent++; } while (FALSE)

Splintはマクロの定義の中のこれらの落とし穴を検出し、 マクロは可能な限り関数のように動作することをチェックします。 関数へのポインタとしてマクロを使用しようと試みると、 クライアントは関数がマクロによって実装されたということを伝えることが 出来るはずです。

Splintは関数に対応するマクロ定義にこれらのチェックを行います。

  • マクロへの各引数(副作用無しで宣言された、これらではない場合はSection 11.2.1を参照してください)は、副作用の引数の動作が期待通りになるよう、マクロの全ての可能な実行にて厳密に1度だけ使用される必要があります。*15(macroparamsフラグで制御されます。)
  • マクロへの引数は代入式の左辺として、あるいは、マクロテキスト内でインクリメントorデクリメント演算子のオペランドとして使用することは出来ません。なぜなら、これは非関数的な動作を生じるためです。(macroassignフラグで制御されます。)
  • 潜在的に危険なコンテキストで使用されたとき、マクロの引数は括弧で囲まれている必要があります。(macroparensフラグで制御されます。)
  • セミコロンで続いて呼び出されている場合、マクロの定義は文法的にその文と同等でなければなりません。(macrostmtフラグで制御されます。)
  • マクロの本体の型は、対応する関数の戻り値の型と一致している必要があります。もし、マクロがvoid型で宣言されている場合、本体はどの型を持ってもかまいませんが、マクロの値は使用されてはなりません。
  • マクロの定義の本体で宣言された全ての変数は、マクロ変数の名前空間でなければなりません。なので、それらはマクロが呼び出された場所(マクロの引数で使用されるかもしれない)のスコープの変数内で競合しません。デフォルトではマクロの名前空間はm_によって始まる全ての名前です。(名前空間の制御についての情報はSection 12.2を参照してください。)

呼び出し側では、マクロは他の関数呼び出しの様にチェックされます。

11.2.1 Side Effect Free Parameters(副作用の無い引数)

本当にマクロでsquareを実装したいが、 安全な方法でそれをしたいと仮定します。 これを行う1つの方法は、副作用を持つ引数で決して呼び出されない ことを要求することです。 Splintは、引数が副作用の無いためにアノテーション付けされている場合、 この制約を保持していることをチェックします。 それは、この引数に対応する式がどのような状態も変更してはならず、 そのため、それが何回評価されるのかは重要ではありません。 sefアノテーションは 引数がどんな副作用も持ってはならないことを表すために使用されます。

extern int square (/*@sef@*/ int x);
# define square(x) ((x) *(x))

今や、Splintはxが複数回使用されていても、 squareの定義をチェックして エラーを報告することはありません。

しかし、もし、squareが 副作用をもつ引数で呼び出された場合にはメッセージが報告されます。 以下のコードの一部に対して、

square (i++)

Splintは以下のメッセージを生成します。

Parameter 1 to square is declared sef, but the argument may modify: i++

マクロの定義の本体でsefマクロ引数として sefで アノテーション付けされていないマクロ引数を渡すことも、また、 エラーです。 例えば、

extern int sumsquares (int x, int y);
# define sumsquares(x,y) (square(x) + square(y))

squareが展開されるため、 xsumsquaresの定義の中で 1度しか現れていませんが、2回評価されます。

Splintが引数を評価することが副作用を持っていない と決定できる場合、引数は報告されるエラー無しにsef引数として 渡せてもかまいません。 関数呼び出しに対して、 副作用がありそうな場合、 変更句は決定するために使用可能です。*16 たくさんの偽のエラーを防ぐため、呼び出された関数が変更句を持たない場合、 sef-unconフラグがオンの場合のみ、 Splintはエラーを報告します。 正当な理由があって疑り深いプログラマはsef-unconをオンに設定する ことを主張し、sefマクロ引数 の中で使用されている制約の無い関数へ変更句を追加するでしょう。

マクロの1つの一般的な適用は、 C言語のポリモーフィズムの不足を回避することです。 代替型が使用されてもよいことを示すために、 /*@alt <type>,+@>構文(Section 4.4参照) を使用することが出来ます。 例えば、

extern int /*@alt float@*/ square (/*@sef@*/ int /*@alt float@*/ x);
# define square(x) ((x) *(x))

は、intfloatの両方に対して square を宣言しています。 しかし、戻り値の型が、実際の引数の型にかかわらず、 intあるいは floatのどちらかであることに 注意してください。 これは、実際に戻り値の型が知られているものよりも弱いです。

11.3 Controlling Macro Checking(マクロチェックの制御)

デフォルトでは、Splintは通常、マクロを展開し、 マクロが展開された後に生成されたコードをチェックします。 フラグと制御コメントはどのマクロが展開され、どれが 関数あるいは定数としてチェックされるかを制御するために使用されます。

fcn-macrosフラグがオンの場合、 Splintは引数のリストで定義された全てのマクロが関数を実装し、 それに応じてそれらをチェックすると想定します。 引数が決められたマクロは、展開されず、不明な結果と引数の型( 1つが与えられた場合は、あるいはプロトタイプ内の型を使用 ) を持つ関数としてチェックされます。 定数を定義するマクロに対しての類似のフラグはconst-macrosです。 オンの場合、引数リストの無いマクロは定数と想定され、 それに応じてチェックされます。 all-macrosフラグは fcn-macrosconst-macrosの両方を設定します。 macro-fcn-declフラグが設定されている場合、 メッセージは対応しない関数プロトタイプで引数が決められたマクロを報告します。 macro-const-declフラグが設定されている場合、 同様のメッセージは対応する定数宣言を持っていない引数の無いマクロを報告します。

前のsectionで説明したマクロのチェックは 関数あるいは定数を置換することを意図しているマクロに対してのみ 意味があります。 fcnmacrosフラグ あるいは、constmacrosフラグがオンの場合、 一般的なマクロが関数や定数としてチェックされないようにマークされる必要があり、 正常に展開されます。 関数のような動作をすることを意図していないマクロには、 /*@notfunction@*/コメント を付けて下さい。 例えば、

/*@notfunction@*/
# define forever for(;;)

notfunction で始まるマクロは通常のチェックが行われる前に正常に展開されます。 もし、構文的にセミコロンが無い文と同等ではないマクロ(例えば、新しいスコープに入ったマクロ) がnotfunctionが前に付いていない場合、 fcn-macrosconst-macrosがオンのときは 解析エラーが発生します。

11.4 Iterators(イテレータ)

異なる値がたくさんある場合について、 同じコードを実行できると便利なことが、しばしばあります。 例えば、整数の集合を表すintSetの中に 全ての要素の合計を計算したいとします。 intSetが抽象型の場合、 型の実表現に依存することなく、クライアントモジュールでこれを 行う簡単な方法はありません。 その代わり、型の実表現の一部としてそのようなメカニズムを提供することが 可能です。 我々は、たくさんの値を通してループするためのメカニズムを イテレータ(iterator)と呼びます。

C言語ではユーザ定義のイテレータを 生成するためのメカニズムを全く提供していません。 Splintは文法規則に従うコメント使用して宣言され、マクロを使用して定義された イテレータの定式化された形式をサポートします。

イテレータの宣言は値を返さない点を除き、 関数の宣言と同じです。それらは、 各繰り返しでyield引数に値を代入します。 例えば、intSet.hへ以下のように、このイテレータ宣言を追加できます。

/*@iter intSet_elements (intSet s, yield int el);@*/

yieldアノテーションは 2番目の実引数として渡された変数が、 int型のローカル変数として宣言され、 各ループの繰り返しで値に代入されることを意味します。

11.4.1 Defining Iterators(イテレータの定義)

イテレータはマクロを使用して定義されています。 ここで、以下は、intSet_elementsを定義する 1つの(特に効率的ではない)方法です。

typedef /*@abstract@*/ struct {
   int nelements;
   int *elements;
} intSet;
…
# define intSet_elements(s,m_el) \
  { int m_i; \
    for (m_i = (0); m_i <= ((s)->nelements); m_i++) { \
        int m_el = (s)->elements[(m_i)];

# define end_intSet_elements }}

ループを通るたびに yield引数m_elが 次の値に代入されます。 各値が1往復に対してm_elに代入された後、 ループは終了します。 イテレータマクロによって宣言された変数 (yield引数を含む) はイテレータが使用される場所のスコープ内で定義された 変数との名前の衝突を防ぐため、マクロ変数名前空間プレフィックス m_Section 11.2参照)が付いています。

11.4.2 Using Iterators(イテレータの使用)

イテレータを使用するための一般的な構文は、

iter (<params>) stmt; end_iter

例えば、クライアントはintSetの要素の合計を 計算するためにintSet_elementsを 使用することが出来ます:

intSet s;
int sum = 0;
...
intSet_elements (s, el) {
   sum += el;
} end_intSet_elements;

yield引数に対応する実引数、 el、は、 関数内で宣言されていません。 その代わり、イテレータ内で宣言され、 各繰り返しに対して適切な値が代入されます。

Splintは定式化されたイテレータの使用に対して 次のチェックを行います:

  • イテレータiterの呼び出しは、end_iterと名づけられた対応する結び(end)でブロック化されている必要があります。
  • 全ての実引数は、yield引数に対応するものを除き、定義されている必要があります。
  • yield引数は新しい識別子でなくてはなりません。現在のスコープあるいは、他のどの外部のスコープでも宣言されていてはなりません。

イテレータを実装するのは少し厄介ですが、 クライアントコードをコンパクトで容易に理解できるようにします。 抽象コレクション型に対しては、クライアントがイテレータはデータの抽象化を 壊すことなく、コレクションの要素を操作できるようにするために 使用可能です。

このドキュメントはSplint(英)のサイトを元に作成しました