TOP >> マニアックなプログラミング
トリッキーコードネット トリッキーなコード

【芸術的な凄いプログラミング】IOCCC作品のコード解説5:入力文字列が走り去るっ!

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 _____;}
すると、あら不思議・・・、入力文字列がひゅ~~んと流れるように走り、何処へと消えていきます ^^;) IOCCC作品解説:入力文字列が走り去る (↑上の 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関数を入れると、挙動が分かりやすくなります。
トリッキーコードネット の TOPへ HOTNEWS の 総合TOPへ