RB_TEST_P(x)
https://github.com/ruby/ruby/blob/ruby_2_6/include/ruby/ruby.h#L478
#define RB_TEST(v) !(((VALUE)(v) & (VALUE)~RUBY_Qnil) == 0) #define RB_NIL_P(v) !((VALUE)(v) != RUBY_Qnil) #define RTEST(v) RB_TEST(v) #define NIL_P(v) RB_NIL_P(v)
RB_TEST(v)
の定義はこれだった。これは、x が false か nil 以外だったら真、というもの。Ruby では nil もしくは false だったら偽、という定義と同じものだ。C コード中で if (RB_TEST(v)) {...}
みたいに、Ruby 的に真だったら、みたいに書く時に使う。
ついでに、NIL_P(v) の定義もある。Qnil
それと等しいかどうか、をチェックしている。いちいち、!(v != Qnil)
と二重否定にしているのは、... なんで?
https://github.com/ruby/ruby/commit/8497a7b9187a05210a4f6b842d06d1268d704d35
if NIL_P(x) { ... }
と(if RTEST(x) { ... }
だったっけ? という話も)C で書いたパッチが来たから、括弧を省略できないようにしたらしい...。
そういえば、例によって RB_TEST に RTEST、RB_NIL_P に NIL_P、RUBY_Qnil
には Qnil
という別名がついている。歴史的には逆で、先に RTEST とかがあった。Ruby の API であることを強調するために RB_ prefix を付けている。
さて、RB_TEST(v)
の実装に戻ると、!((v & ~Qnil) == 0)
だったら真らしい。このビット演算は何をしているのか?
Ruby において、偽の値は fales
と nil
で、それぞれ C では Qfalse
、Qnil
という値である。つまり、素直に書くと、!((v) == Qnil) || (v) == Qfalse)
とすれば良い。それがなぜ論理演算になるのか?
実は、Qfalse
は 0 で、Qnil
は 0b0100 == 0x08
という値である。展開すると !(v == 0x08 || v == 0x00)
こうなる。つまり、v が 0b0000 もしくは 0b0100 であれば偽ということになる。この二つの違いは 0b0100 の 1 bit だけなので、この bit 以外が 0 であれば、Qfalse もしくは Qnil である、と言える。つまり、v の 3 bit 目を 0 にしておけば、あとは全体が 0 であるかをチェックすれば偽であると言える。これを実装するのが (v & ~Qnil) == 0
という式である。
なぜ論理式にするか、というと、分岐が1個減って、もしかしたらちょっと速くなるかも、ということで、こういう式になっている。
ちなみに、gcc では、最適化オプションによって、素直に (v == Qnil || v == Qfalse)
と書いても (v & ~Qnil) == 0
と同じコードが出力される。賢い。clang では、真面目に2回チェックしていた。