C言語編 第28章 関数形式マクロ

先頭へ戻る

この章の概要

この章の概要です。


関数形式マクロ

第24章で、オブジェクト形式マクロを説明しました。これは、#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;

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

C99 (空の実引数)

C99 では、関数形式マクロの実引数を空白にすることが許可されるようになりました。

#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つ目の実引数を空にできます。##演算子 については、「##演算子(トークン連結演算子)」で解説しています。

C99 (可変個引数マクロ)

C99

C99 では、関数形式マクロの引数を可変個数にできます。つまり、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 です。

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

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

C++ であれば、インラインのテンプレート関数を使うことが、マクロの問題点に対する有効な解決策です(Modern C++編【言語解説】第11章)。また、型が明確であれば、C99 のインライン関数で代用することも有効です。

また、通常の関数であれば、ここで見たような問題は起こりませんが、違う問題に巻き込まれることがあります。例えば、「func( ++a, ++a );」のような関数呼び出しでは、どちらのインクリメントが先に行われるか、保証がありません。なぜなら、実引数で行われる演算をどの順序で行うかは規定されていないからです。そのため、順序が問題になり得るような実引数の与え方は避けるようにしなければなりません。

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

#演算子(文字列化演算子)は、関数形式マクロの置換後の文字の並びの中でのみ使用できます。この演算子は、マクロの引数を、"" で囲んだ文字列リテラルに置き換える効果があります

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

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

##演算子(トークン連結演算子)は、マクロの置換後の文字の並びの中でのみ使用できます。この演算子は、## の前後にある字句を連結します。こちらは必ずしも関数形式マクロである必要はありませんが、使い方を考えると、オブジェクト形式マクロで使うことはあまりないでしょう。

ここで、字句というのは、ソースコード上の文字のことを指しています。"" で囲まれた文字列のことではありません。

#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」のような、変数名として有効な言葉になっているので問題ありません。

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

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

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

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


C99 (インライン関数)

C99

C99 で inline指定子が追加されました。

inline指定子を使うことで、関数の呼び出しコストを避けられる可能性があります。「可能性がある」というのが気になりますが、inline指定子の意味合いは、「関数呼び出しを可能な限り高速にする」ことです。その具体的な方法については決まっていませんし、コンパイラはこの指定を単に無視することもあります

なお、inline指定子が付加された関数を、インライン関数と呼びます。

具体的な方法は決まっていませんが、関数定義の内容を、呼び出し元のところに展開する方法が一般的です。そのため、関数呼び出しのコストが無くなりますが、呼び出し箇所が多ければプログラムサイズが大きくなる可能性があります。要するに、関数形式マクロが展開される場合と同じような結果を生む訳ですが、マクロがプリプロセスで展開しているのに対し、インライン関数はコンパイル時に行われます。また、あくまでも関数ですので、きちんと型を持った仮引数や戻り値を使えます。

なお、この手法は、インライン展開と呼ばれています。

VisualStudio 2015/2017 では、正式に対応しているという記述が発見できていませんが、少なくともコンパイルエラーにはならず、対応しているようです。

VisualStudio の場合、inline の代わりに __inline というキーワードを使えば同じ結果を生みます。これは Microsoft の独自拡張です。

インライン関数の仕様は複雑なので、順番に見ていきましょう。

まず、関数が内部結合の場合は簡単で、関数宣言と関数定義に、inline指定子を付加すれば、インライン関数になります。関数が内部結合であるとは、つまり、static を付けた関数であるということです(第23章)。

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

inline static int str_eq(const char* s1, const char* s2);

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

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

    return 0;
}

inline static int str_eq(const char* s1, const char* s2)
{
    return strcmp( s1, s2 ) == 0;
}

実行結果:

OK
OK

一方、関数が外部結合の場合には難しくなります。この難しさの原因は、インライン展開の手法が使われる場合のことをイメージすれば分かります。

インライン関数の定義の内容を、関数を呼び出している箇所に展開するためには、呼び出し箇所から、関数の中身(つまりは関数定義)を知ることができなければならない訳です。内部結合の関数であれば、この要件は必ず満たせます。

しかし、外部結合ではそうはいかないかもしれません。当サイトが徹底しているような、外部関数の定義をソースファイルに、その宣言をヘッダファイルに書くスタイルを想定すると、例えば、main.c から sub.c の中身は見えていません。そのため、関数呼び出しが main.c にあり、呼び出される インライン関数が sub.c にあったら、その関数のコードを展開できないのです。

例えば、次のプログラムはコンパイルエラーになります。

/* sub.h */
#ifndef SUB_H_INCLUDED
#define SUB_H_INCLUDED

inline int str_eq(const char* s1, const char* s2);

#endif
/* sub.c */

#include "sub.h"
#include <string.h>

inline int str_eq(const char* s1, const char* s2)
{
    return strcmp( s1, s2 ) == 0;
}
/* main.c */

#include <stdio.h>
#include "sub.h"

int main(void)
{
    if( str_eq( "abc", "abc" ) ){  /* コンパイルエラー */
        puts( "OK" );
    }

    if( !str_eq( "abc", "ab" ) ){  /* コンパイルエラー */
        puts( "OK" );
    }

    return 0;
}

main.c からは str_eq関数の宣言は見えています。そのため、インライン関数でない通常の関数なら、このプログラムは問題ありません。しかし、str_eq関数の宣言には、inline指定子が付加されているため、インライン関数として処理しようとします。そうなると、str_eq関数の定義が必要ですが、main.c から sub.c の内容を見られませんから、コンパイルエラーになります。

「main.c から sub.c の内容を見られない」という点が飲み込めない方は、各ソースファイルが別個にコンパイルされ、別個にオブジェクトファイル(例えば、main.o と sub.o) が生成されることを思い出してください(第23章)。main.c をコンパイルしているときに sub.o の中身は使えませんし、sub.c をコンパイルしているときに main.o の中身は使えません。

解決方法については非常にややこしく、特にコンパイラによって挙動が異なるため、あるコンパイラでうまく動作しても、他のコンパイラではうまくいかないことがあります。

C++ の インライン関数とも仕様が異なることにも注意が必要かもしれません。C++ であれば、ヘッダファイルに inline指定子付きの関数定義を置けば、それだけで事足りますが、C では逆に、この方法では正しく動作しないはずです。

正しい解決策は、ヘッダファイルには、inline指定子付きで関数定義を書き、ソースファイルに、extern指定子と inline指定子付きの関数宣言を書くことです。直感に反して、ヘッダファイル側に"定義"、ソースファイル側に"宣言" であることに注意してください。

/* sub.h */
#ifndef SUB_H_INCLUDED
#define SUB_H_INCLUDED

#include <string.h>

inline int str_eq(const char* s1, const char* s2)
{
    return strcmp( s1, s2 ) == 0;
}

#endif
/* sub.c */

#include "sub.h"

extern inline int str_eq(const char* s1, const char* s2);
/* main.c */

#include <stdio.h>
#include "sub.h"

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

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

    return 0;
}

実行結果:

OK
OK

sub.h にある、inline指定子付きの関数定義のことを、インライン定義と呼びます。これは、インライン展開が行われるときに、展開するコードの中身を知るために必要なものです。そのため、インライン関数の呼び出しを行っている main.c から見えるところになければなりません。

インライン定義は、コンパイルしても、そこに結果を出力しないかもしれません。なぜなら、インライン展開することをコンパイラが選択したとすれば、コンパイル結果を出力すべき場所は、インライン定義を記述した位置ではなく、その関数を呼び出している箇所だからです。そのため、インライン定義とは別に、関数の本体となるものが必要です。それが、sub.c にある extern と inline が付いた関数宣言です。


練習問題

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

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

問題③ 実引数で指定された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;
}


解答ページはこちら

参考リンク



更新履歴

'2018/7/21 inline を指定子と表記するように修正。

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

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

'2018/4/5 VisualStudio 2013 の対応終了。

'2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。

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



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

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

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

Programming Place Plus のトップページへ


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