9

人々はいつも、マクロは安全ではなく、引数の (直接の) 型チェックを行っていないとも言います。さらに悪いことに、エラーが発生すると、コンパイラは複雑で理解できない診断を行います。これは、マクロが混乱しているためです。

安全な型チェックを行い、典型的な落とし穴を回避し、コンパイラが適切な診断を行う方法で、関数とほぼ同じ方法でマクロを使用することは可能ですか?

  1. 私はこの質問に肯定的に答えます (自動応答)。
  2. この問題に対して私が見つけた解決策をお見せしたいと思います。
  3. 統一された背景を持つために、標準の C99 が使用され、尊重されます。
  4. しかし(明らかに「しかし」があります)、人々が「食べる」必要があるある種の「構文」を「定義」します。
  5. この特別な構文は、理解や処理が最も簡単なだけでなく、最も簡単に記述できるようにすることを目的としており、不正なプログラムのリスクを最小限に抑え、さらに重要なこととして、コンパイラから正しい診断メッセージを取得します。
  6. 最後に、「非戻り値」マクロ (簡単なケース) と「戻り値」マクロ (簡単ではないが、より興味深いケース) の 2 つのケースを検討します。

マクロによって生じるいくつかの典型的な落とし穴を簡単に思い出してみましょう。

例 1

#define SQUARE(X) X*X
int i = SQUARE(1+5);

の意図した値i: 36。真の値i: 11 (マクロ展開あり: 1+5*1+5)。落とし穴!

(典型的な) 解決策 (例 2)

#define SQUARE(X) (X)*(X)
int i = (int) SQUARE(3.9);

の意図した値i: 15. の真の値i: 11 (マクロ展開後: (int) (3.9)*(3.9)).落とし穴!

(典型的な) 解決策 (例 3)

#define SQUARE(X) ((X)*(X))

整数と浮動小数点数で問題なく動作しますが、簡単に壊れます。

int x = 2;
int i = SQUARE(++x);

の意図した値i: 9 (理由(2+1)*(2+1)...)。の真の値i: 12 (マクロ展開: ((++x)*(++x))、これにより が得られます3*4)。落とし穴!

マクロで型チェックを行うための優れた方法は、次の場所にあります。

しかし、私はもっと欲しいです: ある種のインターフェースまたは「標準」構文、および (少数の) 覚えやすいルールです。その意図は、可能な限り機能に似たマクロを「使用できる(実装しない)」ことです。つまり、よく書かれた偽の関数です。

ある意味面白いのはなぜ?

これは、C で達成する興味深い挑戦だと思います。

それは役に立ちますか?

編集:標準 C では、ネストされた関数を定義することはできません。inlineしかし、短い ( ) 関数を他の関数の中にネストして定義できるようにしたい場合もあります。したがって、関数のようなプロトタイプ マクロを考慮に入れる可能性があります。

4

2 に答える 2

7

この回答は 4 つのセクションに分かれています。

  1. ブロック マクロの提案されたソリューション。
  2. その解決策の簡単な要約。
  3. マクロ プロトタイプの構文について説明します。
  4. 関数のようなマクロの提案されたソリューション。
  5. (重要な更新:) コードを壊しています。

(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 が宣言されています。
  • data3 行目は、マクロのすべての引数を一度に収集 する一時変数を宣言します。
  • このステップは、プログラマーが慎重に手動で記述する必要があります...しかし、これは簡単な構文であり、コンパイラーは (少なくとも) 間違った型の一時変数に割り当てられたパラメーターを拒否します。
  • (ただし、コンパイラは「逆の」代入については黙っています.x = (N), .n = (X))。

プロトタイプを宣言するにはxxMacroPrototype、2 つの引数を使用して記述します。

  1. マクロの名前。
  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 つの引数があります。

  1. 「戻り値」の型。
  2. 疑似パラメータを保持するために使用される「typedef 構造体」の名前。
  3. セミコロン (;) で区切られた (および終了した) 疑似パラメーターのリスト。

「戻り」値は、構造体の追加フィールドで、固定名:xxmacro__ret__です。
これは、安全のために構造体の最初の要素として宣言されています。次に、疑似パラメータのリストを「貼り付け」ます。

このインターフェイスを使用する場合 (このように呼び出す場合)、次の一連の規則に従う必要があります。

  1. xxFuncMacroPrototype() に 3 つのパラメーターを与えるプロトタイプ宣言を記述します (例の 2 行目)。
  2. 2 番目のパラメーターは、typedef structマクロ自体が作成する a の名前なので、気にせずそのまま使用してください (例では、この型は ですxxSUM_data)。
  3. タイプが単純にその構造体タイプである変数を定義します (例では: xxSUM_data xxsum;)。
  4. 適切な数の引数を使用して、目的のマクロを定義します#define SUM(X, Y)
  5. ( )EXPRESSION (したがって、「戻り値」) を取得するに は、マクロの本体を括弧で囲む必要があります。
  6. この括弧内では、コンマ演算子 (,) を使用して、操作と関数呼び出しの長いリストを区切ることができます。
  7. 最初に必要な操作は、マクロ 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 つのケースに対して同じアプローチに従います。

于 2013-08-25T00:22:31.110 に答える
2

マクロのような関数に対する自問自答の手法は巧妙ですが、元の「安全でない」マクロの「一般性」は提供されません。これは、任意の型を渡すことができないためです。特定のタイプで機能する場合は、代わりにインライン関数を維持する方が簡単で安全で簡単です。

inline float sum_f (float x, float y) { return x + y; }

C.11 では、新しい汎用選択演算子_Genericを使用して、引数の型を指定して適切なインライン関数を呼び出すことができるマクロを定義できます。型選択式 ( の最初の引数_Generic) を使用して型を決定しますが、式自体は評価されません。

#define SUM(X, Y) \
    _Generic ( (X)+(Y) \
             , float : sum_f(X, Y) \
             , default : sum_i(X, Y) )
于 2013-08-25T01:54:30.463 に答える