関数形式マクロ | Programming Place Plus C言語編 第28章

トップページC言語編

このページの概要 🔗

このページの解説は C99 をベースとしています

以下は目次です。


関数形式マクロ 🔗

第23章で、オブジェクト形式マクロを説明しました。これは、#define を使って記述し、プリプロセスの時点でソースコード上の文字の並びを置換する機能でした。今回説明する関数形式マクロ (function-like macro) も #define を使って実現しますが、少し形が異なるものです。

関数形式マクロは、引数があるマクロで、関数に似ているためこう呼ばれます。関数形式マクロの構文は次のとおりです。

#define マクロ名(仮引数の並び) 置換後の文字の並び

本物の関数と違い、仮引数は名前があるだけで、型は指定しません。仮引数が不要なら () の内側を空白にします。「置換後の文字の並び」の中では、渡されてきた引数を使用できます。

なお、関数のように使うため、置換後の結果を複数行に渡って記述できた方が、見た目の上でも分かりやすくなることがあります。しかし、マクロの置換結果の中で普通に改行を行うことはできません。マクロの置換結果の中で改行を行うには、行末に \ を置きます。

#define INIT_POS(pos)  pos.x = 0; \
                       pos.y = 0; \
                       pos.z = 0;

特に、\ の後ろには改行しかあってはならないことに注意してください。


次の3つのマクロはいずれも関数形式マクロです。

#define PRINT_VALUE(x)                     printf("%d\n", x)
#define PRINT_NAME_AND_VALUE(name, value)  printf("%s: %d\n", name, value)
#define PRINT_EMPTY()                      puts("")

一般的に、関数形式マクロの「置換後の文字の並び」の行末に ; は不要です。マクロを使う側が ; を書くほうが自然ですし、むしろ定義側に ; があると邪魔になることがあります。

関数形式マクロを使用する側は、関数と同じように () を使います。仮引数があるのなら、対応する実引数も指定します。

PRINT_VALUE(123);
PRINT_NAME_AND_VALUE("xyz", 400);
PRINT_EMPTY();

【上級】仮引数の個数を自由にできる可変個引数マクロも実現できます。この話題は第52章で取り上げることにします。

関数形式マクロの用途には、以下のようなものがあります。

  1. 関数呼出しのコストを避ける
  2. 型を問わない関数を作る

ただし、このあと説明するように、関数形式マクロには、完全に解決することができない問題があるため、不用意に使うべきではありません。代替策として、インライン関数 (inline function) がありますが、ここではこの代替策についての話は省き、関数形式マクロで起こる問題の説明だけを行うことにします。

インライン関数は、第57章で取り上げます。


1つ目の用途は、マクロの処理がプリプロセス時点で終了することを活かして、プログラムの実行時の関数呼出しをなくして、高速化を図るというものです。たとえば、ループの内側で呼び出される関数は、そのループの回数分だけ、関数呼出しのコストが掛かることになりますが、関数形式マクロにすれば、関数呼出しは1回たりも行わずに済みます。

2つ目の用途は、まったく同じコードで実装できる関数だが、仮引数や戻り値の型が異なるため、別の関数にしなければならない場合に対応することです。たとえば、仮引数 a と b に対して return a + b; とするだけの場合、a と b は int型でも long long型でも double型でも同じコードになります。関数形式マクロには型の指定がないため、1つのマクロに集約できます。

#include <stdio.h>

#define ADD(a, b)  ((a) + (b))

int main(void)
{
    printf("%d\n", ADD(3, 5));
    printf("%lld\n", ADD(123LL, -340LL));
    printf("%lf\n", ADD(2.34, 6.15));
}

実行結果:

8
-217
8.490000

「置換後の文字の並び」の中に現れる引数は、1つ1つ () で囲むようにすると安全性が増します。全体を () で囲むことも多くの場合は有効ですが、置換内容によっては余計になるかもしれません。

#include <stdio.h>

#define BAD_MUL(a, b)  (a * b)
#define BETTER_MUL(a, b)  ((a) * (b))

int main(void)
{
    printf("%d\n", BAD_MUL(5 + 2, 5));
    printf("%d\n", BETTER_MUL(5 + 2, 5));
}

実行結果:

15
35

5 + 2 つまり 7 を 5倍しているつもりなので、35 が望まれる結果です。BAD_MUL の方の置換後の文字の並びは、(5 + 2 * 5) となるため、15 という間違った結果になりました。BETTER_MUL の方は、((5 + 2) * (5)) に置換されるので、35 が得られます。

() で丁寧に囲ったとしても、インクリメントやデクリメントを使われると問題になることがあります。そして、この問題は、使う側が気を付ける以外に解決策がありません。2つの引数のうち、大きい方に置換される MAX マクロで考えてみます。

#include <stdio.h>

#define MAX(a,b) ((a) > (b) ? (a) : (b))

int main(void)
{
    int a = 10;
    printf("%d\n", MAX(a, 5));
    printf("%d\n", MAX(a + 1, 5));
    printf("%d\n", MAX(++a, 5));
}

実行結果:

10
11
12

最初の2つの使い方には問題ありませんが、3つ目の結果は意外なものです。10 が入った変数 a をインクリメントしているので、想定される結果は 11 ですが、実際には 12 が出力されています。こうなってしまうのは、置換後の文字の並びが ((++a) > (5) ? (++a) : (5)) となるからです。条件式のところで1度インクリメントされたあと、結果の値のところでも再びインクリメントされています。

空の実引数 🔗

関数形式マクロの実引数を空白にすることが許可されています。

#include <stdio.h>

#define CAT_VALUE(value, suffix)  value ## suffix

int main(void)
{
    int i_value = CAT_VALUE(10, );
    long int l_value = CAT_VALUE(10, L);
    long long int ll_value = CAT_VALUE(10, LL);

    printf("%d\n", i_value);
    printf("%ld\n", l_value);
    printf("%lld\n", ll_value);
}

実行結果:

10
10
10

この例では、整数定数に付けるサフィックスを、##演算子による連結で実現しています。このとき、サフィックスが不要な場合には、2つ目の実引数を空にできます。

##演算子については、このあと「##演算子」の項で解説します。

#演算子 🔗

置換後の文字の並びの中で、マクロの仮引数の名前の頭に # を付加すると、実引数として渡された内容を文字列にしたものへ置き換えられます。この # は、関数形式マクロでのみ使用できる演算子の一種です。

次のプログラムでは、変数の値を、その変数の名前とともに出力する関数形式マクロを定義しています。

#include <stdio.h>

#define PRINT(v)    printf("%s = %d\n", #v, (v))

int main(void)
{
    int x = 123;
    long int y = 123456L;

    PRINT(x);
    PRINT(y);
}

実行結果:

x = 123
y = 123456

##演算子 🔗

置換後の文字の並びの中で ## を使うと、2つの文字の並びを連結できます。この ## は、マクロでのみ使用できる演算子の一種です。

#include <stdio.h>

#define VALUE(number)   value ## number

int main(void)
{
    int value1 = 10;
    int value2 = 20;
    int value3 = 30;

    printf("%d\n", VALUE(1));
    printf("%d\n", VALUE(2));
    printf("%d\n", VALUE(3));
}

実行結果:

10
20
30

VALUE マクロは、value という文字の並びと、仮引数number に渡されてきた文字の並びを連結しています。そのため、VALUE(1)value ## 1 を意味しており、value1 に置換されます。

マクロの置換はプリプロセスで行われることなので、次のようには書けません。

#include <stdio.h>

#define VALUE(number)   value ## number

int main(void)
{
    int value1 = 10;
    int value2 = 20;
    int value3 = 30;

    for (int i = 1; i <= 3; ++i) {
        printf("%d\n", VALUE(i));
    }
}

VALUE(i)value ## i であり、置換結果は valuei です。プリプロセスの時点では i は i でしかありません。これは求めていた結果ではないばかりか、未定義の動作ですらあります。##演算子によって作られる文字の並びは、前処理字句 (preprocessing token) と呼ばれる、意味が特定できる文字の並びでなければ未定義とされているためです[1]

前処理字句は、識別子や各種リテラル、ヘッダファイルの名前といったものです。[2]

なお、#演算子と ##演算子が両方登場する場合、どちらが先に評価されるかは未規定です[3]


練習問題 🔗

問題① 実引数で指定された値の絶対値を返す関数形式マクロを作ってください。

問題② 配列の名前を実引数に与えると、その配列の要素数を返すような関数形式マクロを作成してください。

問題③ 実引数で指定された2つの変数の値を交換する関数形式マクロを作成してください。


解答ページはこちら

参考リンク 🔗


更新履歴 🔗

≪さらに古い更新履歴≫

 VisualStudio 2015 の対応終了。

 inline を指定子と表記するように修正。

 「アサートマクロ」「独自のアサート」「コンパイル時アサート」の項を、第30章へ移動。
「独自のアサート」の中に紛れていた、マクロの置換結果内での改行に関する話題は、「関数形式マクロ」で行うようにした。

 「コンパイル時アサート」の項を追加(第50章から修正を加えて移動してきた)

 VisualStudio 2013 の対応終了。

 「VisualC++」という表現を「VisualStudio」に統一。

 全面的に文章を見直し、修正を行った。
章のタイトルを「マクロの活用」から「関数形式マクロ」に変更。

 コンパイラの対応状況について、対応している場合は明記しない方針にした。
Xcode 8.3.3 を clang 5.0.0 に置き換え。

 clang 3.7 (Xcode 7.3) を、Xcode 8.3.3 に置き換え。

 VisualC++ 2017 に対応。

 clang の対応バージョンを 3.7 に更新。

 clang の対応バージョンを 3.4 に更新。

 VisualC++ 2012 の対応終了。

 Xcode での NDEBUGマクロの定義について追記。
リンク先を修正。

 VisualC++ 2010 の対応終了。

 VisualC++ 2015 に対応。

 clang 3.2 に対応。

 VisualC++ 2013 に対応。

 VisualC++ 2008 の対応終了。

 clang 3.0 に対応。

 「C99 (空の実引数)」の項を追加。

 関数形式マクロの引数リストは、空にできることを追記。

 C99 の可変個引数マクロについての項を追加。

 「C99 のインライン関数について」の項を追加。

 「関数形式マクロ」の項に、定義全体を ( ) で囲むことについて追記。

 #演算子、##演算子の日本語表記での呼び名を併記した。

 新規作成。



前の章へ (第27章 いろいろな式)

次の章へ (第29章 事前定義マクロとプラグマ)

C言語編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る