Swiftにおけるオーバーフローについて

2015.11.23

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

1 はじめに

Swiftで次のようなコードを実行すると、プログラムは例外で落ちます。

001

これは、Intに格納できる最大値を超え、オーバーフローが発生しているためです。

原則としてオーバーフローが許されないことは、ドキュメントにも記載されています。


The Swift Programming Language (Swift 2.1) Basic Operators

Arithmetic operators (+, -, *, /, % and so forth) detect and disallow value overflow, to avoid unexpected results when working with numbers that become larger or smaller than the allowed value range of the type that stores them.

Swiftでは、Intは8バイトの巨大な器になっており、そう簡単にはオーバーする心配は無いと思いますが、ビット列の操作を行うプログラムや、他のデータ型に合わせるためにUInt8などを扱う場合、充分に注意する必要があります。

print(sizeof(Int))  // 8
print(Int.max)   // 9223372036854775807
print(Int.min)    // -9223372036854775808

//UInt8は、1バイトのサイズしかなく負の値が格納できないため0〜255しか保持できない
print(sizeof(UInt8)) // 1 
print(UInt8.max)  // 255
print(UInt8.min)  // 0

なお、ここで扱っている「オーバーフロー」に対して、「アンダーフロー」というのもありますが、ビット(桁)が足りなくなる事象という意味では、同じに考えていいと思います。

「オーバーフロー(桁あふれ)」: 演算においてビット幅を越えて桁が溢れること

「アンダーフロー(下位桁あふれ)」: 限りなく0に近づいた小数が、精度を保てなくなる下位桁が溢れること

2 オーバーフロー演算子

Swiftでは、「オーバーフロー」でも例外が発生しない演算子が用意されています。 確信犯的に桁あふれを無視して良い場合は、これを利用するのが簡単です。

var val:UInt8 = 254
print(String(val, radix: 2)) // “11111110”

val = val &+ 1
print(String(val, radix: 2)) // “11111111” 1を足して、最大値と成っている

val = val &+ 1 // 通常の演算子である + を使用していたら、ここで例外が発生している
print(String(val, radix: 2)) // “0”  最大値に1を足して、オーバーフローした

上の例では、0b11111111に1を追加した時点で、0x100000000 となるが、最上位のビットが格納できないため捨てられて、結果的に0になったことになります。

Swiftのオーバーフロー演算子には、&+の他に、&-,&*がありますが、どれも&をつけることで、通常の演算子の動作+オーバーフロー許可となります。

なお、ドキュメントのよっては、ゼロ除算も許してしまうという無敵の「除算 &/」 と「余剰 &%」もあったように紹介されていますが、現在は使えないようです。

002

オーバーフロー演算子を使用すると例外を回避することは可能ですが、計算結果がプログラムの目的とするものになっているかどうかは、当然、設計次第となりますので注意が必要です。

3 addWithOverflowメソッド

SwiftのInt構造体のリファレンスに、そのメソッドとしてaddWithOverflow というのがあります。

このメソッドでは、オーバーフロー演算子と同じように、例外を発生させずにオーバーフロー部分を切り捨てて演算を行います。また、オーバー(アンダー)フローが発生したかどうかの結果も取得することができます。

addWithOverflowメソッドは、(演算結果,オーバーフロー発生の有無)というタプル値を返します。

次の例では、最初にオーバーフローの発生していない演算、次にオーバーフローの発生する演算を行っています。

let val0:UInt8 = 254
var result = UInt8.addWithOverflow(val0, 1)

print(result.0) // “255” 結果は255
print(result.1) // “false” result.1がfalseなので、問題なく演算できている

let val1:UInt8 = 255
result = UInt8.addWithOverflow(val1, 1)

print(result.0) // “0” 結果は0
print(result.1) // “true”  オーバーフローが発生したためにresult.1がtrueになっている

なお、addWithOverflowの他にも、divideWithOverflow、multiplyWithOverflow、remainderWithOverflow、subtractWithOverflowmが用意されています。 また、このパターンは、Intに限らず、各種の値変数で定義されいます。

想定外の問題を発生させないためには、こちらでキッチリ制御する方が、型言語を使っている恩恵をより得られると言えるかも知れません。

4 ダウンキャストによる例外

次の演算はダウンキャスにおける例外の発生です。

003

最初のダウンキャストは、a=255 ( 0b11111111元々8ビットに収まっている値)を8ビットの変数bにキャストしたので問題なかったのですが、2回目のダウンキャストでは、c=256 (0b100000000 9ビットで表現されている値)を8ビットにキャストしたので例外となったのです。

ダウンキャストする前に、切り捨てが発生するかどうかに注意が必要です。 このような場合は、予め切り捨てが発生する可能性があるビットを倒してからダウンキャストをするべきだと思います。

下の例では、元々UInt16(16ビットの数値)あった c を0xFF でアンドして、上位8ビットを予め倒してからUInt8(8ビットの数値)にキャストしています。 これで想定外の例外はありえないでしょう。

var c:UInt16 = 256
var d:UInt8 = UInt8(c & 0xFF)

5 まとめ

今回は、Swiftにおけるオーバーフローについて、いろいろ確認した事項をまとめてみました。 厳格な型言語であるSwiftの警告や、例外に耳を澄ますことは大事だと思います。

6 参考リンク


Swift Integer Overflow
Swift Standard Library Reference Int Structure Reference
Int構造体のリファレンス