Cでのマクロと関数


100

マクロを使用する方が関数を使用するよりも優れている例やケースを見てきました。

関数と比較したマクロの欠点を例を挙げて誰かが私に説明できますか?


21
質問を頭に入れてください。マクロはどのような状況で優れていますか?マクロの方が優れていることを実証できない限り、実際の関数を使用してください。
デビッドヘ

回答:


112

マクロはテキスト置換に依存し、型チェックを実行しないため、エラーが発生しやすくなります。たとえば、次のマクロ:

#define square(a) a * a

整数と一緒に使用すると正常に動作します。

square(5) --> 5 * 5 --> 25

しかし、式で使用すると非常に奇妙なことをします:

square(1 + 2) --> 1 + 2 * 1 + 2 --> 1 + 2 + 2 --> 5
square(x++) --> x++ * x++ --> increments x twice

引数を括弧で囲むことは役立ちますが、これらの問題を完全に排除するわけではありません。

マクロに複数のステートメントが含まれている場合、制御フロー構造で問題が発生する可能性があります。

#define swap(x, y) t = x; x = y; y = t;

if (x < y) swap(x, y); -->
if (x < y) t = x; x = y; y = t; --> if (x < y) { t = x; } x = y; y = t;

これを修正するための通常の戦略は、ステートメントを「do {...} while(0)」ループ内に置くことです。

たまたま名前が同じでセマンティクスが異なるフィールドを含む2つの構造がある場合、同じマクロが両方で機能し、奇妙な結果になることがあります。

struct shirt 
{
    int numButtons;
};

struct webpage 
{
    int numButtons;
};

#define num_button_holes(shirt)  ((shirt).numButtons * 4)

struct webpage page;
page.numButtons = 2;
num_button_holes(page) -> 8

最後に、次の例のようにデバッガーはマクロをステップ実行できないため、マクロはデバッグが困難で、奇妙な構文エラーまたはランタイムエラーを生成して理解する必要があります(gcc -Eなど)。

#define print(x, y)  printf(x y)  /* accidentally forgot comma */
print("foo %s", "bar") /* prints "foo %sbar" */

インライン関数と定数は、マクロに関するこれらの問題の多くを回避するのに役立ちますが、常に適用できるとは限りません。意図的に多態性の振る舞いを指定するためにマクロが使用されている場合、意図しない多態性を回避するのが難しい場合があります。C ++には、テンプレートを使用するなど、マクロを使用せずにタイプセーフな方法で複雑なポリモーフィックコンストラクトを作成するのに役立つ多くの機能があります。詳細については、StroustrupのC ++プログラミング言語を参照してください。


43
C ++広告とは何ですか?
Pacerier 2013

4
同意します。これはCの質問です。バイアスを追加する必要はありません。
ideasman42

16
C ++はCの拡張であり、Cのこの特定の制限に対処することを目的とした機能を(とりわけ)追加します。私はC ++のファンではありませんが、ここでは話題になっていると思います。
D Coetzee 2014

1
マクロ、インライン関数、およびテンプレートは、パフォーマンスを向上させるためによく使用されます。これらは過度に使用され、コードの膨張によりパフォーマンスを低下させる傾向があり、CPU命令キャッシュの有効性が低下します。これらの手法を使用しなくても、Cで高速な汎用データ構造を作成できます。
Sam Watkins、2015年

1
ISO / IEC 9899:1999§6.5.1によると、「オブジェクトの前と次のシーケンスポイントの間では、式の評価によって、オブジェクトに格納された値が最大で1回変更されます。」(似たような言い回しは以前のC規格にも後続のC規格にも存在します。)したがって、式x++*x++x2倍に増えるとは言えません。実際には、未定義の動作を呼び出します。つまり、コンパイラは自由に何でも実行できますx。つまり、2回、1回、またはまったく増加しない可能性があります。エラーで中断したり、悪魔が鼻から飛び出すことさえあります
サイコノート2015

38

マクロ機能

  • マクロは前処理されています
  • タイプチェックなし
  • コード長の増加
  • マクロの使用は副作用につながる可能性があります
  • 実行速度が速い
  • コンパイル前のマクロ名はマクロ値に置き換えられます
  • 小さなコードが何度も現れる場合に便利です
  • マクロがコンパイルエラーをチェックしない

機能の特徴

  • 関数がコンパイルされました
  • 型チェックが完了しました
  • コード長は同じまま
  • 副作用 なし
  • 実行速度が遅い
  • 関数呼び出し中に、制御の転送が行われます
  • 大きなコードが何度も現れる場合に便利です
  • 関数チェックのコンパイルエラー

2
「実行速度が速い」参照が必要です。過去10年間のいくらか有能なコンパイラであっても、パフォーマンス上の利点を提供すると考えれば、関数をインライン化できます。
Voo

1
低レベルMCU(AVR、つまりATMega32)コンピューティングのコンテキストでは、関数呼び出しのようにマクロが呼び出しスタックを拡張しないため、マクロの方が適していますか?
hardyVeles

1
@hardyVelesそうではありません。コンパイラは、AVRであっても、非常にインテリジェントにコードをインライン化できます。次に例を示します。godbolt.org
Edward

32

副作用は大きなものです。これが典型的なケースです:

#define min(a, b) (a < b ? a : b)

min(x++, y)

以下に拡張されます:

(x++ < y ? x++ : y)

x同じステートメントで2回インクリメントされます。(および未定義の動作)


複数行のマクロを書くのも面倒です:

#define foo(a,b,c)  \
    a += 10;        \
    b += 10;        \
    c += 10;

\各行の終わりにaが必要です。


マクロは、1つの式にしない限り、何も「返す」ことができません。

int foo(int *a, int *b){
    side_effect0();
    side_effect1();
    return a[0] + b[0];
}

GCCの式ステートメントを使用しない限り、マクロでそれを行うことはできません。(編集:カンマ演算子を使用することもできますが...見落としがちですが、それでも読みにくくなる可能性があります。)


操作の順序:(@ouahの厚意による)

#define min(a,b) (a < b ? a : b)

min(x & 0xFF, 42)

以下に拡張されます:

(x & 0xFF < 42 ? x & 0xFF : 42)

しかし&、よりも優先順位が低くなり<ます。したがって0xFF < 42、最初に評価されます。


5
マクロの定義でマクロの引数に括弧を付けないと、優先順位の問題が発生する可能性があります。例:min(a & 0xFF, 42)
ouah

ああそう。投稿の更新中にコメントが表示されませんでした。私もそれについて言及します。
ミスティック

14

例1:

#define SQUARE(x) ((x)*(x))

int main() {
  int x = 2;
  int y = SQUARE(x++); // Undefined behavior even though it doesn't look 
                       // like it here
  return 0;
}

一方:

int square(int x) {
  return x * x;
}

int main() {
  int x = 2;
  int y = square(x++); // fine
  return 0;
}

例2:

struct foo {
  int bar;
};

#define GET_BAR(f) ((f)->bar)

int main() {
  struct foo f;
  int a = GET_BAR(&f); // fine
  int b = GET_BAR(&a); // error, but the message won't make much sense unless you
                       // know what the macro does
  return 0;
}

に比べ:

struct foo {
  int bar;
};

int get_bar(struct foo *f) {
  return f->bar;
}

int main() {
  struct foo f;
  int a = get_bar(&f); // fine
  int b = get_bar(&a); // error, but compiler complains about passing int* where 
                       // struct foo* should be given
  return 0;
}

13

疑わしい場合は、関数(またはインライン関数)を使用してください。

ただし、ここでの回答は主にマクロの問題を説明するものであり、馬鹿げた事故が発生する可能性があるためマクロは悪であるという単純な見方はありません。
あなたは落とし穴を認識し、それらを避けることを学ぶことができます。次に、正当な理由がある場合にのみマクロを使用します。

マクロを使用する利点がある特定の例外的なケースがあり、これらには次のものが含まれます。

  • ジェネリック関数は、以下に示すように、さまざまなタイプの入力引数で使用できるマクロを持つことができます。
  • Cを使用する代わりに、可変数の引数をさまざまな関数にマップできますva_args
    例:https : //stackoverflow.com/a/24837037/432509
  • 彼らがすることができ、必要に応じて、このようなデバッグ文字列として地元の情報を含めます:
    __FILE____LINE____func__)。assertコードが不適切に使用されてもコンパイルされないように、障害発生時の事前/事後条件、または静的アサートさえチェックします(ほとんどがデバッグビルドに役立ちます)。
  • 入力引数を検査します。型のチェック、sizeofのチェック、structキャストの前にメンバーが存在するかどうかのチェックなどの入力引数のテストを行うことができます
    (多相型の場合に役立ちます)
    または、配列がある長さの条件を満たしていることを確認してください。
    参照してください:https : //stackoverflow.com/a/29926435/432509
  • 関数は型チェックを行うことが指摘されていますが、Cは値も強制します(たとえば、ints / floats)。まれに、これが問題になることがあります。入力引数に関する関数よりも厳密なマクロを作成することができます。参照してください:https : //stackoverflow.com/a/25988779/432509
  • 関数のラッパーとしての使用。場合によっては、自分自身を繰り返さないようfunc(FOO, "FOO");にすることができます。例:... 、文字列を展開するマクロを定義できますfunc_wrapper(FOO);
  • 呼び出し元のローカルスコープで変数を操作する場合、ポインターへのポインターの受け渡しは正常に正常に機能しますが、場合によっては、マクロを使用しても問題が少ないことがあります。
    (ピクセルごとの操作のための複数の変数への割り当ては、関数よりもマクロを好むかもしれない例です...ただし、inline関数はオプションである可能性があるため、コンテキストに大きく依存します)

確かに、これらの一部は標準C以外のコンパイラー拡張に依存しています。つまり、移植性の低いコードになってifdefしまうか、コードを組み込む必要があるため、コンパイラーがサポートする場合にのみ利用されます。


複数の引数のインスタンス化を回避する

マクロでのエラーの最も一般的な原因の1つであるため、これに注意してくださいx++たとえば、マクロが複数回インクリメントする可能性がある場合に渡されます)

引数の複数のインスタンス化による副作用を回避するマクロを書くことが可能です。

C11ジェネリック

squareさまざまなタイプで機能し、C11をサポートするマクロが必要な場合は、これを行うことができます...

inline float           _square_fl(float a) { return a * a; }
inline double          _square_dbl(float a) { return a * a; }
inline int             _square_i(int a) { return a * a; }
inline unsigned int    _square_ui(unsigned int a) { return a * a; }
inline short           _square_s(short a) { return a * a; }
inline unsigned short  _square_us(unsigned short a) { return a * a; }
/* ... long, char ... etc */

#define square(a)                        \
    _Generic((a),                        \
        float:          _square_fl(a),   \
        double:         _square_dbl(a),  \
        int:            _square_i(a),    \
        unsigned int:   _square_ui(a),   \
        short:          _square_s(a),    \
        unsigned short: _square_us(a))

ステートメント式

これは、GCC、Clang、EKOPath、およびIntel C ++ (MSVCではない)でサポートされているコンパイラー拡張です。

#define square(a_) __extension__ ({  \
    typeof(a_) a = (a_); \
    (a * a); })

したがって、マクロの欠点は、最初にこれらを使用することを知っておく必要があり、マクロがそれほど広くサポートされていないことです。

1つの利点は、この場合、square多くの異なるタイプに同じ関数を使用できることです。


1
"...広くサポートされています。" cl.exeでサポートされていないステートメント式ですか?(MSのコンパイラ)
ギデオン2015

1
@gideon、正しい編集された回答ですが、言及されている機能ごとに、コンパイラ機能のサポートマトリックスが必要かどうかは不明です。
ideasman42

12

パラメータとコードの型チェックが繰り返されないため、コードが膨張する可能性があります。マクロ構文は、セミコロンまたは優先順位が邪魔になる可能性のある奇妙なエッジケースをいくつも引き起こす可能性があります。ここにいくつかのマクロを実証するリンクがあります


6

マクロの1つの欠点は、マクロが展開されていないソースコードをデバッガーが読み取るため、マクロ内でデバッガーを実行することが必ずしも有用ではないことです。言うまでもなく、関数のようにマクロ内にブレークポイントを設定することはできません。


ブレークポイントは、指摘してくれてありがとう。
Hans

6

関数は型チェックを行います。これにより、安全性がさらに高まります。


6

この答えに追加..

マクロはプリプロセッサによってプログラムに直接置き換えられます(それらは基本的にプリプロセッサディレクティブであるため)。したがって、それらは必然的にそれぞれの関数よりも多くのメモリ空間を使用します。一方、関数を呼び出すと結果を返すには時間がかかるため、マクロを使用することでこのオーバーヘッドを回避できます。

また、マクロには、さまざまなプラットフォームでのプログラムの移植性に役立ついくつかの特別なツールがあります。

マクロは、関数とは異なり、引数にデータ型を割り当てる必要はありません。

全体的に、それらはプログラミングに役立つツールです。また、状況に応じてマクロ命令と関数の両方を使用できます。


3

上記の回答では、非常に重要であると考えるマクロに対する関数の利点の1つに気づきませんでした。

関数は引数として渡すことができますが、マクロはできません。

具体的な例:別の文字列内で検索する文字の明示的なリストではなく、文字が0になるまで0を返す(aへのポインター)関数を受け入れる標準の 'strpbrk'関数の代替バージョンを作成したいいくつかのテスト(ユーザー定義)に合格することがわかりました。これを行う理由の1つは、他の標準ライブラリ関数を利用できるようにするためです。句読点でいっぱいの明示的な文字列を提供する代わりに、代わりにctype.hの 'ispunct'を渡すことができます。'ispunct 'がマクロ、これは機能しません。

他にもたくさんの例があります。たとえば、比較が関数ではなくマクロによって行われる場合、それをstdlib.hの 'qsort'に渡すことはできません。

Pythonでの類似の状況は、バージョン2とバージョン3での「印刷」です(非受け渡し可能なステートメントと受け渡し可能な関数)。


1
この回答をありがとう
Kyrol

1

関数を引数としてマクロに渡すと、毎回評価されます。たとえば、最も人気のあるマクロの1つを呼び出す場合:

#define MIN(a,b) ((a)<(b) ? (a) : (b))

そのように

int min = MIN(functionThatTakeLongTime(1),functionThatTakeLongTime(2));

functionThatTakeLongTimeは5回評価されるため、パフォーマンスが大幅に低下する可能性があります

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.