C言語基礎|コンマ演算子を用いた関数形式マクロ

「if の中でマクロが壊れる理由、ちゃんと説明できますか?─コンマ演算子で安全にまとめるコツ」

コンマ演算子を用いた関数形式マクロって、何がうれしいの?

関数形式マクロは、見た目は関数呼び出しっぽいのに、実体はプリプロセッサによる展開(置き換え)です。
この性質のおかげで「便利に書ける」反面、複数の処理をまとめようとして { } を使うと、if–else の形が崩れて翻訳時エラーになったりします。

そこで活躍するのが コンマ演算子
複数の式を 1つの式 にまとめられるので、マクロの展開結果を「単一の式文」にできて、if–else の中でも安全に使えるようになります。

この記事では、わざと失敗するマクロ → なぜ失敗するか → コンマ演算子で直す、という流れで、表と図つきでしっかり解説しますね。

まずは失敗例:複合文 { } を返すマクロは if–else と相性が悪い

次のサンプルは、記号を出してからメッセージを表示するマクロです。内容はシンプルですが、わざと “危ない形” にしています。

サンプル(誤り):複合文に展開されるマクロ

プロジェクト名:chap8-4-1 ソースファイル名:chap8-4-1.c

Visual Studio でこのプログラムを実行するには、SDLチェック設定を変更しておく必要があります。
1.プロジェクト名を右クリックして、「プロパティ」をクリックします。
2.「C/C++」→「全般」→「SDLチェック」を「いいえ」に切り替えて「OK」をクリックします。

#include <stdio.h>

// 記号を出してからメッセージ表示(誤り)
#define mark_puts(msg) { putchar('>'); puts(msg); }

int main(void)
{
    int n;

    printf("整数を入力してください:");
    scanf("%d", &n);

    if (n > 0)
        mark_puts("プラスの値です。");
    else
        mark_puts("ゼロ以下の値です。");

    return 0;
}

このプログラムは、翻訳時エラーになりやすいです(コンパイラは else が迷子になります)。

展開後に if が途中で終わってしまう

上の if 文を「マクロ展開後の姿」にすると、こうなります。

誤ったマクロの展開(イメージ)

if (n > 0)
{ putchar('>'); puts("プラスの値です。"); } ;
else
{ putchar('>'); puts("ゼロ以下の値です。"); } ;

何が起きている?

ポイントはここです。

  • { putchar(...); puts(...); } 複合文(1つの文)
  • その直後の ; は 空文(何もしない1文)
  • つまり if (n > 0) が担当するのは、最初の { ... } までで完結してしまう
    そのあとに ; が独立した文として現れ、さらに else が出てくる

コンパイラ目線だと、
「if がもう終わっているのに、どうして else があるの?」
となってエラーになります。

じゃあ { } を消せばいい?

例えばこうすると…

#define mark_puts(msg) putchar('>'); puts(msg);

今度は if が putchar だけを担当して、puts は常に実行される みたいな “別の事故” を呼びやすいです。
つまり、複数処理を素直に書くほど、if–else の文のまとまりと衝突しやすいんですね。

解決策:コンマ演算子で「複数の式」を「単一の式」にする

ここで使うのが コンマ演算子 です。

Table 1 コンマ演算子のルール

意味
a, ba を評価して捨て、次に b を評価する。式全体の型と値は b の結果になる。

表の説明
コンマでつないだ a, b は 1つの式 です。
中では左から順に評価されますが、最終的な「式の値」は右側(b)になります。

正しい書き方:マクロを 1つの式に展開させる

さっきのマクロを、コンマ演算子で書き直します。

サンプル(正しい):コンマ演算子で単一の式にする

プロジェクト名:chap8-4-2 ソースファイル名:chap8-4-2.c

Visual Studio でこのプログラムを実行するには、SDLチェック設定を変更しておく必要があります。
1.プロジェクト名を右クリックして、「プロパティ」をクリックします。
2.「C/C++」→「全般」→「SDLチェック」を「いいえ」に切り替えて「OK」をクリックします。

#include <stdio.h>

// 記号を出してからメッセージ表示(正しい)
#define mark_puts(msg) (putchar('>'), puts(msg))

int main(void)
{
    int n;

    printf("整数を入力してください:");
    scanf("%d", &n);

    if (n > 0)
        mark_puts("プラスの値です。");
    else
        mark_puts("ゼロ以下の値です。");

    return 0;
}

実行例

整数を入力してください:0
>ゼロ以下の値です。

図で理解:if の中身が「式文 1個」になる

正しいマクロの展開(イメージ)

if (n > 0)
    (putchar('>'), puts("プラスの値です。"));
else
    (putchar('>'), puts("ゼロ以下の値です。"));

ポイント

  • (putchar(...), puts(...))
  • その式に ; が付くと 式文(単一の文) になる
  • だから if と else の対応関係が崩れません

重要:複数の式に置換するマクロは「単一の式」になるように作る

ここ、この記事の核心です。

複数処理マクロの安全度

展開先if–else での安全性なぜ
{ 文; 文; }危険if の文が終わった扱いになりやすい。
文; 文;危険if が1文しか担当しない。
(式, 式)安全1つの式 → 1つの式文として扱える。

表の説明
if の後ろに置けるのは「1つの文」です。
コンマ演算子を使って「1つの式」にしておけば、最後に ; を付けて「1つの式文」になり、構文が安定します。

コンマ演算子の性質を、もう少しだけ丁寧に

コンマ式 a, b は次の性質があります。

  • a が先に評価される(副作用があればここで起きる)
  • a の値は捨てられる
  • b が評価され、式全体の型と値になる

例:右側の結果が代入される

int i = 3;
int j = 5;
int x;

x = (++i, ++j);

このときの流れはこうです。

評価順序のイメージ

++i を実行 → i は 4(この値は捨てる)
++j を実行 → j は 6(この値が式全体の値)
x に 6 が代入される

この章で登場する命令・記号の書式と役割

ここからは、出てきた要素を「何をするもの?」で整理します。

#define の書式(関数形式マクロ)

書式

#define マクロ名(仮引数) 置換内容

何をする?
ソース中の マクロ名(…) を、コンパイル前に 置換内容 へ展開します。
今回の mark_puts(msg) は、msg の部分に引数が入り、(putchar(...), puts(msg)) に展開されます。

if / else の書式

書式

if (条件){
    文;
}else{
    文;
}

何をする?
条件が真なら if 側の文を実行し、偽なら else 側の文を実行します。
大事なのは、if (条件) の直後に置けるのは 文が1つ という点です(ここにマクロが食い込むと事故が起きます)。

putchar の書式

書式

putchar(文字);

何をする?
1文字を出力します。今回の例では > を表示して、ログっぽい見た目を作っています。

puts の書式

書式

puts("文字列");

何をする?
文字列を表示し、最後に改行も出します。短いメッセージ表示に便利です。

printf / scanf の書式(入力を受け取る部分)

printf

printf("書式文字列", 値, ...);

表示用(問いかけ文を出す)。

scanf

scanf("書式文字列", 変数のアドレス, ...);

入力用(今回なら %d で int を読む)。
&n は n の格納場所(アドレス)を渡しています。

セミコロン ; の意味

  • 式; は 式文(その式を実行する1つの文)
  • ; だけだと 空文(何もしない1つの文)

失敗例では、この ; が構文を崩す引き金になりました。

まとめ:コンマ演算子マクロの使いどころ

  • 複数の処理をマクロでまとめたいとき
  • しかも if–else の中でも安全に使いたいとき
  • その場合は (式, 式) で 1つの式にして、式文として扱える形にする。

これで、見た目が関数っぽいマクロでも、構文事故をかなり減らせます。