本日 3 月 5 日は国際ビット演算デーです。嘘です。
折しも 1 ビット LLM がなにやら世間では話題ですが、筆者がそんな高度な話題に言及できるわけがないので、低度な話題です。
ビット演算子の罠
よくビット演算を書くときはカッコで括るようにと言われます。その理由としてビット演算子の優先順位が関係演算子のそれよりも低い言語があることが挙げられます。
例えば、以下の C 言語のコードを見てください。
if (flags & mask != 0) {
// some operation
}
このコードは一見すると flags
と mask
で AND をとり、その結果が非ゼロのときに実行される…すなわち、flags
のビットの中に mask
のビットがあれば実行する、というコードに見えます。しかし、flags & mask
よりも mask != 0
が先に解決されるため、
-
mask
はおそらく非ゼロであるため、この部分は真を示す非ゼロの値(多くの場合は1
)を返す。 -
その値と
flags
との AND をとる -
それが非ゼロであれば実行する
という意図しない動作を引き起こします。本当は mask
と比較したかったのに 1
と比較することになっていますね。
故に、以下のように書く必要があります。
if (flags & mask) {
// some operation
}
おっと間違えた。
if ((flags & mask) != 0) {
// some operation
}
なぜビット演算子の優先順位は低いのか
このルールは一見すると奇妙です。普通に考えて比較よりも二項演算を先に解決してほしいですよね? 共感していただいたと仮定して話を進めます。
もちろん我々に嫌がらせをするためにこのような仕様を作っているわけではありません。一言で言うならば、これは歴史的な理由です。
それというのは、どうやらかつては論理演算子の役割をビット演算子が担っていたようなのです。実際、0
と 1
に対してビット演算を行うと、以下の表に示すように AND と OR はそれぞれ連言と選言に相当することがわかります。
P | Q | P ∧ Q | P ∨ Q |
---|---|---|---|
偽 | 偽 | 偽 | 偽 |
偽 | 真 | 偽 | 真 |
真 | 偽 | 偽 | 真 |
真 | 真 | 真 | 偽 |
x |
y |
x & y |
x | y |
---|---|---|---|
0 |
0 |
0 |
0 |
0 |
1 |
0 |
1 |
1 |
0 |
0 |
1 |
1 |
1 |
1 |
1 |
ビット演算子を論理演算子と思えば、たとえば lisence_duration >= 1 & age < 70
とかはどう考えても関係演算子の方を先に評価してほしいことに納得いただけるかと思います。実際、いまでもビット演算子を「短絡しない論理演算子」として紹介している教本も多いはずです。ビット演算子といってもシフト系の演算子はちゃんと関係演算子より先に評価することが多いのも、論理演算と無関係だからでしょう。
しかし、現代の多くの言語には専用の論理演算子が存在しています。聞いているのか VBA。 短絡しない論理演算子を使いたいタイミングとビット演算を使いたいタイミング、後者のほうが多い気もします。
それでも、最初から論理演算子を持っていたとしても古い言語に合わせてビット演算子の順位を低くしている言語が多いです。とくに、現代の言語の多くはその文法などに C 言語を下敷きにしているので、C から入ったプログラマが混乱しないようにと踏襲している感がありますね。
しかし! その一方で、ちゃんと(?)ビット演算子を関係演算子よりも先に評価する言語も増えてきています。こういう言語ではいちいちカッコで括らなくてもバグらないということになります。
まあ、こういう事を言うと「常にカッコでくくる習慣をつけるべき」と言われそうですし、実際正論なんですが。でも 1 + (5 * 2)
みたいな式を書いているような気分になってきて…ごにょごにょ。
言語別! ビット演算子と関係演算子、どっちを先に評価する? 早見表
ビット演算子を先に評価する言語
- Haskell
- Python
- Lua
- Ruby
- Go
- Rust
- Kotlin
- Dart
- Swift
関係演算子を先に評価する言語
- C++
- Perl
- Visual Basic
- Visual Basic for Applications
- Java
- JavaScript
- PHP
- C#
言語別詳細
C++ - 1983 年
【乗法演算子 > 加法演算子 > シフト演算子 > 関係演算子 > ビット演算子 > 論理演算子】となっています。典型的な関係演算子の方を先に評価する言語です。
ビット演算子内での順序は &
> ^
> |
となっています。これも典型的な順序です。
参考: C++ の組み込み演算子、優先順位、結合規則 | Microsoft Learn
Perl - 1987 年
【乗法演算子 > 加法演算子 > シフト演算子 > 関係演算子 > ビット演算子 > 論理演算子】となっています。典型的な関係演算子の方を先に評価する言語です。
ビット演算子内での順序は &
> ^
= |
です。OR と XOR が同じ順序になっています。
参考: perlop - Perl の演算子と優先順位 - perldoc.jp
Haskell - 1990 年
一般的には、【シフト演算子 > 乗法演算子 = ビット AND > 加法演算子 = ビット XOR > ビット OR > 関係演算子 > 論理演算子】となっています。通常の算術演算子と複雑に絡み合っていますが、一応ビット演算子を先に評価します。
で、「一般的には」という但し書きについてですが、そもそも Haskell は演算子が言語に組み込まれていないので、ビット演算子(に限らずほぼすべての演算子が、ですが)の順序は読み込むライブラリによって変わってきます。
Haskell でのビット演算には言語公式が出している Data.Bits
というライブラリを使うのが一般的なので、そのときに限りこのようになる、ということです。
Visual Basic - 1991 年
【乗法演算子 > 加法演算子 > シフト演算子 > 関係演算子 > ビット演算子 = 論理演算子】となっています。関係演算子の方を先に評価します。
よく見ると【ビット演算子 = 論理演算子】となっています。Visual Basic はまさしく上で話していた「ビット演算子を論理演算子として扱っていた言語」であり、専用の(短絡する)論理演算子は VB.NET になってから入りました。その影響か、対応する論理演算子と全く同じ順序となっているのです。
ビット演算子内での順序は And
> Or
> Xor
となっています。こちらは XOR のほうが低いようです。
参考: 演算子の優先順位 - Visual Basic | Microsoft Learn
Python - 1991 年
【乗法演算子 > 加法演算子 > シフト演算子 > ビット演算子 > 関係演算子 > 論理演算子】となっています。ビット演算子を先に評価する言語です。
ビット演算子内での順序は &
> ^
> |
です。
参考: 6. 式 (expression) ― Python 3.12.2 ドキュメント
Lua - 1993 年
【乗法演算子 > 加法演算子 > シフト演算子 > ビット演算子 > 関係演算子 > 論理演算子】となっています。ビット演算子を先に評価する言語です。
ビット演算子内での順序は &
> ~
> |
です。べき乗に ^
を使用しているため、Lua でのビット XOR は ~
です。
ちなみにビット演算子が導入されたのは結構遅く 5.3 からなので、2015 年とするべきかもしれません(てかちゃんと調べてないので他の言語もこういうことありそうですね)。
参考: 3.4 式 - Lua 5.4 リファレンスマニュアル (翻訳) - inzkyk.xyz
Visual Basic for Applications - 1993 年
【乗法演算子 > 加法演算子 > 関係演算子 > ビット演算子】となっています。関係演算子を先に評価する言語です。
進化前の VB といった感じです。論理演算子はありませんのでビット演算子で代用します。また、シフト演算子もありません。
ビット演算子内での順序は VB と同様に And
> Or
> Xor
です。
参考: 演算子の優先順位 | Microsoft Learn
Java - 1995 年
【乗法演算子 > 加法演算子 > シフト演算子 > 関係演算子 > ビット演算子 > 論理演算子】となっています。典型的な関係演算子の方を先に評価する言語です。
ビット演算子内での順序は &
> ^
> |
です。
参考: Operators (The Java™ Tutorials > Learning the Java Language > Language Basics)
JavaScript - 1995 年
【乗法演算子 > 加法演算子 > シフト演算子 > 関係演算子 > ビット演算子 > 論理演算子】となっています。典型的な関係演算子の方を先に評価する言語です。
ビット演算子内での順序は &
> ^
> |
です。
参考: 演算子の優先順位 - JavaScript | MDN
PHP - 1995 年
【乗法演算子 > 加法演算子 > シフト演算子 > 関係演算子 > ビット演算子 > 論理演算子】となっています。典型的な関係演算子の方を先に評価する言語です。
ビット演算子内での順序は &
> ^
> |
です。
Ruby - 1995 年
【乗法演算子 > 加法演算子 > シフト演算子 > ビット演算子 > 関係演算子 > 論理演算子】となっています。ビット演算子を先に評価する言語です。
ビット演算子内での順序は &
> ^
= |
です。OR と XOR が同じ順序になっています。
参考: 演算子式 (Ruby 3.3 リファレンスマニュアル)
C# - 2000 年
【乗法演算子 > 加法演算子 > シフト演算子 > 関係演算子 > ビット演算子 > 論理演算子】となっています。典型的な関係演算子の方を先に評価する言語です。
ビット演算子内での順序は &
> ^
> |
です。
参考: C# 演算子と式 - C# リファレンス | Microsoft Learn
Go - 2009 年
【乗法演算子 = ビット AND = シフト演算子 > 加法演算子 = ビット XOR = ビット OR > 関係演算子 > 論理演算子】となっています。ビット AND とシフトは乗法系、ビット XOR とビット OR は加法系としてまとめて扱われており、ビット演算を先に評価することになります。
参考: The Go Programming Language Specification - The Go Programming Language
Rust - 2010 年
【乗法演算子 > 加法演算子 > シフト演算子 > ビット演算子 > 関係演算子 > 論理演算子】となっています。ビット演算子を先に評価する言語です。
ビット演算子内での順序は &
> ^
> |
です。
参考: Expressions - The Rust Reference
Kotlin - 2011 年
あえて書くなら【乗法演算子 > 加法演算子 > シフト演算子 = ビット演算子 > 関係演算子 > 論理演算子】となります。
ビット演算子を先に評価する言語、といえなくもないですが、正確に言うとシフトやビット演算には演算子が存在しておらず、Int
や Long
に生えているメソッドです。このメソッドを Infix 記法で演算子風に書いた場合、上記の順序で解決されます。
Infix 記法はすべて同等の順序で解決されるため、ビット演算子(?)内での順序も当然 and
= xor
= or
です。
参考: Grammar、Int - Kotlin Programming Language
Dart - 2011 年
【乗法演算子 > 加法演算子 > シフト演算子 > ビット演算子 > 関係演算子 > 論理演算子】となっています。ビット演算子を先に評価する言語です。
ビット演算子内での順序は &
> ^
> |
です。
参考: Operators | Dart
Swift - 2014 年
【シフト演算子 > 乗法演算子 = ビット AND > 加法演算子 = ビット XOR = ビット OR > 関係演算子 > 論理演算子】となっています。ビット AND は乗法系、ビット XOR とビット OR は加法系としてまとめて扱われており、ビット演算を先に評価することになります。