えびちゃんの日記

えびちゃん(競プロ)の日記です。

マクロについて

人々がマクロをめちゃ使ってるんですが,罠を理解してる人が多いとは思えないのでいくつか書きます.

定数定義

constexpr 使ってくれ〜.

#define INFLL 1e18

int main() {
  ...
  if (x == INFLL) return puts("-1"), 0;
}

のようなコードは競プロでありがちですね(たぶん).

これは,x == 999999999999999999,つまり \(10^{18}-1\) のときに double の誤差の関係で -1 が出力されてしまいます. 素直に以下のように書きませんか?

constexpr long long INFLL = 1e18;
int main() {
  ...
  if (x == INFLL) return puts("-1"), 0;
}

キャストしてまで #define したい人はよくわからない.怖い人っぽい.

#define INFLL ((long long)1e18)

テンプレートとかやるときに constexpr まわりで意味不明が生じたりするので,思考停止でどうにかする手段としてマクロがなくはないんですが,よくはないと思う.

複文マクロ

これは有名ネタだと思ってたんですが,競プロ er のテンプレートを見てると意外と知られていなさそうなので書いておきます.

#define YES(x) printf("Yes %d", x); return 0;

複数の文からなるマクロをそのまま書くのは期待しないことになりがちなので注意しようねという話です.

int main() {
  int x;
  scanf("%d", &x);
  if (x == 1) YES(1);

  ... // x != 1 のケースでたくさんがんばる
}

これがやばいことはすぐわかりますか? わからない人はマクロ使うの避けた方がいいと思うんですけど...

えー,知らなかったんですがこれコンパイル時警告出るんですね.[-Wmultistatement-macros]

if (x == 1) printf("Yes %d\n", 1); return 0;;

の部分をわかりやすくインデントすると次のようになりますね.

if (x == 1)
  printf("Yes %d\n", 1);
return 0;
;

まぁこれは当然やばい. 知らない人は「goto fail」とかでググると面白いかもしれません?

じゃぁ,次のように書けば安全ですか?

#define YES(x) { printf("Yes %d", x); return 0; }

う〜ん,だめ!w

if (foo) YES(x);
else ...

とか書いてみましょう.これは次のようになります.

if (foo) {
  printf("Yes %d\n", x);
  return 0;
}
;
else ...

elseif に対応していなくてだめになりますね.

古くから伝わるイディオムを紹介します.次のように書くと安心です.

#define YES(x) do {       \
  printf("Yes %d\n", x);  \
  return 0;               \
} while (false)

do-while 文の最後にセミコロンを置いていないのがポイントで,マクロを使う側でセミコロンを書けるようになります.

if (foo) YES(x);
else ...

としても,

if (foo)
  do {
    printf("Yes %d\n", x);
    return 0;
  } while (false);
else ...

となり,いい感じになります.

ということで,複数の文からなるマクロを使いたいときは do { ... } while (false) の中に入れるようにしましょう.

GCC の拡張を許すのであれば,以下のように書くこともできます.

#define f(x) (void)({  \
  ...                  \
})

こっちの方が見た目は簡単ですね.関数マクロをカンマ区切りで使いたいときはこっちの方法じゃないとだめだと思います.

マクロの用法

そもそもマクロは基本的に使うべきではなくて,できる限り関数テンプレートかなんかでやるべきでしょう.

副作用の回数

たとえば,以下のような関数マクロを考えます.

#define f_macro(x, y) y = std::max(g(x), h(x))

関数テンプレートではこんな感じです.

template <typename Tp>
void f_template(Tp const& x, Tp& y) {
  y = std::max(g(x), h(x));
}

さて,整数を標準入力から受け取る関数 read_int()x に渡すことを考えてみましょうか.

f_template(read_int(), y);

では呼び出し時に一つ整数を受け取ります.一方,

f_macro(read_int(), y);

では整数が二回読み込まれる(しかも各々の整数が gh のどちらに渡るかは未規定)ことになりますね.展開すると次のようになってしまうためです.

y = std::max(g(read_int()), h(read_int()));

マクロ関数の場合は引数を二回以上使わないように気をつける(というかちょうど一回の方がいいのかな)とかをすると予期しないことになりにくいです.そんなことに気を払ってまでマクロを使いたいのはちょっと謎ですけど.

演算子の優先順序

#define f_macro(x, y) x * y

template <typename Tp>
Tp f_template(Tp const& x, Tp const& y) { return x * y; }

a+1b+1 を渡してみましょう.

f_macro(a+1, b+1);  // a+1 * b+1 == a + (1*b) + 1
f_template(a+1, b+1);  // (a+1) * (b+1)

マクロの方は期待した動作になっていますか? もちろん括弧でくくれば問題ないんですが,人は当然忘れるのでバグの元です.

例外

関数テンプレートで代替できないような,構文レベルでどうにかしたい場合に限ってマクロを使うのがいいと思います.競プロでよくある rep マクロみたいなやつですね.

他にも ## 演算子とか,そういうのを使いたい局面とかかなぁ. トークンを結合させるやつです.

#define f(a, b) a ## b
f(ho, ge)  // hoge になる

にゃんにゃん

えびちゃんは敏感なので「このコードはこういう局面で期待しないことになるでしょ」というのをすぐ気にしちゃう*1んですが,そもそも多くの人はそこまでやばいコードは書かないんですよね(たぶん).

そうは言っても,危険が起こりにくいコードを書くように心がけておく方がいいんじゃないかなぁとえびちゃん的には思っています.

*1:生きづらそうって言われる側の人間っぽい.ちょっと生きづらい.