🤖

続:未定義動作でFizzBuzz

2023/09/24に公開

はじめに

たまにFizzBuzzが話題になりますね。いろんな解法があると思いますが、ここではC/C++言語の未定義動作を使ってFizzBuzzしてみましょうか。未定義動作でFizzBuzzではclang++にfizzを、g++にbuzzを表示してもらいましたが、今回はどちらもコンパイルはgccに任せつつ、環境で結果が変わるコードを書いてみます。とりあえずx86ならfizzを、ARMならbuzzを表示してもらうことにしましょう。

コード

以下がコードです。

#include <stdio.h>

int a = 0;
int f() {
  a += 1;
  return a;
}
int g() {
  a *= 2;
  return a;
}
int h(int a1, int a2) {
  return a1 * a2;
}

int main() {
  a = h(f(), g());
  for (int i = 0; i < 16; i++) {
    if (i % (a + 3) == 0) {
      printf("%s\n", a ? "buzz" : "fizz");
    } else {
      printf("%d\n", i);
    }
  }
}

実行結果はこんな感じになります。

x86

普通にx86のgccを使うと3の倍数の時だけfizzが表示されます。

$ gcc test.cc

fizz
1
2
fizz
4
5
fizz
7
8
fizz
10
11
fizz
13
14
fizz

AARCH64

ARMでコンパイル、実行すると5の倍数の時だけbuzzと言います。手元にARMのマシンがなかったのでクロスコンパイルしてQEMUで実行します。

$ aarch64-linux-gnu-gcc -static test.cc
$ ./a.out
buzz
1
2
3
4
buzz
6
7
8
9
buzz
11
12
13
14
buzz

ちなみにQEMUが入っていると、クロスコンパイルされた実行バイナリを検出して自動でエミュレートしてくれるので、そのまま./a.outで実行できます。

原理

何か環境依存するコードがあって、整数aが、ある環境なら0に、別の環境になら2にするようにできれば、あとは

  for (int i = 0; i < 16; i++) {
    if (i % (a + 3) == 0) {
      printf("%s\n", a ? "buzz" : "fizz");
    } else {
      printf("%d\n", i);
    }
  }

でいいわけですね。なので環境依存で0と2を作ればいいだけです。

さて、C言語では副作用を持つ処理を関数の複数の引数として渡すと、その実行順序は保証されません。したがって、

a = h(f(), g());

みたいなコードを書いたとき、先にf()が実行されるか、g()が実行されるかは処理系依存になります。それを利用します。

グローバルな変数aを0に初期化しておき、f()aに1を足す処理を、g()aを2倍する処理をします。

すると、x86のgccは、引数に渡された関数を後ろから処理するので、

int h(int a1, int a2) {
  // a1 = 0, a2 = 1
  return a1 * a2; // 0
}

と0を返します。逆に、ARMのgccは前から処理するので、

int h(int a1, int a2) {
  // a1 = 1, a2 = 2
  return a1 * a2; // 2
}

となり、2を返します。これで環境により変数が0になったり2になったりするコードが作れました。

ちなみに、副作用を持つ関数の引数をどちらから処理するかはgccとclangでも意見がわかれます。なので、

$ gcc test.cc
$ ./a.out
fizz
1
2
fizz
4
5
fizz
7
8
fizz
10
11
fizz
13
14
fizz

$ clang test.cc
$ ./a.out
buzz
1
2
3
4
buzz
6
7
8
9
buzz
11
12
13
14
buzz

と、gccとclangでも異なる結果になります。

いや、それだけの話しなんですけど。

参考

GitHubで編集を提案

Discussion