LoginSignup
3
1

More than 5 years have passed since last update.

なんで言ってくれなかったの? オーバーフローしてたって...

Posted at

前置きをスキップするときは「オーバーフローの検出」に進んでください。

F#はMicrosoftのDon Syme氏が開発したプログラミング言語で、命令型、オブジェクト指向、関数型の要素を併せ持つマルチパラダイム言語と位置付けられています。実装はオープンソースのMITライセンスのもと、githubのリポジトリで公開されています。

実行環境は.NET Frameworkおよび.NET CoreやMonoといったマルチプラットフォームに対応していますが、サポート状況については各プラットフォームごとの情報を参照してください(「F# の概要」など)。

言ってくれないオーバーフロー

F#のデフォルト環境では、オーバーフローが起きても警告や例外を全く発しない場面があります。たとえばこんな計算。

これは何の問題もない
255 + 1 = 256

255に1を足したら256になるはずです。しかし、これをbyte型に変えてみるとどうでしょうか。

結果は0
255uy + 1uy = 0uy    // (?!)

255uy1uyを足すと結果は0uになってしまいます。これがオーバーフローの一例です。

計算以外でもオーバーフローを起こす場合があります。それはキャスト(型変換)です。値によっては正負が逆転してしまうこともあります。

キャストで正負が逆転
sbyte 384 = -128y

これでも、やはり警告や例外は出ません。こうした現象をわざと利用する場合もないことはないでしょうが、とはいえ「なんで言ってくれないの?」と思うこともあるのではないでしょうか。

そこで、ここではなぜオーバーフローが起こるのかを探り、それを検出する方法を紹介します。F#のバージョンは4.1(FSharp.Core 4.4.1.0)とします。

なぜオーバーフローするのか

オーバーフローは桁あふれともいいますが、設定しようとしている値が数値型で扱える範囲を超えてしまうことをいいます。F#では、定数で数値を表すときにuyなどの接尾辞をつけられます。これにより簡単に数値型を表せると同時に、オーバーフローの可能性も見通しやすくなります。ただし、これは数値の定数を表す場合に限られます。

F#では型推論のおかげで型名を書かなくて済むことも多いため、ソースコードを見ただけではオーバーフローが起こる可能性のある個所を見落としてしまうリスクがあります。結果を見て初めてオーバーフローしていたと気づく場面もあるかもしれません。

演算によるオーバーフロー

byte型では、扱える数値の範囲が0uy~255uyの整数と決まっています。これは符号なし8ビットで表せる数値を表しています。この型でオーバーフローが起きるのは、結果が255を上回るときと、0を下回るときです。

byte型でのオーバーフローの例
255uy + 1uy = 0uy    // 結果が255を上回るときのオーバーフロー (本来なら256)
0uy - 1uy = 255uy    // 結果が0を下回るときのオーバーフロー (本来なら-1)

255 + 1の結果は本来256ですが、255uy + 1uyではオーバーフローして0uyと値が小さくなってしまいます。2進法で表すと255は0b1111_1111となります(最下位ビットから上位に4桁ごとで区切っています)。これに1を加えると、本来なら0b1_0000_0000となるのですが、byte型は8ビットですから桁数が足りません(ここではビット数を2進数での桁数とします)。そのため最上位の桁はメモリに収まらず(桁あふれを起こして)、結果が0uyとなるのです。

次の0uy - 1uyの結果は255uyで、上記とは逆に数値が大きくなります。これがなぜオーバーフローかというと、最上位よりも上位の桁から1を借りようとするからです。

ただ、実際の引き算(-:減算)では2の補数(すべてのビットを反転させて1を足した値で、正負を反転させた値に相当)を足すという処理が行われるため、この計算では桁借り(繰り下がり)が起こりません(qiita内にも2の補数についての説明がいくつかあるようです)。

では0uy - 1uyはどう計算するかというと、これを0uy + (-1)ととらえ、-11uyの2の補数、つまり~~~1uy + 1uy = 255uyで置き換えます(byte型は符号なしなので負の数にはなりません)。よって0uy - 1uy0uy + 255uyに置き換えられ、結果が255uyとなるのです。

こうした処理は、定数とuyなどの接尾辞が書かれていれば気づきやすいですが、関数内でどのような値がどう使われているかがわからなければ気づかないかもしれません。そうなると、オーバーフローが起こる可能性のある個所を特定するのが難しくなります。

キャストによるオーバーフロー

今度はキャストによるオーバーフローを探っていきます。

F#の数値型は扱える範囲が異なるものが複数存在するため、範囲の広さや符号の有無によって、キャスト(型変換)の際に意図しない結果を生むことがあります。

以下はできれば行うべきでないキャストの例です。

ここではint16型からsbyte型にキャストする場合を取り上げます。

キャスト(型変換)によるオーバーフローの例
sbyte  384s = -128y   // 正の数 → 負の数
sbyte -192s =  64y    // 負の数 → 正の数

sbyte 384s = -128yint16(16ビット)からsbyte(8ビット)へのキャストです。どちらも符号ありの型なので負の数を扱えますが、キャストによってこのように正負が逆転してしまうことがあります。扱える数値の範囲が広い型からそれが狭い型に変換されるときは、上位のビットが削られます。384は2進法で0b1_1000_0000と表されますが、下位8ビットだけを見ると0b1000_0000です。これをsbyte型では-128yとして扱います(最上位ビット1は負の数を表すため)。よってsbyte 384s = -128yとなり、正負が反転してしまうのです。

一方、sbyte -192s = 64yですが、-192sは2進法で表すと0b1111_1111_0100_0000sです。下位8ビットだけに注目すれば0b0100_0000で、これが結果の64yです(最上位ビット0は正の数を表す)。よって、この場合も正負が逆転してしまうのです。

ここまでオーバーフローの原因として演算とキャストを取り上げましたが、こうした処理が行われても、F#のデフォルトでは警告も例外も発してくれません。ですが、F#ではこれを検出する方法が提供されていますので、これを使えばオーバーフローが起きた時でも対応できるようになります。

オーバーフローの検出

F#における演算とキャストによるオーバーフローを検出するにはMicrosoft.FSharp.Core.Operators.Checkedモジュールを設定します。元に戻すにはMicrosoft.FSharp.Core.Operatorsモジュールを設定し直します。

オーバーフローを検出する設定
open Microsoft.FSharp.Core.Operators.Checked  // を実行
設定を戻す場合
open Microsoft.FSharp.Core.Operators  // を実行

このCheckedモジュールが設定されると、上記のような演算およびキャスト(型変換)によってオーバーフローが起きたときにSystem.OverflowExceptionが発生するため、オーバーフローを検出できるようになります。そのため、たとえばオーバーフローが発生する可能性がある処理を行うときは結果をoption型にするといった対応もとれます。

オーバーフローを検出する可能性がある処理の結果をoption型にする
open Microsoft.FSharp.Core.Operators.Checked    // オーバーフローを検出する設定

// オーバーフローが起こる可能性がある処理
let f x = x + 1uy

// オーバーフローの検出を考慮して結果をoption型にする
let tryOption f x =
    try Some(f x) with
    | :? OverflowException -> None
    | _ -> reraise()

tryOption f 255uy  // None     (オーバーフローを検出した)
tryOption f 1uy    // Some 2uy (オーバーフローを検出しなかった)

この設定で対応できる演算子は以下のとおりです。

  • 掛け算 - (*)
  • 足し算 - (+)
  • 引き算 - (-)
  • マイナス(正負の反転) - (~-)
オーバーフローで例外発生
open Microsoft.FSharp.Core.Operators.Checked

// 以下はすべて例外発生「System.OverflowException: 算術演算の結果オーバーフローが発生しました。」
128uy * 3uy
255uy + 1uy
-128y - 1y

同じくこのモジュールで対応できるキャストの型は以下のとおりです。

  • byte, sbyte
  • char
  • int, int16, int32, int64
  • uint16, uint32, uint64
  • nativeint, unativeint
オーバーフローで例外発生
open Microsoft.FSharp.Core.Operators.Checked

// 以下はすべて例外発生「System.OverflowException: 算術演算の結果オーバーフローが発生しました。」
byte    256
uint16 -128y

デフォルトでオーバーフロー時に例外発生する処理

ここまで、F#におけるオーバーフローの例と、それを検出する方法を取り上げましたが、上記の設定をしなくてもオーバーフローによる例外が発生する処理もありますので、最後にその例を紹介しておきます。

デフォルトでもオーバーフローによる例外が発生する例
pown 2 31                         // 例外発生「System.OverflowException: 算術演算の結果オーバーフローが発生しました。」
System.Decimal.MaxValue + 0.5M    // 例外発生「System.OverflowException: Decimal 型の値が大きすぎるか、または小さすぎます。」

F#を使う皆さんの参考になれば幸いです。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1