0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftAdvent Calendar 2023

Day 18

カウンターの実装例で見るオーバーフローの回避方法

Last updated at Posted at 2023-12-17

はじめに

何かをカウントするプログラムを書くとき、おそらく普通は以下のようなコードを書くのではないでしょうか。

Swift
var count: UInt64 = 0

func countUp() {
    // +1する
    count = count + 1
    print("count=\(count)")
}

countUp() // count=1
countUp() // count=2
countUp() // count=3
...

この coutUp メソッド、仮にcountが最大値( UInt64.max )に達した次はどうなるでしょうか? 正解はオーバーフローしてクラッシュします。エラーも投げられないので、いきなり死にます。潜在的なバグを仕込んでしまっているわけです。
(ただし -Ounchecked オプションを付けたビルドのときはクラッシュしないはずです: 参考)

本稿では、カウンターの実装例を通して、オーバーフローを回避する方法を紹介します。

Count構造体の作成

カウントが最大値に達した後はどうすれば良いでしょうか? 話は簡単で、 加算前に count == UInt64.max で比較し、trueなら加算しないようにすれば良いわけです。

ここで以下のようなCount構造体を作ります。

Count.swift
struct Count {
    let sum: UInt64

    init(_ initialValue: UInt64) {
        self.sum = initialValue
    }

    func countUp() -> Count {
        // UInt64の最大値を超えないように確認
        if sum == UInt64.max {
            // オーバーフローを防ぐために、現在のsumを維持(あるいはエラーを投げるなどの処理でもOK)
            return Count(sum)
        } else {
            // sumに1を加えた新しいCountオブジェクトを返す
            return Count(sum + 1)
        }
    }
}

このCount構造体の使い方は以下の通りです。

let a = Count(0)
print("a=\(a.sum)") // a=0
let b = a.countUp()
print("b=\(b.sum)") // b=1

let x = Count(.max)
print("x=\(x.sum)") // x=18446744073709551615
let y = x.countUp()
print("y=\(y.sum)") // y=18446744073709551615 <- 最大値のまま

x.countUp() が返した y は最大値でカウンターストップしていることが分かります。

上記の例では、最大値でカンストするようにしていますが、エラーを投げるようにして適切なエラーハンドリングをするやり方も考えられます。

これで安心して何かをカウントすることができますね。

応用編

ところで、 countUp メソッドは常に+1するだけでしたが、任意の整数を加算したいこともあります。次に add(value:) メソッドを考えてみましょう。先程のCount構造体に追加します。

Count.swift
struct Count {
    let sum: UInt64
    
    init(_ initialValue: UInt64) {
        self.sum = initialValue
    }
    
    func countUp() -> Count {
        // UInt64の最大値を超えないように確認
        if sum == UInt64.max {
            // オーバーフローを防ぐために、現在のsumを維持
            return Count(sum)
        } else {
            // sumに1を加えた新しいCountオブジェクトを返す
            return Count(sum + 1)
        }
    }

    func add(value: UInt64) -> Count {
        // UInt64の最大値を超えないように確認
        if sum + value > UInt64.max {
            // オーバーフローを防ぐために、現在のsumを維持
            return Count(sum)
        } else {
            // sumにvalueを加えた新しいCountオブジェクトを返す
            return Count(sum + value)
        }
    }
}

このコードは一見正しそうですが、クラッシュする可能性があります。なぜなら sum + value の演算時点でオーバーフローする可能性があるからです。

ではどうすれば良いでしょうか? SwiftのUInt64構造体には便利な addingReportingOverflow というメソッドが用意されています。

UInt64以外のInt系の構造体にも addingReportingOverflow メソッドは用意されています。

以下は addingReportingOverflow メソッドを利用した実装です。

Count.swift
struct Count {
    let sum: UInt64
    
    init(_ initialValue: UInt64) {
        self.sum = initialValue
    }
    
    func countUp() -> Count {
        // UInt64の最大値を超えないように確認
        if sum == UInt64.max {
            // オーバーフローを防ぐために、現在のsumを維持
            return Count(sum)
        } else {
            // sumに1を加えた新しいCountオブジェクトを返す
            return Count(sum + 1)
        }
    }

    func add(value: UInt64) -> Count {
        let (next, overflow) = sum.addingReportingOverflow(value)
        if overflow {
            // オーバーフローしたため、現在のsumを維持
            return Count(sum)
        } else {
            // 加算結果を新しいCountオブジェクトに詰めて返す
            return Count(next)
        }
    }
}

addingReportingOverflow メソッドは加算結果とオーバーフローした場合trueになるBool値を返します。この overflow フラグをチェックすることで安全に加算することができます。

以下は add(value:) メソッドの使用例です。

let a = Count(3).add(value: 2)
print("a=\(a.sum)") // a=5

let x = Count(.max).add(value: 2)
print("x=\(x.sum)") // x=18446744073709551615 <- 最大値のまま

x はオーバーフローせずに最大値でカウンターストップしていることが分かります。

ちなみに、乗算のときも同様に multipliedReportingOverflow(by:) メソッドが用意されています。扱う数字が巨大なときは、このメソッドを利用した方が安全かもしれません。

まとめ

現実的には最大値までカウンターが回ることは滅多にないかもしれません。ただ、Intの加算には上限があるんだよ、ということを頭の片隅に入れておくと良いと思います。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?