この回答は 4 つのセクションに分かれています。
- ブロック マクロの提案されたソリューション。
- その解決策の簡単な要約。
- マクロ プロトタイプの構文について説明します。
- 関数のようなマクロの提案されたソリューション。
- (重要な更新:) コードを壊しています。
(1.) 最初のケース。ブロック マクロ (または値を返さないマクロ)
最初に簡単な例を考えてみましょう。整数の 2 乗を出力し、その後に '\n' を付ける「コマンド」が必要だとします。マクロで実装することにしました。しかし、コンパイラによって引数が として検証されることを望みますint
。私達は書く:
#define PRINTINT_SQUARE(X) { \
int x = (X); \
printf("%d\n", x*x); \
}
- 括弧で囲まれている
(X)
ため、ほとんどすべての落とし穴が回避されます。
- さらに、括弧は、コンパイラが構文エラーを適切に診断するのに役立ちます。
- マクロ パラメーター
X
は、マクロ内で 1 回だけ呼び出されます。これにより、質問の例 3 の落とし穴が回避されます。
- の値
X
はすぐに変数に保持されますx
。
x
マクロの残りの部分では、代わりに変数を使用しますX
。
- [重要な更新:] (このコードは壊れている可能性があります: セクション5を参照してください)。
この規律を体系化すれば、マクロの典型的な問題は回避されます。
さて、このようなものは正しく9を出力します:
int i = 3;
PRINTINT_SQUARE(i++);
明らかに、このアプローチには弱点がある可能性がありますx
。マクロ内で定義された変数は、プログラム内の他の変数とも競合する可能性がありますx
。これはスコープの問題です。ただし、マクロ本体は で囲まれたブロックとして記述されているので問題ありません{ }
。これは、すべてのスコープの問題を処理するのに十分であり、「内部」変数に関するすべての潜在的な問題に対処しx
ます。
x
変数は追加のオブジェクトであり、望ましくない可能性があると主張できます。しかし、x
一時的な持続時間しかありません: マクロの開始時に作成され、開始時にマクロ{
が終了し、終了時に破棄され}
ます。このように、x
これは関数パラメーターとして機能します。パラメーターの値を保持するために一時変数が作成され、マクロが「戻る」ときに最終的に破棄されます。関数がまだ行っていない罪を犯していません!
さらに重要: プログラマーが間違ったパラメーターでマクロを「呼び出そうと」すると、コンパイラーは、同じ状況で 関数が与えるのと同じ診断を行います。
これで、すべてのマクロの落とし穴が解決されたようです。
ただし、ここでわかるように、少し構文上の問題があります。
do {} while(0)
したがって、ブロックのようなマクロ定義に構造 を追加することが不可欠です (私は言います) :
#define PRINTINT_SQUARE(X) do { \
int x = (X); \
printf("%d\n", x*x); \
} while(0)
さて、これdo { } while(0)
は問題なく機能しますが、美的ではありません。問題は、プログラマにとって直感的な意味がないことです。次のような意味のあるアプローチを使用することをお勧めします。
#define xxbeg_macroblock do {
#define xxend_macroblock } while(0)
#define PRINTINT_SQUARE(X) \
xxbeg_macroblock \
int x = (X); \
printf("%d\n", x*x); \
xxend_macroblock
}
( inを含めることで、xxend_macroblock
とのあいまいさが回避されwhile(0)
ます)。もちろん、この構文はもはや安全ではありません。誤用を避けるために、慎重に文書化する必要があります。次の見苦しい例を考えてみましょう:
{ xxend_macroblock printf("Hello");
(2.) 要約
値を返さないブロック定義マクロは、規律あるスタイルに従って記述すれば、関数のように動作できます。
#define xxbeg_macroblock do {
#define xxend_macroblock } while(0)
#define MY_BLOCK_MACRO(Par1, Par2, ..., ParN) \
xxbeg_macroblock \
desired_type1 temp_var1 = (Par1); \
desired_type2 temp_var2 = (Par2); \
/* ... ... ... */ \
desired_typeN temp_varN = (ParN); \
/* (do stuff with objects temp_var1, ..., temp_varN); */ \
xxend_macroblock
- マクロの呼び出し
MY_BLOCK_MACRO()
はステートメントであり、式ではありません。いかなる種類の「戻り値」も存在しませんvoid
。
- マクロ パラメーターは、マクロの開始時に 1 回だけ使用し、それらの値をブロック スコープの実際の一時変数に渡す必要があります。マクロの残りの部分では、これらの変数のみを使用できます。
(3.) マクロのパラメーターのインターフェースを提供できますか?
パラメーターの型チェックの問題は解決しましたが、プログラマーはパラメーターが「持っている」型を理解できません。ある種のマクロ プロトタイプを提供する必要があります。これは可能であり、非常に安全ですが、少しトリッキーな構文といくつかの制限も許容する必要があります。
次の行が何をするのか理解できますか?
xxMacroPrototype(PrintData, int x; float y; char *z; int n; );
#define PrintData(X, Y, Z, N) { \
PrintData data = { .x = (X), .y = (Y), .z = (Z), .n = (N) }; \
printf("%d %g %s %d\n", data.x, data.y, data.z, data.n); \
}
PrintData(1, 3.14, "Hello", 4);
- 1 行目は、マクロのプロトタイプを「定義」し
PrintData
ます。
- 以下では、関数のようなマクロ PrintData が宣言されています。
data
3 行目は、マクロのすべての引数を一度に収集 する一時変数を宣言します。
- このステップは、プログラマーが慎重に手動で記述する必要があります...しかし、これは簡単な構文であり、コンパイラーは (少なくとも) 間違った型の一時変数に割り当てられたパラメーターを拒否します。
- (ただし、コンパイラは「逆の」代入については黙っています
.x = (N), .n = (X)
)。
プロトタイプを宣言するにはxxMacroPrototype
、2 つの引数を使用して記述します。
- マクロの名前。
マクロ内で使用される「ローカル」変数の型と名前のリスト。この項目を呼び出します:マクロの 疑似パラメータ。
疑似パラメータのリストは、セミコロン (;) で区切られた (および終了した) 型と変数のペアのリストとして記述される必要があります。
マクロの本体では、最初のステートメントは次の形式の宣言になります。
MacroName foo = { .pseudoparam1 = (MacroPar1), .pseudoparam2 = (MacroPar2), ..., .pseudoparamN = (MacroParN) }
- マクロ内では、疑似パラメータは 、 などとして呼び出され
foo.pesudoparam1
ますfoo.pseudoparam2
。
xxMacroPrototype() の定義は次のとおりです。
#define xxMacroPrototype(NAME, ARGS) typedef struct { ARGS } NAME
シンプルですね。
- 疑似パラメータは として実装されます
typedef struct
。
- ARGS は、適切に構成された型と識別子のペアのリストであることが保証されています。
- コンパイラがわかりやすい診断を提供することが保証されています。
- 擬似パラメータのリストには、
struct
宣言と同じ制限があります。(たとえば、可変サイズの配列のみをリストの最後に置くことができます)。(特に、疑似パラメータとして可変サイズの配列宣言子 の代わりにポインタ先を使用することをお勧めします。)
- NAME が実際のマクロ名であるという保証はありません (しかし、この事実はあまり関係ありません)。
重要なのは、マクロのパラメーターリストに関連付けられた構造体型が「そこに」定義されていることを知っていることです。
- ARGS によって提供される疑似パラメータのリストが、実際のマクロの引数のリストと実際に何らかの形で一致することは保証されていません。
- プログラマがマクロ内でこれを正しく使用することは保証されていません。
- struct-type 宣言のスコープ
xxMacroPrototype
は、呼び出しが行わ れるポイントと同じです。
- マクロ プロトタイプをまとめ、その後すぐに対応するマクロ定義を作成することをお勧めします。
ただし、その種の宣言は簡単に規律され、プログラマーがルールを尊重するのは簡単です。
ブロックマクロは値を「返す」ことができますか?
はい。実際には、参照によって引数を渡すだけで、必要な数の値を取得できますscanf()
。
しかし、あなたはおそらく別のことを考えているでしょう:
(4.) 2 番目のケース。関数のようなマクロ
それらについては、返される値の型を含む、マクロ プロトタイプを宣言するための少し異なるメソッドが必要です。また、必要な型の戻り値を使用して、ブロック マクロの安全性を維持できる (難しくない) 手法を習得する必要があります。
引数の型チェックは、次のように実行できます。
NAME
ブロック マクロでは、マクロ自体のすぐ内側でstruct 変数を宣言できる
ため、プログラムの残りの部分からそれを隠しておくことができます。関数のようなマクロの場合、これは実行できません (標準の C99 では)。NAME
マクロを呼び出す前に、タイプの変数を定義する必要があります。この価格を支払う準備ができている場合は、特定の型の値を返すことで、目的の「安全な関数のようなマクロ」を獲得できます。
例を示してコードを示し、コメントします。
#define xxFuncMacroPrototype(RETTYPE, MACRODATA, ARGS) typedef struct { RETTYPE xxmacro__ret__; ARGS } MACRODATA
xxFuncMacroPrototype(float, xxSUM_data, int x; float y; );
xxSUM_data xxsum;
#define SUM(X, Y) ( xxsum = (xxSUM_data){ .x = (X), .y = (Y) }, \
xxsum.xxmacro__ret__ = xxsum.x + xxsum.y, \
xxsum.xxmacro__ret__)
printf("%g\n", SUM(1, 2.2));
最初の行は、関数マクロ プロトタイプの「構文」を定義します。
このようなプロトタイプには 3 つの引数があります。
- 「戻り値」の型。
- 疑似パラメータを保持するために使用される「typedef 構造体」の名前。
- セミコロン (;) で区切られた (および終了した) 疑似パラメーターのリスト。
「戻り」値は、構造体の追加フィールドで、固定名:xxmacro__ret__
です。
これは、安全のために構造体の最初の要素として宣言されています。次に、疑似パラメータのリストを「貼り付け」ます。
このインターフェイスを使用する場合 (このように呼び出す場合)、次の一連の規則に従う必要があります。
- xxFuncMacroPrototype() に 3 つのパラメーターを与えるプロトタイプ宣言を記述します (例の 2 行目)。
- 2 番目のパラメーターは、
typedef struct
マクロ自体が作成する a の名前なので、気にせずそのまま使用してください (例では、この型は ですxxSUM_data
)。
- タイプが単純にその構造体タイプである変数を定義します (例では:
xxSUM_data xxsum;
)。
- 適切な数の引数を使用して、目的のマクロを定義します
#define SUM(X, Y)
。
( )
EXPRESSION (したがって、「戻り値」) を取得するに は、マクロの本体を括弧で囲む必要があります。
- この括弧内では、コンマ演算子 (,) を使用して、操作と関数呼び出しの長いリストを区切ることができます。
- 最初に必要な操作は、マクロ SUM(X,Y) の引数 X、Y をグローバル変数 に「渡す」ことです
xxsum
。これは次の方法で行われます。
xxsum = (xxSUM_data){ .x = (X), .y = (Y) },
C99 構文によって提供される複合リテラルを使用して、型のオブジェクトが空中xxSUM_data
で作成されることに注意してください。このオブジェクトのフィールドは、マクロの引数 X、Y を 1 回だけ読み取ることによって埋められ、安全のために括弧で囲まれます。
次に、コンマ演算子 (,) で区切られた式と関数のリストを評価します。
最後に、最後のコンマの後に を書きます。これは、コンマ式の最後の項と見なされ、マクロの「戻り値」となります。
xxsum.xxmacro__ret__
なんでそんなもの?なぜtypedef struct
ですか?構造体を使用する方が、個々の変数を使用するよりも優れています。これは、情報がすべて 1 つのオブジェクトにパックされ、データがプログラムの残りの部分に対して隠されているためです。プログラム内の各マクロの引数を保持するために「たくさんの変数」を定義したくありません。代わりに、体系的にマクロに関連付けて定義することで、そのtypedef struct
ようなマクロをより簡単に扱うことができます。
上記の「外部変数」xxsum を回避できますか? 複合リテラルは左辺値であるため、これが可能であると信じることができます。
実際、次のように、この種のマクロを定義できます。
しかし、実際には、安全な方法で実装する方法を見つけることができません。
たとえば、上記のマクロ SUM(X,Y) は、このメソッドだけでは実装できません。
(構造体へのポインター + 複合リテラルでいくつかのトリックを作成しようとしましたが、不可能のようです)。
アップデート:
(5.) 私のコードを破る。
セクション 1 で示した例は、次のように分割できます (以下の Chris Dodd のコメントで示したように)。
int x = 5; /* x defined outside the macro */
PRINTINT_SQUARE(x);
マクロの内部には x という名前の別のオブジェクトがあるため ( this: int x = (X);
、X
はマクロの仮パラメーターPRINTINT_SQUARE(X)
)、引数として実際に「渡される」のは、マクロの外部で定義された「値」 5 ではなく、別のものです: ガベージ価値。
それを理解するために、マクロ展開後に上記の 2 行を展開してみましょう。
int x = 5;
{ int x = (x); printf("%d", x*x); }
ブロック内の変数x
が初期化されます...独自の未定値に!
一般に、ブロック マクロのセクション 1 ~ 3 で開発された手法は、同様の方法で破ることができますが、パラメーターを保持するために使用する構造体オブジェクトはブロック内で宣言されます。
これは、この種のコードが壊れる可能性があることを示しているため、安全ではありません。
パラメータを保持するためにマクロの「内部」で「ローカル」変数を宣言しようとしないでください。
- 「解決策」はありますか?私は「はい」と答えます: ブロック マクロ (セクション 1 から 3 で開発したように) の場合にこの問題を回避するには、関数のようなマクロに対して行ったことを繰り返す必要があると思います。行の直後の、マクロの外側の hold-parameters 構造体
xxMacroPrototype()
。
これはそれほど野心的ではありませんが、とにかく、「どのくらい可能ですか...?」という質問に答えます。一方、ここでは、ブロックと関数のようなマクロの 2 つのケースに対して同じアプローチに従います。