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

# はじめに

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

```swift: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 オプションを付けたビルドのときはクラッシュしないはずです: [参考](https://developer.apple.com/documentation/swift/uint64/+=(_:_:)-38bug))

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

# Count構造体の作成

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

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

```swift: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構造体の使い方は以下の通りです。

```swift
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構造体に追加します。

```swift: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`](https://developer.apple.com/documentation/swift/uint64/addingreportingoverflow(_:))** というメソッドが用意されています。

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

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

```swift: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:)` メソッドの使用例です。

```swift
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` はオーバーフローせずに最大値でカウンターストップしていることが分かります。

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

# まとめ

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