1985年(第二回)IOCCCの入賞作品をご紹介します。
下記のコードをパラメータ無しで実行し、長~い文字列を入力します。
#define o define
#o ___o write
#o ooo (unsigned)
#o o_o_ 1
#o _o_ char
#o _oo goto
#o _oo_ read
#o o_o for
#o o_ main
#o o__ if
#o oo_ 0
#o _o(_,__,___)(void)___o(_,__,ooo(___))
#o __o (o_o_<<((o_o_<<(o_o_<<o_o_))+(o_o_<<o_o_)))+(o_o_<<(o_o_<<(o_o_<<o_o_)))
o_(){_o_ _=oo_,__,___,____[__o];_oo ______;_____:___=__o-o_o_; _______:
_o(o_o_,____,__=(_-o_o_<___?_-o_o_:___));o_o(;__;_o(o_o_,"\b",o_o_),__--);
_o(o_o_," ",o_o_);o__(--___)_oo _______;_o(o_o_,"\n",o_o_);______:o__(_=_oo_(
oo_,____,__o))_oo _____;}
すると、あら不思議・・・、入力文字列がひゅ~~んと流れるように走り、何処へと消えていきます ^^;)
(↑上の aaa... が入力文字列、その下の aaaaa が、走りながらだんだんと短くなって消えて行く最中の文字列です。)
相変わらずIOCCCのコードは、パッと見 意味不明なコードですが、とりあえず正常に動作する事を確認してしまったので(笑)、解説を始めます。
まず、冒頭のコードをVC++2010で実行すると、次の様なエラーが出てコンパイラに怒られます。
----------------------------------------------------------------
fatal error C1021: プリプロセッサ コマンド 'o' が無効です。
----------------------------------------------------------------
#oより先に「#define o define」が定義してあるので、 プリプロセッサが#oを#defineと解釈してくれてもよさそうなのですが、
どうやらVC++2010ではそうもいかないみたいです。
そこで、冒頭のコードを以下の様に書き換えます。
#define ___o write
#define ooo (unsigned)
#define o_o_ 1
#define _o_ char
#define _oo goto
#define _oo_ read
#define o_o for
#define o_ main
#define o__ if
#define oo_ 0
#define _o(_,__,___)(void)___o(_,__,ooo(___))
#define __o (o_o_<<((o_o_<<(o_o_<<o_o_))+(o_o_<<o_o_)))+(o_o_<<(o_o_<<(o_o_<<o_o_)))
o_(){_o_ _=oo_,__,___,____[__o];_oo ______;_____:___=__o-o_o_; _______:
_o(o_o_,____,__=(_-o_o_<___?_-o_o_:___));o_o(;__;_o(o_o_,"\b",o_o_),__--);
_o(o_o_," ",o_o_);o__(--___)_oo _______;_o(o_o_,"\n",o_o_);______:o__(_=_oo_(
oo_,____,__o))_oo _____;}
これで、VC++2010でもコンパイルが通るようになりました^^;)
つづいて、ここで紹介した技を使い、VC++にプリプロセッサの実行結果を表示させます。
main(){char _=0,__,___,____[(1<<((1<<(1<<1))+(1<<1)))+(1<<(1<<(1<<1)))];goto ______;_____:___=(1<<((1<<(1<<1))+(1<<1)))+(1<<(1<<(1<<1)))-1; _______:
(void)write(1,____,(unsigned)(__=(_-1<___?_-1:___)));for(;__;(void)write(1,"\b",(unsigned)(1)),__--);
(void)write(1," ",(unsigned)(1));if(--___)goto _______;(void)write(1,"\n",(unsigned)(1));______:if(_=read(
0,____,(1<<((1<<(1<<1))+(1<<1)))+(1<<(1<<(1<<1)))))goto _____;}
そして、このままでは非常に読み辛い為、適当なインデントを入れます。
main()
{
char _ = 0,__,___,____[(1<<((1<<(1<<1))+(1<<1)))+(1<<(1<<(1<<1)))];
goto ______;
_____:
___ = (1<<((1<<(1<<1))+(1<<1)))+(1<<(1<<(1<<1)))-1;
_______:
(void)write(1,____,(unsigned)(__=(_-1<___?_-1:___)));
for (;__;(void)write(1,"\b",(unsigned)(1)),__--);
(void)write(1," ",(unsigned)(1));
if(--___)
goto _______;
(void)write(1,"\n",(unsigned)(1));
______:
if(_=read(0,____,(1<<((1<<(1<<1))+(1<<1)))+(1<<(1<<(1<<1)))))
goto _____;
}
これで、なんとな~くプログラムの構造が見えてきたんじゃないかな??という気がします^^;)
しかし、まだまだコードが読み辛い為、変数名・ラベル名を置換します。
1 : main()
2 : {
3 : char c1 = 0, c2, c3, c4[(1<<((1<<(1<<1))+(1<<1)))+(1<<(1<<(1<<1)))];
4 : goto LABEL6;
5 :
6 :
7 : LABEL5:
8 : c3 = (1<<((1<<(1<<1))+(1<<1)))+(1<<(1<<(1<<1)))-1;
9 :
10 :
11 : LABEL7:
12 : (void)write(1, c4, (unsigned)(c2 = (c1-1 < c3 ? c1-1: c3)));
13 :
14 : for (; c2; (void)write(1, "\b", (unsigned)(1)), c2--);
15 :
16 : (void)write(1, " ", (unsigned)(1));
17 :
18 : if (--c3)
19 : goto LABEL7;
20 :
21 : (void)write(1, "\n", (unsigned)(1));
22 :
23 :
24 : LABEL6:
25 :
26 : if (c1 = read(0, c4, (1<<((1<<(1<<1))+(1<<1)))+(1<<(1<<(1<<1)))))
27 : goto LABEL5;
28 : }
これで下準備は終わりました。
後は、先頭から順にソースコードを読んでいくだけです。
3行目で宣言されているc4は、char型が80個の配列です。
∵
(1<<((1<<(1<<1))+(1<<1))) = 64,
(1<<(1<<(1<<1))) = 16
で、 64 + 16 = 80!
同様に、8行目の c3の中には79が代入され、26行目のread関数の第三パラメータには80が渡されます。
これらを踏まえると、コードは以下の様に修正できます。
1 : main()
2 : {
3 : char c1 = 0, c2, c3, c4[80];
4 : goto LABEL6;
5 :
6 :
7 : LABEL5:
8 : c3 = 79;
9 :
10 :
11 : LABEL7:
12 : (void)write(1, c4, (unsigned)(c2 = (c1-1 < c3 ? c1-1: c3)));
13 :
14 : for (; c2; (void)write(1, "\b", (unsigned)(1)), c2--);
15 :
16 : (void)write(1, " ", (unsigned)(1));
17 :
18 : if (--c3)
19 : goto LABEL7;
20 :
21 : (void)write(1, "\n", (unsigned)(1));
22 :
23 :
24 : LABEL6:
25 :
26 : if (c1 = read(0, c4, 80))
27 : goto LABEL5;
28 : }
プログラムが開始され、変数を宣言した後、4行目のgotoで24行目にジャンプします。
26行目では、標準入力から 配列c4へ、80byte分データを読み込みます。
そして、読み込んだデータサイズを 変数c1へ代入します。
(※ ただEnterキーを押しただけでも、配列c4の中には改行が入り、変数c1には1が代入されます)
27行目のgotoで、7行目へとジャンプします。
8行目では、変数c3に 配列c4の最後の添字を格納します。
12行目では、標準出力に 配列c4の値を書き込み(表示)します。
書き込むサイズは、入力文字数長 と 配列の最大添字の、どちらか小さい方です。
その「書き込みサイズ」を、変数c2に代入します。
14行目では、その変数c2の値だけ、ループでバックスペースキーを表示します。
そして、16行目でスペースを表示し、18行目で変数c3の値をデクリメントします。
変数c3の値が 0以上であれば、19行目のgotoで再び11行目へと戻ります。
つまり、
12~19行目の繰り返しにより、配列c4の表示位置と長さが徐々にずれて行く事で、入力文字列が走り去っていくように見える
というわけでした~~^^;)
そして、文字列が走り去った後は、
21行目で改行を表示し、再び26行目で文字列入力を受け付ける、(以下繰り返し)
めでたし、めでたし・・・チャンチャン♪
※
このコードのロジックがいまいち見えてこない人は、
16行目でスペースの変わりに別の文字を表示させたり、17行目にsleep関数を入れると、挙動が分かりやすくなります。