C++のunsigned変数とは、負の値を持たない整数型のことです。
通常の整数型であるintは、負の値・0・正の値を扱えます。
一方、unsigned intは0以上の値だけを扱います。
たとえば、一般的な32bit環境では、intはおおよそ「-2,147,483,648〜2,147,483,647」の範囲を扱います。
それに対して、unsigned intは「0〜4,294,967,295」の範囲を扱います。
つまり、unsignedは負の範囲を持たない代わりに、正の方向へより大きな値を表せる整数型です。
ただし、unsignedは「負の値が入らないから安全な型」と単純に考えるべきではありません。
C++では、unsignedを使うことで、かえって予期しないバグが発生することがあります。
特に注意が必要なのは、次のような場面です。
- 負の値を代入したとき
- 減算によって0未満になる可能性があるとき
signed型とunsigned型を比較するとき- 逆順ループを書くとき
std::vector::size()などの戻り値と比較するとき
unsignedを正しく理解するには、「非負の安全な整数」ではなく、回り込みの性質を持つ符号なし整数として捉えることが大切です。
unsignedの基本的な意味
unsignedは符号なし整数を表す
unsignedは、日本語では「符号なし」と訳されます。
ここでいう「符号」とは、数値がプラスかマイナスかを表す情報のことです。
通常のintは符号付き整数です。
つまり、正の値も負の値も扱えます。
一方、unsigned intは符号なし整数なので、負の値を扱えません。
そのため、値の範囲は0以上になります。
unsignedだけ書いた場合はunsigned intになる
C++では、unsignedとだけ書いた場合、通常はunsigned intを意味します。
そのため、unsignedとunsigned intは基本的に同じ意味として扱われます。
ただし、実務では可読性を考えて、あえてunsigned intと書く場合もあります。
また、サイズを明確にしたい場合は、std::uint32_tやstd::uint64_tのような固定幅整数型を使うこともあります。
unsignedの値の範囲
signed intとunsigned intの違い
intとunsigned intは、同じビット数でも表現できる範囲が異なります。
一般的な32bit環境では、intは負の値と正の値の両方を扱います。
一方、unsigned intは負の値を扱わず、すべての範囲を0以上の値に使います。
そのため、unsigned intはintよりも正の方向に大きな値を扱えます。
ただし、これは「unsigned intの方が常に優れている」という意味ではありません。
正の範囲が広い反面、負の値を扱えないことによる落とし穴があります。
unsignedの最大値は環境によって変わる
unsigned intの具体的な最大値は、処理系や環境によって異なる場合があります。
ただし、現在の多くの環境では32bitのunsigned intが使われることが多く、その場合の最大値は4,294,967,295です。
一方、unsigned longやunsigned long longは、より大きな範囲を持つことがあります。
サイズを明確にしたい場合は、std::uint32_tやstd::uint64_tのような固定幅整数型を使うと意図が伝わりやすくなります。
unsignedに負の値を入れた場合
負の値はそのまま入らない
unsigned型は負の値を表現できません。
そのため、負の値をunsigned型に代入すると、その値がそのまま保存されるわけではありません。
たとえば、-1をunsigned intに変換すると、32bit環境では4,294,967,295になることがあります。
これは、unsigned型がその範囲内に収まるように値を変換するためです。
コンパイルエラーになるとは限らない
ここで重要なのは、負の値をunsignedに代入しても、必ずコンパイルエラーになるわけではないという点です。
C++では、このような変換が許可される場面があります。
そのため、コンパイラによって警告が出ることはありますが、プログラム自体はコンパイルできてしまうことがあります。
これがunsignedの怖いところです。
「負の値を入れたらエラーで止まる」と期待していると、実際には巨大な正の値として扱われ、バグに気づきにくくなります。
unsignedの回り込み
unsignedは範囲外になると回り込む
unsigned型の大きな特徴は、範囲外の値になったときに回り込むことです。
たとえば、0から1を引くと、数学的には-1になります。
しかし、unsigned型は負の値を表せません。
そのため、0から1を引くと、最大値側へ回り込みます。
32bitのunsigned intであれば、0から1を引いた結果は4,294,967,295になります。
unsignedの回り込みは定義された動作
この回り込みは、C++において定義された動作です。
つまり、unsigned型では、範囲を超えた演算が起きても、その動作は一定のルールに従います。
一方、signed intで最大値を超えるようなオーバーフローを起こすと、未定義動作になる可能性があります。
この点は、signedとunsignedの大きな違いです。
回り込みは便利な場合もある
回り込みは危険なだけではありません。
ハッシュ計算、暗号処理、チェックサム、ビット演算などでは、unsignedの回り込みを意図的に利用することがあります。
ただし、通常の数値計算では、この回り込みがバグの原因になることが多いです。
特に、個数・在庫数・点数・年齢などを扱うときに、誤って0未満になる計算をすると、巨大な値に変わってしまう可能性があります。
signedとunsignedを混ぜる危険性
比較結果が直感と違うことがある
unsignedで特に注意すべきなのが、signed型との比較です。
たとえば、intの値が-1で、unsigned intの値が1だったとします。
人間の感覚では、当然-1の方が小さいと考えます。
しかし、C++では比較の前に型変換が行われます。
このとき、int側の値がunsigned intに変換されることがあります。
その結果、-1が巨大な正の値として扱われ、比較結果が直感と逆になることがあります。
暗黙の型変換がバグを生む
C++では、異なる整数型を一緒に使うと、暗黙の型変換が発生します。
この変換は、プログラマーが明示的に書かなくても自動で行われます。
そのため、コードを見たときには普通の比較に見えても、実際には一方の値が別の型に変換されてから比較されていることがあります。
特に、signedとunsignedを混ぜると、負の値が巨大な正の値に変わってしまう場合があるため注意が必要です。
警告を無視しないことが大切
多くのコンパイラは、signedとunsignedの比較に対して警告を出すことがあります。
この警告は非常に重要です。
警告が出てもコンパイルできるため、軽視してしまう人もいますが、signedとunsignedの比較警告は実際のバグにつながりやすいものです。
実務では、こうした警告をできるだけ解消する方が安全です。
std::size_tとunsigned
vectorのsizeはstd::size_tを返す
C++の標準ライブラリでは、サイズや要素数を表す型としてstd::size_tがよく使われます。
たとえば、std::vectorの要素数を取得するときの戻り値は、通常std::size_tです。
std::size_tは、多くの環境で符号なし整数型です。
そのため、int型の変数と比較すると、signedとunsignedの比較になることがあります。
indexが負になる可能性がある場合は注意
たとえば、添字を表す変数に-1が入る可能性がある場合、std::size_tとの比較は危険です。
人間の感覚では、-1はどんなサイズよりも小さいと考えます。
しかし、-1がstd::size_tに変換されると、巨大な値になる可能性があります。
その結果、条件判定が期待通りに動かないことがあります。
先に負でないことを確認する
signedなインデックスをstd::size_tと比較する場合は、まず値が0以上であることを確認する必要があります。
そのうえで、std::size_tに変換してサイズと比較すると安全です。
ただし、より根本的には、「見つからなかった場合は-1を返す」という設計を避けることも重要です。
C++では、見つからなかったことを表すためにstd::optionalなどを使う設計の方が安全な場合があります。
unsignedを使ったループの注意点
逆順ループでバグが起きやすい
unsignedを使ったループで特に危険なのが、逆順ループです。
たとえば、値を10から0まで減らしていくループを書いたとします。
このとき、ループ変数がunsigned型だと、0より小さくなることができません。
そのため、0の次にさらに減らすと、最大値へ回り込んでしまいます。
結果として、ループが終わらず、非常に大きな値からさらに減り続けるような動作になることがあります。
unsignedでは0以上の判定が常に真になる
unsigned型の変数に対して「0以上か」を判定しても、その条件は常に真です。
なぜなら、unsigned型はそもそも負の値を持てないからです。
この性質を理解していないと、逆順ループで終了条件を書いたつもりでも、実際には終了条件になっていないことがあります。
逆順ループでは書き方を工夫する
std::size_tのような符号なし整数で逆順ループを書く場合は、終了条件に注意が必要です。
実務では、現在の値をそのまま添字として使うのではなく、1つずらして扱う書き方がよく使われます。
たとえば、サイズから始めて、ループ内では「現在値から1を引いた値」を添字として使う考え方です。
この方法なら、空のコンテナでも安全に扱いやすくなります。
また、C++20以降ではstd::ssizeを使って、コンテナのサイズをsignedな値として取得する方法もあります。
unsignedを「非負の値だから安全」と考えてはいけない
unsignedはエラーを防ぐ型ではない
unsignedは、負の値が入ったときに自動的にエラーで止めてくれる型ではありません。
むしろ、負の値に相当する計算結果が出たときに、巨大な正の値へ変換されることがあります。
そのため、「個数はマイナスにならないからunsignedにしよう」「年齢はマイナスにならないからunsignedにしよう」と単純に考えるのは危険です。
減算があるなら特に注意
個数や残数を扱う場合でも、減算が発生するなら注意が必要です。
たとえば、在庫数が0の状態でさらに1を引くような処理があると、本来ならエラーとして扱うべきです。
しかし、unsignedでは巨大な値に回り込んでしまう可能性があります。
この場合、プログラムは異常を検知するどころか、「在庫が大量にある」と誤認してしまうかもしれません。
値の妥当性チェックは別途必要
非負の値だけを扱いたい場合でも、unsignedにするだけで十分とは限りません。
入力値や計算結果が妥当かどうかは、別途チェックする必要があります。
特に、業務ロジックでは「0未満になったらエラー」「上限を超えたらエラー」といった判定を明示的に行うことが重要です。
unsignedを使うべき場面
ビット演算を扱う場合
unsignedが有効に使える代表的な場面は、ビット演算です。
フラグ管理、マスク処理、ビット列の操作などでは、符号付き整数よりも符号なし整数の方が自然です。
符号付き整数でビット演算を行うと、符号ビットの扱いが絡んで分かりにくくなる場合があります。
一方、unsignedであれば、純粋なビット列として扱いやすくなります。
バイナリデータを扱う場合
ファイルフォーマット、通信プロトコル、画像データ、音声データ、暗号処理などでは、符号なし整数型がよく使われます。
これらの場面では、値を「数学的な整数」として扱うというより、「決められた長さのビット列」として扱うことが多いです。
そのため、std::uint8_t、std::uint16_t、std::uint32_t、std::uint64_tのような固定幅の符号なし整数型が適しています。
外部仕様で符号なし整数と決まっている場合
ファイル仕様や通信仕様で「32bit符号なし整数」と定められている場合は、それに合わせて符号なし整数型を使うのが自然です。
このような場面では、unsigned intよりも、幅が明確なstd::uint32_tなどを使う方が意図が明確になります。
コンテナのサイズを扱う場合
標準ライブラリのコンテナサイズは、一般的にstd::size_tで表されます。
そのため、要素数やサイズをそのまま扱う場合は、std::size_tを使うことがあります。
ただし、サイズ同士の差分を計算したり、負の値になる可能性のある処理をしたりする場合は注意が必要です。
unsignedを避けた方がよい場面
通常の数値計算
通常の数値計算では、まずsigned型を検討する方が安全です。
特に、加算だけでなく減算がある場合、結果が負になる可能性があります。
このような計算をunsignedで行うと、負の結果が巨大な正の値になってしまうことがあります。
差分を扱う場合
差分は負になる可能性があります。
現在値と過去値の差、2つの位置の差、2つのインデックスの差などは、どちらが大きいかによって結果が正にも負にもなります。
このような場面では、unsignedよりもsignedな型を使う方が自然です。
ポインタやイテレータの距離を扱う場合は、std::ptrdiff_tのようなsignedな型が使われることがあります。
無効値として-1を使う場合
「見つからなかった場合は-1を返す」という設計とunsignedは相性が悪いです。
unsignedは-1を表現できないため、-1を代入すると巨大な正の値になる可能性があります。
このような場合は、intなどのsigned型を使うか、より現代的にはstd::optionalを使って「値が存在しない」ことを表す方が安全です。
signedとunsignedが混在する場合
すでにコード内でsigned型の値が多く使われている場合、そこにunsignedを混ぜると比較や計算で問題が起きやすくなります。
特に、コンパイラ警告が多発するような場合は、型設計を見直した方がよいです。
autoとunsignedの注意点
autoで意図せずunsignedになることがある
C++ではautoを使うと、右辺の型から自動的に型が推論されます。
たとえば、コンテナのサイズをautoで受け取ると、その変数の型はstd::size_tになります。
std::size_tは多くの場合、符号なし整数型です。
そのため、本人が意識していなくても、変数がunsigned系の型になっていることがあります。
sizeから値を引くときは注意
autoで受け取ったサイズから別の数を引くと、結果が負になるはずの場面でも、巨大な正の値になることがあります。
たとえば、要素数が3のときに5を引けば、数学的には-2です。
しかし、型がstd::size_tであれば、負の値にはならず、回り込みが発生します。
このような処理では、std::ssizeを使ってsignedなサイズとして扱うことを検討できます。
unsignedリテラルの注意点
uやUを付けるとunsignedになる
整数リテラルにuやUを付けると、そのリテラルはunsigned型になります。
これはビット演算などでは便利です。
一方で、通常の比較で使うと、意図せずsignedとunsignedの混在が発生することがあります。
負の値との比較に注意
たとえば、負の値を持つ可能性があるsigned変数と、unsignedリテラルを比較すると、暗黙の型変換によって比較結果が直感と異なることがあります。
そのため、整数リテラルに不用意にuを付けるのは避けた方がよい場合があります。
ビット演算や固定幅整数の処理など、意図が明確な場面で使うのが安全です。
charとunsigned charの違い
charはsignedかunsignedかが処理系による
C++では、char、signed char、unsigned charは区別されます。
ここで注意すべきなのは、単なるcharがsignedとして扱われるか、unsignedとして扱われるかは処理系によって異なるという点です。
そのため、charに大きな数値を入れた場合、環境によって結果が変わる可能性があります。
バイト値にはunsigned charやstd::uint8_tを使う
文字ではなく、純粋なバイト値を扱いたい場合は、charではなくunsigned charやstd::uint8_tを使う方が意図が明確です。
ただし、std::uint8_tは多くの環境でunsigned charの別名として定義されています。
そのため、出力時に数値ではなく文字として扱われる場合があります。
数値として表示したい場合は、整数型に変換してから出力する必要があります。
C++17以降ならstd::byteも選択肢になる
バイナリデータを「数値」ではなく「バイト列」として扱いたい場合、C++17以降ではstd::byteも選択肢になります。
std::byteは整数計算を目的とした型ではなく、バイトそのものを表す型です。
そのため、ファイルの生データや通信データなどを扱う場面では、意図がより明確になることがあります。
unsigned intとstd::size_tの違い
std::size_tはサイズを表すための型
unsigned intとstd::size_tは同じものではありません。
std::size_tは、オブジェクトのサイズやコンテナの要素数を表すための型です。
環境によってはunsigned intの場合もありますが、unsigned longやunsigned long longの場合もあります。
特に64bit環境では、std::size_tは64bitの符号なし整数型であることが多いです。
サイズにはstd::size_tが自然
配列やコンテナのサイズを扱う場合、unsigned intよりもstd::size_tを使う方が自然です。
標準ライブラリの多くの関数がstd::size_tを使っているため、型を合わせやすくなります。
ただし、std::size_tも符号なし整数型であることが多いため、減算や比較には注意が必要です。
差分にはstd::ptrdiff_tが使われる
サイズではなく、2つの位置の差やポインタの差を扱う場合は、std::ptrdiff_tが使われます。
std::ptrdiff_tはsignedな整数型です。
差分は負になる可能性があるため、符号付きであることが重要です。
実務でのunsignedの使い分け
普通の整数にはsigned型を使う
通常の数値計算では、まずintやlong longなどのsigned型を使うことを検討するとよいです。
点数、金額、差分、カウント、残数など、計算の途中で負になる可能性が少しでもあるなら、signed型の方が自然です。
サイズにはstd::size_tを使う
コンテナのサイズや配列の要素数を表す場合は、std::size_tがよく使われます。
ただし、std::size_tを使う場合でも、intなどのsigned型と混ぜて比較しないよう注意が必要です。
差分にはsigned型を使う
2つの値の差を扱う場合は、負になる可能性があります。
そのため、std::ptrdiff_tやlong longなどのsigned型を使う方が安全です。
ビット列には固定幅のunsigned型を使う
ビット演算やバイナリ形式を扱う場合は、std::uint32_tやstd::uint64_tのような固定幅の符号なし整数型が適しています。
このような型を使うと、何ビットのデータを扱っているのかが明確になります。
unsignedを安全に使うための考え方
signedとunsignedをなるべく混ぜない
最も大切なのは、signedとunsignedを不用意に混ぜないことです。
混在すると、暗黙の型変換によって予期しない比較結果や計算結果になることがあります。
特に、コンパイラが警告を出している場合は、その警告を無視せず、型の設計を見直すべきです。
減算がある場合は結果が負にならないか確認する
unsigned型で減算を行う場合は、結果が0未満にならないことを事前に確認する必要があります。
0未満になる可能性があるなら、unsignedで計算するのではなく、signed型に変換してから計算するか、そもそも型設計を見直す方が安全です。
無効値を表すならoptionalなどを使う
「値がない」「見つからなかった」といった状態を表すために、-1を使う設計は昔からあります。
しかし、unsignedと組み合わせると危険です。
現代のC++では、値が存在しないことを表すためにstd::optionalを使う方が安全で分かりやすい場合があります。
unsignedは用途を絞って使う
unsignedは便利な型ですが、何にでも使うべき型ではありません。
特に、次のような用途では有効です。
- ビット演算
- バイナリデータ
- 外部仕様で符号なし整数と決まっている値
- コンテナのサイズ
- ハッシュ計算
- チェックサム
- オーバーフローの回り込みを意図的に利用する処理
一方、通常の業務ロジックや数値計算では、signed型の方が安全なことも多いです。
前回内容の正確性について
大筋では正しい
前回の説明は、全体としては大筋で正しい内容です。
特に、unsignedの回り込み、signedとの比較の危険性、逆順ループの注意点、std::size_tとの関係などは、C++を学ぶうえで非常に重要なポイントです。
一部は表現をより厳密にした方がよい
一方で、いくつかの表現はより厳密にした方がよいです。
たとえば、負の値をunsignedに代入した場合について、「コンパイルが通ることがある」という表現は少し曖昧です。
実際には、多くの場合コンパイルは通ります。
ただし、警告が出ることがあります。
また、signedとunsignedの比較についても、「変換される場合がある」というより、intとunsigned intの比較では、通常int側がunsigned intへ変換されると説明した方が正確です。
初心者向けには逆順ループの説明を工夫するとよい
逆順ループについては、unsignedの回り込みを避ける書き方が複数あります。
ただし、初心者向けには、ややトリッキーな書き方よりも、サイズから始めて、添字としては1を引いた値を使う考え方の方が理解しやすいです。
unsignedの逆順ループは、慣れている人でもミスしやすい部分なので、できるだけ読みやすい書き方を選ぶことが大切です。
まとめ
unsignedは負の値を持たない整数型
unsignedは、0以上の値だけを扱う符号なし整数型です。
負の範囲を持たない代わりに、正の方向へより大きな値を表せます。
unsignedは回り込みに注意が必要
unsigned型では、範囲を超えた演算が起きると回り込みます。
0から1を引くと、負の値にはならず、最大値へ回り込みます。
この動作はC++では定義されていますが、通常の数値計算ではバグの原因になりやすいです。
signedとunsignedの混在は危険
signed型とunsigned型を混ぜると、暗黙の型変換によって比較結果が直感と異なることがあります。
特に、負の値を持つsigned型がunsigned型に変換されると、巨大な正の値として扱われることがあります。
unsignedは用途を選んで使うべき
unsignedは、ビット演算、バイナリデータ、固定幅の外部仕様、コンテナサイズなどでは有効です。
しかし、通常の計算、差分、負の値になる可能性がある処理、無効値として-1を使う設計などには向いていません。
実務では型の意図を明確にすることが重要
C++でunsignedを使うときは、「値が非負だから」という理由だけで選ぶのではなく、その値が本当に符号なし整数として扱うべきものかを考える必要があります。
実務では、次のように考えると整理しやすいです。
- 通常の数値計算にはsigned型
- サイズや要素数には
std::size_t - 差分にはsigned型や
std::ptrdiff_t - ビット列やバイナリ処理には固定幅のunsigned型
- 無効値の表現には
std::optionalなど
結論として、unsignedは「負にならない安全な整数」ではありません。
回り込みと暗黙変換に注意しながら、用途を絞って使うべき型です。
以上、C++のunsigned変数についてでした。
最後までお読みいただき、ありがとうございました。
