小ネタ。プログラミングの合間にふと思ったちょっぴり邪悪なアイデアを実装してみました。
C++のbool型変数で、そのままでも否定してもtrueになる変数を用意できるよ、というお話です。
組み込み型に未定義の値を入れたらどうなるんだろうと思ってやってみました。
ゆるC++プログラマーを混乱させることができます。良い子のみんなは真似しないように。
おまけで書きますが、良い子のclangには効きませんでした。
前置き編
bool型変数というのは真か偽の2値のみをとる変数です。2値のみというのが当然大事な点で、いくら高度で現代的な言語でも「ちょっと間違い」とか「だいたい真」とかそういうものはbool型変数では表現されません。(そうでないとまともな論理演算もできなくなります。)
C++においてもこれは当然なのですが、ところが内部ではbool型は大抵8bitの値(正確には処理系依存)で表現されるため、実際にはかなり冗長になっています。
これはまあ身近な処理系が8bit=1Byteを1単位で扱っているため自然な表現ではあるのですが、そうすると2値以外の値も頑張れば実現可能ということになるわけです。
例えばある環境ではtrueが00000001、falseが00000000で表現されていたとすると、無理矢理01010101とかいう値をbool型に代入できるわけです。
C++を触る以上、気をつけないと未定義の世界というのはそこかしこにあるわけですが、この値はただの組み込み型でしかも一見普通に振る舞う(異常なままコピーまでできる)ためどことなく気を許してしまいます。
実装&実験編
こんなソースコードを書いて実行してみます。
//bool_test.cpp #include int main(int argc, char** argv){ bool b1 = true; char* c1 = reinterpret_cast<char*>(&b1); *c1 = 2; if(b1 == true){ std::cout << "b1 == true" << std::endl; } if(!(b1) == true){ std::cout << "(!b1) == true" << std::endl; } return 0; }
実行結果:
% g++ -o bool_test bool_test.cpp -O0 % ./bool_test b1 == true (!b1) == true
両方のif文の中に入るという一見不可解な挙動の完成です。if文で丁寧に”=true”と書いているのには特に意味はありません。なくても動きます。
環境はlinux, x86_64, gcc4.7です。ガチガチに環境依存なコードなので移植性はありません。
この環境ではfalseは00000000 (=0), trueは00000001 (=1)になります。
大事なのは6行目です。やっていることは簡単で、bool型の変数b1をC++のreinterpret_cast (ポインタ型を強制的に別のポインタ型に解釈できる)で無理矢理char型に再解釈して、char型の値を代入しています。これにより内部が00000010というbool型の出来上がりです。シンプルですが無茶苦茶です。
(もしreinterpret_castを使わないで素直に代入しようとすると、C++の通常の型システムにより「この操作はbool型に変換して代入するんだな」ということで正しい変換が行われて代入されてしまいます。)
最後のところ、b1の否定で何が起きてるのか知りたい場合はc1を出力させればわかります。実際に出力すると3 (=00000011)となっており、否定の操作が下位1bitの反転という振る舞いであることがわかります。
値のコピーにより修正されないため関数の戻り値にすることもできます。
//bool_test2.cpp #include bool crazyBool(){ bool b1 = true; char* c1 = reinterpret_cast<char*>(&b1); *c1 = 2; return b1; } int main(int argc, char** argv){ bool b1 = crazyBool(); if(b1 == true){ std::cout << "b1 == true" << std::endl; } if(!(b1) == true){ std::cout << "(!b1) == true" << std::endl; } return 0; }
実行結果:
% g++ -o bool_test2 bool_test2.cpp -O0 % ./bool_test2 b1 == true (!b1) == true
こういう関数を誰かのコードにこっそり仕組んでやれば邪悪な嫌がらせができますね。ぐへへ。
邪悪さアップ作戦
このコードにも欠点があって、最適化を施すとコンパイル時最適化が働いて正しいbool型変数になってしまうことです。
% g++ -o bool_test bool_test.cpp -O3 % ./bool_test (!b1) == true
この場合は変数をvolatile(変数が任意のタイミングで勝手に変わりうるという宣言)にすることで、目的のコードが静的に最適化できなくなるため解決(?)します。
//bool_test3.cpp #include int main(int argc, char** argv){ volatile bool b1 = true; volatile char* c1 = reinterpret_cast(&b1); *c1 = 2; if(b1 == true){ std::cout << "b1 == true" << std::endl; } if(!(b1) == true){ std::cout << "(!b1) == true" << std::endl; } return 0; }
これで邪悪さは保証されます。ぐへへ。
おまけ: clangの高い壁
さて、ここまで書いたコードもclang/LLVMの前にはまったく効き目がありません。
% clang++ -o bool_test bool_test.cpp -O0 % ./bool_test (!b1) == true
あれれ、見透かされてる?
どうしてこんなことになるのかは、オブジェクトファイルを逆アセンブルしてみればわかります。
該当行前後について、
gcc:
56: 0f b6 45 ff movzbl -0x1(%rbp),%eax 5a: 83 f0 01 xor $0x1,%eax 5d: 84 c0 test %al,%al
clang:
90: 34 01 xor $0x1,%al 92: 24 01 and $0x1,%al 94: 0f b6 c8 movzbl %al,%ecx 97: 81 f9 01 00 00 00 cmp $0x1,%ecx
なんと、下位1bitをxorで反転した後にきちんと上位n-1bitをandで0にされています。
さすがclang!
当然こういう作業が入るぶん動作は遅くなります。試しにbool型の代入をしこたま繰り返すfor文を書いて速度をとってみましたが、clangのほうが1割程度遅い結果となりました。
clangはまだgccより動作が多少遅い場合もあるとされていますが、こういう違いもあるようですね。
まあ、今回のようなコードは意図して書かないとできないわけですが、万が一自然にこんな結果が出てしまった日には(自分含む)ゆるプログラマーは絶望に襲われることでしょう。普段からきれいなコードを書くことは重要ですね、と、適当に綺麗な結論で締めくくりにしたいと思います。
ピンバック: 最近のブログ記事まとめ | ぞうさんの何でもノート