先頭へ戻る

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

Programming Place Plus トップページC言語編

先頭へ戻る

この章の概要

この章の概要です。


関数形式マクロ

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

関数形式マクロは、使用時の記述が関数を呼び出しているように見えるため、このように呼ばれます。関数形式マクロの定義は、次のように行います。

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

マクロ名の直後に ( ) で仮引数を指定する点が、オブジェクト形式マクロとの違いです。ただし、仮引数に型の指定はありません。仮引数がない場合は void とは書かず、単に ( ) の中を空にします。

実際の使用例を見てみましょう。

#include <stdio.h>
#include <string.h>

#define STR_EQ(s1,s2)       strcmp(s1,s2)==0

int main(void)
{
    if( STR_EQ( "abc", "abc" ) ){
        puts( "OK" );
    }

    if( !STR_EQ( "abc", "ab" ) ){
        puts( "OK" );
    }

    return 0;
}

実行結果:

OK
OK

文字列同士の一致を調べる STR_EQ という関数形式マクロを定義しました。strcmp関数は、2つの文字列の内容が一致したときに偽(0) を返す仕様ですが、このマクロを使うと、自然な形で(一致したときに真)判定できます。

関数形式マクロを使うときには、実引数を指定します。この使い方が通常の関数とよく似ています(というより同じです)。ただし、プリプロセスの過程で置換されるので、このプログラムは実際には次のような形でコンパイルされます。

(stdio.h の中身がある)
(string.h の中身がある)

int main(void)
{
    if( strcmp("abc","abc")==0 ){
        puts( "OK" );
    }

    if( !strcmp("abc", "ab")==0 ){
        puts( "OK" );
    }

    return 0;
}

実行結果:

OK
OK

結果をさらに否定している方に、やや違和感があるかもしれません。このままでも正しく動作はしますが、一般的には、マクロの置換結果全体を ( ) で囲むべきです。そうすることで、演算子の優先順位の兼ね合いで予期しない結果になることを防ぐ効果があります。つまり、次のように定義します。

#define STR_EQ(s1,s2)       (strcmp(s1,s2)==0)

この場合、置換結果は次のようになります。

if( !(strcmp("abc", "ab")==0) ){
    puts( "OK" );
}

通常の関数と異なり、マクロの場合はプリプロセスの段階で置換済みであることを忘れないようにしましょう。関数を呼び出すという行為そのものにも処理時間がかかるのですが、マクロの場合はこの呼び出し自体が発生しませんから、処理効率は向上します。

一方で、マクロを使っている箇所が個別に置換されるため、使用箇所が多いと、プログラムの大きさは増大しやすくなります。

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

#define INITIALIZE(a,b,c)  \
    (a) = 0; \
    (b) = 0; \
    (c) = 0;

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

空の実引数

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

#include <stdio.h>

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

int main(void)
{
    int iValue = CAT_VALUE(10, );
    long int lValue = CAT_VALUE(10, L);
    long long int llValue = CAT_VALUE(10, LL);

    printf( "%d\n", iValue );
    printf( "%ld\n", lValue );
    printf( "%lld\n", llValue );

    return 0;
}

実行結果:

10
10
10

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

##演算子については、「##演算子(トークン連結演算子)」で解説しています。

可変個引数マクロ

関数形式マクロの引数を可変個数にできます。つまり、printf関数のように、引数の個数が固定的でないようなマクロが使えます。

この機能に関する詳細は、第52章で取り上げます。


マクロ使用時の注意点

関数形式マクロは便利な反面、うまく使わないと予期せぬ動作をしてしまいます。

まず 1つ目の問題は、引数に型がないことです。仮引数に名前はあるものの、型は指定しませんから、どんな型の実引数でも指定できてしまいます。そのため、使うときには間違った型を指定しないように注意する必要があります。

プリプロセスで置換が行われた後、C言語のルールどおりにコンパイルされるので、その段階で型が明らかに正しくなければ、コンパイルエラーになります。

2つ目の問題は、計算順序です。たとえば、次の関数形式マクロはどういう結果になるでしょう?

#define COMPUTE(a,b) a+b*10

このマクロを、次のように使ったとします。

int num1 = COMPUTE( 5, 2 );
int num2 = 3 * COMPUTE( 5, 2 );

これは次のように置換されます。

int num1 = 5+2*10;
int num2 = 3 * 5+2*10;

num1 の値は、問題なく 25 です。

num2 の値は 35 です。恐らく望んでいた答えは、num1 の値の 3倍にあたる、75 のはずです。

置換された結果から分かるように、本当は、「3 * (5+2*10)」となってほしいところが、「(3*5) + (2*10)」のようになってしまっています。計算の優先順位が、意図したとおりになっていません。

この問題は、関数形式マクロを定義するときに、置換結果全体を ( ) で囲むことで解決できます

#define COMPUTE(a,b) (a+b*10)

すると、先ほどの 2つの呼び出しは、次のように展開されます。

int num1 = (5+2*10);
int num2 = 3 * (5+2*10);

これで意図どおりに計算されます。このように、マクロの置換後の文字の並びには、( ) も含まれます。

しかし、これでもまだ問題になるケースがあります。たとえば、次のように呼び出されると困ります。

int num3 = COMPUTE( 5, num1+3 );

num1 は先ほどの続きで 25 になっているとしましょう。意図としては、「5 + (25+3)*10」なので、285 になってほしいのですが、実際には 60 です。置換結果は次のようになっています。

int num3 = (5+num1+3*10);

この問題も解決できます。そのためには、関数形式マクロの置換結果に登場するすべての引数の名前を、 ( ) で囲みます

#define COMPUTE(a,b) ((a)+(b)*10)

すると、次のように置換されます。

int num3 = ((5)+(num1+3)*10);

これで解決しました。なかなか面倒ではありますが、置換結果全体と、置換結果の中に現れる引数の名前をそれぞれ、( ) で囲むことは、関数形式マクロを使う上では必須事項です

最後に、3つ目の問題です。今度は、次のようなマクロを用意します。

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

2つの引数を持ち、大きい方の値を返すマクロです。次のように使います。

int a = 10;
int ans1 = MAX(a, 5);
int ans2 = MAX(++a, 5);

ans1 の方は何も問題なく、10 が代入されますが、ans2 の方が問題です。置換結果は次のようになります。

int ans2 = ((++a) > (5) ? (++a) : (5));

条件判定の際に、a はインクリメントされるので、11 と 5 を比較します。大きいのは 11 の方なので、++a が返されますが、ここが問題です。++a なので、ここでもう1度インクリメントされてしまうのです。つまり、11 と 5 の大きい方を返すはずなのに、結果は 12 です。

実は、このインクリメント(もちろん、デクリメントも)による問題の解決策はありません。注意して使うしか方法はないのです。

記憶に留めておいて欲しいことは、関数のように見えるものを使うとき、不用意に実引数にインクリメントやデクリメントを使わないことです。特に、標準ライブラリ関数には、関数のように見えて、実は関数形式マクロになっているものがいくつかあります。

このようなマクロの問題点に対する有効な解決策として、インライン関数があります。これは第57章で取り上げます。


#演算子(文字列化演算子)

#演算子(文字列化演算子)は、実引数を文字列リテラルに置換する効果があります。つまり、実引数を "" で囲んだものをつくります。

#演算子を使えるのは関数形式マクロだけです。置換後の文字の並びの中に「#仮引数名」という形で記述します。

#include <stdio.h>

#define LOG_INT(var)    printf(#var ": %d\n", var)

int main(void)
{
    int num1 = 123;
    int num2 = -350;

    LOG_INT( num1 );
    LOG_INT( num2 );
    LOG_INT( 999 );

    return 0;
}

実行結果:

num1: 123
num2: -350
999: 999

LOG_INTマクロは、実引数に符号付き整数を与えると、その実引数を文字列リテラルにしたものとともに、現在の値を出力します。変数を指定した場合には、その変数名が得られていることが分かります。定数を指定した場合も、その定数値がそのまま文字列リテラルになっています。

このサンプルプログラムで、LOG_INTマクロの実引数の前後に空白文字がありますが、これらは削除されます。

なお、1つの関数形式マクロの中で、#演算子(および後で取り上げる ##演算子)が複数登場するとき、どの順番で評価されるかは未規定です。

##演算子(トークン連結演算子)

##演算子(トークン連結演算子)は、マクロの置換後の文字の並びの中でのみ使用できます。この演算子は、## の前後にある字句を連結します。#演算子と違って、こちらは必ずしも関数形式マクロである必要はありません。

#include <stdio.h>

#define CAT(first,second)   first ## second

int main(void)
{
    int num1 = 10;
    int num2 = 20;
    int num3 = 30;

    printf( "%d\n", CAT(num, 1) );
    printf( "%d\n", CAT(num, 2) );
    printf( "%d\n", CAT(num, 3) );

    return 0;
}

実行結果:

10
20
30

たとえば、CAT(num, 1) というコードは、「num」と「1」が連結されて「num1」になります。文字列リテラルの “num” と “1” が連結されて “num1” になるのとは違うことを理解してください。

連結された結果、できあがった文字の並びが、変数名や関数名などの何らかの有効な言葉になっていなければなりません。そうなっていない場合の扱いは未定義です。このサンプルプログラムでは、「num1」のような、変数名として有効な言葉になっているので問題ありません。

1つのマクロの中で、##演算子(および #演算子)が複数登場するとき、どの順番で評価されるかは未規定です。

なお、printf関数の呼び出しの部分を、以下のように変更すると、意図どおりにならないことに注意してください。

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

いつものように、#define がプリプロセスで処理されていることを考えてください。CAT の呼び出しが置換されるのは、プリプロセスの段階ですから、変数 i の値が何であるかは関係ありません。そのため、CAT を置換した結果は「numi」です。「numi」という言葉は、有効な名前になっていないので、結果は未定義です。

for文と ##演算子を組み合わせて、何か楽ができないかと考えてしまいますが、両者は処理される段階が異なるので、残念ながら不可能です。


練習問題

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

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

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

問題④ 次のプログラムはコンパイル可能ですか? 可能であるとしたら、どのような実行結果になりますか?

#include <stdio.h>

#define CALC(a,b,op)        a ## op ## b
#define CALC_STR(a,b,op)    #a ## #op ## #b

int main(void)
{
    printf( "%s=%d\n", CALC_STR( 10, 2, + ), CALC( 10, 2, + ) );
    printf( "%s=%d\n", CALC_STR( 10, 2, - ), CALC( 10, 2, - ) );

    return 0;
}


解答ページはこちら

参考リンク


更新履歴

’2019/2/12 VisualStudio 2015 の対応終了。

≪さらに古い更新履歴を展開する≫



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

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

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

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
Twitter でツイート Twitter をフォロー LINE で送る
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー