C言語では、同じような処理を何度も書かずに済むようにするために「関数」を使いますが、コンパイル前の置き換え機能であるマクロを使って、見た目が関数そっくりの仕組みを作ることもできます。
これを「引数付きの関数風マクロ」と呼びます。
本記事では、C言語初心者の方に向けて、関数風マクロの基本から注意点、関数との使い分けまでをやさしく解説します。
引数付きの関数風マクロとは
関数風マクロの基本
関数風マクロとは、関数のように引数を書いて呼び出せるマクロのことです。
通常のマクロは単なる置き換えですが、関数風マクロは値を受け取り、その値を使った式に展開されます。
通常マクロと関数風マクロのイメージの違い
通常のマクロは、例えば次のような形です。
#define PI 3.14159 // 通常のマクロ(引数なし)
これはPIという文字列を3.14159に置き換えるだけの仕組みです。
一方、関数風マクロは次のように定義します。
#define SQUARE(x) ((x) * (x)) // 引数付きの関数風マクロ
SQUARE(5)と書くと、コンパイル前に((5) * (5))というコードに単純に置き換えられます。
ここで「単純に置き換え」という点が関数との大きな違いにつながります。
関数との違い
関数と関数風マクロは、見た目はよく似ていますが、内部で行われていることはまったく違います。
実行タイミングと仕組みの違い
次の表で、ざっくりと違いを整理します。
| 項目 | 関数風マクロ | 関数 |
|---|---|---|
| 処理されるタイミング | プリプロセス(コンパイル前) | 実行時 |
| 実体 | 文字列の置き換え | 機械語としての処理本体 |
| 引数の型チェック | なし | あり(コンパイラがチェック) |
| デバッグのしやすさ | 低い | 高い |
| 評価回数の制御 | 難しい(副作用の危険) | 1回だけ評価される |
関数風マクロは「コードをそのまま貼り付ける感じ」、関数は「処理をひとまとめにして呼び出す感じ」と考えるとイメージしやすいです。
引数付きマクロを使うメリット・デメリット
メリット
文章でまとめると、引数付きの関数風マクロには次のようなメリットがあります。
まず、実行速度の面で有利になることがあります。
マクロは展開されて直接コードとして埋め込まれるため、関数呼び出しのオーバーヘッドがありません。
特に、小さな処理を大量に呼び出すときには、かつてはよく使われていました。
また、戻り値の型に縛られないことも特徴です。
同じ定義でも、引数の式によって結果の型が変わるケースもあります。
さらに、マクロはプリプロセッサの機能を使えるため、条件付きコンパイルなどと組み合わせやすいという柔軟性もあります。
デメリット
一方で、デメリットも多くあります。
特に初心者にとっては、意図しない挙動を生みやすいことが大きな問題になります。
代表的なものとして、以下のような点が挙げられます。
1つ目に、引数が複数回評価されてしまうことがあります。
その結果、引数に副作用を持つ式(インクリメントなど)を書いた場合、同じ処理が何度も実行されてしまう危険があります。
2つ目に、デバッグが難しいという問題があります。
マクロはコンパイル前に展開されるため、デバッガ上ではマクロの呼び出しではなく展開後のコードしか見えず、原因追跡がしづらくなります。
このように、便利だが落とし穴も多いのが関数風マクロです。
引数付きの関数風マクロの基本構文
基本の書き方
関数風マクロの基本形は次のようになります。
#define マクロ名(引数リスト) 置き換え後のコード
例えば、二つの値のうち大きい方を返すマクロは次のように書けます。
#define MAX(a, b) ((a) > (b) ? (a) : (b))
この定義があると、MAX(x, y)という呼び出しは、プリプロセス時に((x) > (y) ? (x) : (y))というコードに置き換えられます。
括弧の付け方
関数風マクロで最も重要なのが括弧の付け方です。
括弧が足りないと、計算結果が思っていたものと違うといったバグを生みやすくなります。
なぜ括弧が重要なのか
マクロはあくまで文字列の置き換えなので、演算子の優先順位によって結果が変わる可能性があります。
安全のために、次の2つのルールを意識します。
1つ目は、引数はすべて( )で囲むことです。
2つ目は、マクロ全体の結果も( )で囲むことです。
括弧の有無による具体的な違い
括弧を適切に付けなかった場合の例を見てみます。
#include <stdio.h>
// 括弧が不足している良くないマクロ
#define BAD_SQUARE(x) x * x
// 括弧をきちんと付けたマクロ
#define GOOD_SQUARE(x) ((x) * (x))
int main(void) {
int a = 3;
// 期待する計算: 2 * (a * a) = 18
// BAD_SQUARE(a) は 2 * a * a に展開されるので
// 実際の計算は (2 * a) * a = 18 になり、たまたま合う
int r1 = 2 * BAD_SQUARE(a);
// こちらも同じく 2 * ((a) * (a)) に展開される
int r2 = 2 * GOOD_SQUARE(a);
// しかし、式の左側に書くと違いが出ることがある
int r3 = BAD_SQUARE(a + 1); // -> a + 1 * a + 1 に展開
int r4 = GOOD_SQUARE(a + 1); // -> ((a + 1) * (a + 1)) に展開
printf("r1 = %d\n", r1);
printf("r2 = %d\n", r2);
printf("r3 = %d\n", r3);
printf("r4 = %d\n", r4);
return 0;
}
r1 = 18
r2 = 18
r3 = 7
r4 = 16
同じ見た目のマクロ呼び出しでも、中身の定義によって結果が大きく変わることが分かると思います。
特にr3とr4の違いは、括弧の有無による典型的なバグです。
具体例
ここでは、初心者でもよく使うことがある関数風マクロの具体例をいくつか紹介します。
あくまで学習用の例として理解し、実際の現場コードでは後述するインライン関数なども検討することをおすすめします。
絶対値を求めるマクロ
#include <stdio.h>
// 整数の絶対値を求める関数風マクロ
#define ABS(x) ((x) < 0 ? -(x) : (x))
int main(void) {
int a = -5;
int b = 10;
int ra = ABS(a); // -> ((a) < 0 ? -(a) : (a))
int rb = ABS(b); // -> ((b) < 0 ? -(b) : (b))
printf("ABS(%d) = %d\n", a, ra);
printf("ABS(%d) = %d\n", b, rb);
return 0;
}
ABS(-5) = 5
ABS(10) = 10
最大値を取るマクロ
#include <stdio.h>
// 2つの値のうち大きい方を返すマクロ
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main(void) {
int x = 3;
int y = 7;
int m = MAX(x, y); // -> ((x) > (y) ? (x) : (y))
printf("MAX(%d, %d) = %d\n", x, y, m);
return 0;
}
MAX(3, 7) = 7
このように、短い式を何度も使い回したいときに関数風マクロはよく使われます。
関数風マクロの注意点と落とし穴
副作用に注意
関数風マクロの最大の落とし穴は、引数が複数回評価される可能性があることです。
そのせいで、副作用を持つ式を渡すと、意図しない回数だけ処理が実行されてしまうことがあります。
副作用とは何か
ここでいう副作用とは、インクリメントや代入など、値を計算すると同時に状態も変えてしまう操作のことです。
例えばi++は、値を返すだけでなくiを1増やすという副作用を持ちます。
典型的な危険例
次のコードを見てください。
#include <stdio.h>
// 引数を2倍にするマクロ
#define DOUBLE(x) ((x) + (x))
int main(void) {
int i = 5;
// i++ がマクロの中で2回評価される
int r = DOUBLE(i++); // -> ((i++) + (i++))
printf("r = %d, i = %d\n", r, i);
return 0;
}
r = 11, i = 7
この例では、DOUBLE(i++)が((i++) + (i++))に展開されるため、i++が2回実行されます。
その結果、iは2回インクリメントされることになり、予想しにくい動作になります。
関数風マクロの引数に、副作用を持つ式(cst-code>i++やi += 2など)を書くのは避けるのが基本です。
複数行マクロの書き方
関数風マクロは、1行で書くだけでなく複数行の処理をまとめることもできます。
ただし、そのためには少し特殊な書き方が必要です。
バックスラッシュを使った複数行マクロ
複数行にわたるマクロ定義では、行の末尾に\(バックスラッシュ)を書きます。
これにより、プリプロセッサは次の行も同じマクロ定義の一部だとみなします。
#include <stdio.h>
// 2つの値を入れ替える複数行マクロ
#define SWAP(a, b) \
do { \
int tmp = (a); \
(a) = (b); \
(b) = tmp; \
} while (0)
int main(void) {
int x = 3;
int y = 7;
printf("before: x = %d, y = %d\n", x, y);
// マクロ呼び出しのように見えるが、展開されるだけ
SWAP(x, y);
printf("after : x = %d, y = %d\n", x, y);
return 0;
}
before: x = 3, y = 7
after : x = 7, y = 3
do { … } while (0) と書く理由
複数行マクロでは、ブロック全体をdo { ... } while (0)で囲むという書き方がよく使われます。
これは、マクロをif文などと組み合わせたときに、構文上の不具合を避けるための定石です。
例えば、if (cond) SWAP(x, y);のように書いたときでも、展開後のコードが1つの文として扱えるようにするために、このパターンが利用されます。
デバッグのしづらさ
関数風マクロは、実際には存在しない「見かけだけの呼び出し」なので、デバッガで追いにくいという問題があります。
なぜデバッグが難しいのか
デバッガでステップ実行をするとき、関数であればその関数の中に入って処理を1行ずつ追うことができます。
しかし、マクロはコンパイル前に展開され、main関数などの中に直接コードとして埋め込まれてしまいます。
そのため、ソースコード上ではMAX(x, y)と1行に見えていても、コンパイルされたコードの中にはMAXという関数は存在せず、((x) > (y) ? (x) : (y))という式だけが現れます。
このギャップがバグの原因を見つけづらくするのです。
エラーメッセージも分かりにくくなる
さらに、コンパイラのエラーメッセージや警告も、展開後のコードに対して出されるため、どのマクロが原因なのかが分かりにくくなることがあります。
初心者のうちは、難しいマクロをいきなり多用しない方が、問題切り分けの面で安全です。
関数風マクロと関数の使い分け
どんなときに関数風マクロを使うか
現代のCプログラミングでは、何でもかんでもマクロで書くのは推奨されません。
それでも、関数風マクロが意味を持つ場面はいくつかあります。
まず、「型に依存しないごく短い式」を書きたいときです。
例えばABSやMAXのような処理は、intだけでなくdoubleなどにも使いたくなることがあります。
その場合、型に依存した関数を多数作るよりも、マクロで1つにまとめた方が楽なケースがあります。
また、デバッグ用のログ出力など、__FILE__や__LINE__といったプリプロセッサ専用の情報を組み合わせたい場合にも、マクロがよく使われます。
インライン関数との比較と使い分け
C99以降では、インライン関数inlineという機能が使えるようになりました。
インライン関数は、マクロのように展開されて関数呼び出しのオーバーヘッドを減らしつつ、関数としての型チェックも受けられるという、中間的な存在です。
簡単なインライン関数の例
#include <stdio.h>
// C99以降で利用できるインライン関数
static inline int square_int(int x) {
return x * x; // 関数なので x は1回だけ評価される
}
int main(void) {
int a = 5;
int r = square_int(a);
printf("square_int(%d) = %d\n", a, r);
return 0;
}
square_int(5) = 25
インライン関数は、コンパイラに「この関数は小さいので、できれば展開してね」と依頼する仕組みです。
コンパイラが最適化の都合で実際にインライン展開しない可能性もありますが、多くの場合はマクロに近い効率が期待できます。
マクロとインライン関数の使い分け
文章で整理すると、使い分けの目安は次のようになります。
マクロを選ぶ場面としては、まず型に依存しない式を1つの定義で表したいときです。
さらに、プリプロセッサの特殊な機能(cst-code>#や##、__FILE__など)を使いたいときもマクロの出番になります。
インライン関数を選ぶ場面としては、型チェックをきちんと受けたいときや、副作用を持つ引数でも安全に使いたいときが挙げられます。
また、デバッグしやすさを重視したいときもインライン関数の方が適しています。
初心者のうちは、「まずは関数やインライン関数で書いてみて、本当に必要な場合だけマクロを検討する」という方針が安全です。
C言語初心者がまず覚えておきたいポイント
C言語を学び始めた段階では、関数風マクロについて次のポイントだけ押さえておくと十分です。
1つ目に、関数風マクロは「関数に見えるけれど、実態はただの文字列置き換え」であることを理解することです。
そのため、型チェックがなく、引数が何回使われるかも注意しなければなりません。
2つ目に、括弧を徹底して付けることです。
引数もマクロ全体も( )で囲むことで、演算子の優先順位による思わぬバグを防ぐことができます。
3つ目に、副作用を持つ式をマクロの引数に渡さないというルールを守ることです。
i++などを渡すと、意図せず複数回評価される危険があります。
最後に、迷ったらまずは普通の関数で書くと決めておくと、マクロに振り回されずにC言語の基本を身につけやすくなります。
まとめ
関数風マクロは、C言語のプリプロセッサ機能を使って、関数のように引数を取る「コードのひな形」を定義する仕組みです。
実行速度や記述の短さというメリットがある一方で、副作用や括弧不足による予期しない挙動、デバッグのしづらさなど、多くの落とし穴も抱えています。
初心者のうちは、括弧を徹底すること、副作用のある式を渡さないこと、そしてまずは通常の関数やインライン関数を優先することを心がけると、安全にマクロと付き合っていけます。
