符号無し整数 | Programming Place Plus 新C++編

トップページ新C++編

このページの概要 🔗

このページでは、符号無し整数という、これまで int型で扱ってきたものとは異なる種類の整数を取り上げます。ここまでのページではあえて考えないようにしてきましたが、std::vector や std::string を使っていると、符号無し整数が登場することはほぼ避けられません。int型と混在する場合のルールを理解しておく必要があります。また、話題に関連して、型に別名を付ける機能についても取り上げています。

このページの解説は C++14 をベースとしています

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



符号付き整数と符号無し整数 🔗

少し脱線気味の話題になりますが、このページでは、符号無し整数というものを取り上げます。

符号無し整数の話をする理由は、std::vector を使うようになってから、ごまかし続けていることがあって、それに関係するからです。ごまかしというのは、次のコードに含まれています。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v {0, 1, 2};

    for (int i = 0; i < v.size(); ++i) {
        std::cout << v.at(i) << "\n";
    }
}

実行結果:

0
1
2

このプログラムをコンパイルすると、i < v.size() の部分に対して「signed と unsigned の数値を比較している」という内容の警告を出すコンパイラが多いはずです。変数i が int型なのに対して、v.size() で得られる整数の型は int型ではなく、こういった比較は正しい結果が得られない可能性があるからです。コンパイルエラーにはなりませんが、本来、警告の意味を理解して、適切に対処しなければなりません。警告を消すことが目的になってしまわないように注意してください。そうではなく、警告される理由を理解し、その原因を修正するのです。

このプログラムでいえば、変数i は 0~3 までの範囲にしかなりませんし、v.size() は 3 にしかなりません。この関係性の中で i < v.size() が問題になることはありません。それでも警告されるのは、コンパイラは実行中の値ではなく、型を判断材料にしているからです。コンパイラは、プログラムを実行するよりも前に仕事をしているのですから、実行中の値を判断材料にはできないのです。そのため、実際にはまったく問題がないのだとしても、異なる型による比較を疑わしいものとして警告しています。つまり、警告が出ているから、プログラムにバグがあるということではありません


int型は、整数型の中でも、符号付き整数型 (signed integer type) と呼ばれる型に分類されます。「符号付き」といっているように、符号がある整数型ということで、正の数でも負の数でも(そして 0 も)表現できます。

一方、std::vector や std::string の size関数(および length関数)で得られる整数の型は、符号無し整数型 (unsigned integer type) に分類されます。符号というものを考えないことにした整数型という意味で、正の数(と 0)だけを表現できます。

【上級】要素数は負の数になりえないものであるため、符号無し整数型が用いられています。ただし、この考え方で符号無し整数型を使うについては、支持する意見がある一方[1]、否定的な見解もあります[2]

要素にアクセスするときに使う [] や at関数にも、符号無し整数型の値を指定することになっています。int型の値を渡しても警告が出ないかもしれませんが、符号付き整数型から符号無し整数型へ暗黙の型変換が起きています(あとで取り上げます)。

char型は分類上、整数型の一種ですが、符号の有無については処理系定義です。整数型とはいえ、基本的に文字をあつかうための型であると捉えて、整数を表現するために使わないほうがいいでしょう。ほかのページで取り上げますが、符号の有無を明確に指定した signed char型と unsigned char型があります。

bool型も分類上、整数型の一種ですが、true か false にしかならないので、符号の有無は関係ありません。

unsigned int型 🔗

もっとも基本的な符号無し整数型は unsigned int型です。「unsigned」(アンサインド)は、「符号無し」を意味するキーワードで、「unsigned int」は「符号無し版の int型」を意味しています。ちなみに、「符号付き」は「signed」(サインド)と表現されます。

普通の int型を signed intsigned と記述してもいいことになっています(わざわざこのような書き方をする必要はありませんが)。

unsigned int ui {100};

このコードでは、unsigned int型の変数ui の初期値として、100 を与えています。このように、ごく普通に整数の値を与えられますが、100 という整数リテラルは int型なので、int型の値を使って、unsigned int型の変数を初期化しようとしていることになります。unsigned int型で表現できる値なのであれば、これでも問題ありませんが、これは暗黙の型変換によって許されているものです。この話題はあとであらためて取り上げます

int型で表現できない整数リテラルは、int ではないほかの符号付き整数型になります[3]

ただし、次のコードはエラーになります。

int si {100};
unsigned int ui {si};  // エラー

これは、さきほどと同じく 100 を与えているようにみえますが、実際には「int型の変数に入れられた何か」を与えていることになります。その「何か」が、unsigned int型で表現できない可能性があるため、リスト初期化の構文ではエラーになります。

uサフィックス 🔗

100u のように、整数リテラルの末尾に u(あるいは U)を付加することによって、それが符号無し整数型であることを明確にできます。ですから、さきほどのように、100 で初期化するよりも、100u で初期化するほうが本来は適切であるといえます。

unsigned int ui {100u};
ui = 0u;
ui = 0U;  // 大文字の U でも同じ

u のように、末尾に付け足すことで何かしらの意味を持つ表記のことを、サフィックス(接尾辞) (suffix) といいます。

uサフィックスが付加された整数リテラルは、その値が unsigned int型で表現できる範囲内であれば、unsigned int型です。

unsigned int型で表現できない場合は、ほかの符号無し整数型になります。[3]

変数の型を auto にして型推論に任せるときにも、uサフィックスが便利に使えます。

auto ui = 100u;    // unsigned int型
auto si = 100;     // int型

表現できる値 🔗

unsigned int型は符号無し版の int型という存在なので、その大きさ(ビット数)は int型と同じです。しかし、int型の大きさは処理系によって異なるのでした(「int型の限界」を参照)。そのため、unsigned int型で表現できる数の範囲も処理系によって異なります。ただし、符号無し整数の意味からいって、下限値は必ず 0 です。

表現できる数の範囲は、std::numeric_limits を使って確認できます。

#include <iostream>
#include <limits>

int main()
{
    std::cout << "unsigned int\n"
              << "min: " << std::numeric_limits<unsigned int>::min() << "\n"
              << "max: " << std::numeric_limits<unsigned int>::max() << "\n";
    std::cout << "int\n"
              << "min: " << std::numeric_limits<int>::min() << "\n"
              << "max: " << std::numeric_limits<int>::max() << "\n";
}

実行結果:

unsigned int
min: 0
max: 4294967295
int
min: -2147483648
max: 2147483647

符号無し整数型は、負の数を表現しない代わりに、正の数の表現範囲を増やすことができるため、上限値は、同じ大きさの符号付き整数型の約2倍になります。ただし、上限値を増やしたいという理由で、符号無し整数型を使うのはやめましょう。むやみに使うと、符号付き整数型と符号無し整数型の混在につながってしまうからです。

いずれ紹介しますが、表現できる値の範囲を広げたいのであれば、int型よりも表現範囲が大きい符号付き整数型を使った方がいいです。たとえば、long long int という型を使うと、どんな処理系でも、少なくとも 9,223,372,036,854,775,807 まで表現できます。

符号無し整数型においては、上限値を超える値になったときには、「その型の上限値 + 1」で割った余りになるという保証があります[4]。たとえば std::numeric_limits<unsigned int>::max() + 1 の結果は 0 です。また、下限値を下回る(つまり 0 より小さくなる)場合、上限値の側から戻ってくるような動作になります。つまり、0u - 1 の結果は、上限値と同じになります。いずれにしても、限界までいったら反対側から戻ってくるという動作といえますが、こういう動作をラップアラウンド (wrap around) といいます(たまに、オーバーフローと誤解されますが、これはオーバーフローではありません)。

int型の限界」のページで説明したとおり、符号付き整数型の場合には、表現できる範囲外の値になるとオーバーフローします。オーバーフローは未定義の動作です[5]

#include <iostream>
#include <limits>

int main()
{
    auto ui = std::numeric_limits<unsigned int>::max();
    ui++;
    std::cout << ui << "\n";

    ui--;
    std::cout << ui << "\n";
}

実行結果:

0
4294967295

型の別名 (typedef名) 🔗

型の名前に、別名を与える機能があります。型の別名のことを、あとで登場する typedef指定子を使って定義されることから、typedef名 (typedef name) と呼びます。

このような機能があるのは、型も変数と同じで、意味が通じやすい名前を付けたほうがいいと考えられるためです。単に int(この型は何かしらの整数を表現する)よりも、money(この型は金額を表現する)のような名称のほうが分かりやすく、間違いを防ぐ効果もあります(たとえば、money型の変数に個数を代入することはおかしいと気付きやすい)。実際には money のような名前では、変数名との区別が難しくなってしまうため、money_tmoney_type などとすることが多いです。

typedef名を宣言する方法が2つあります。1つは typedef指定子 (typedef specifier) を使う方法で、古い C++(およびC言語)で使われてきたものです。

typedef 元の名前 別名;

もう1つの方法は using キーワードを使うもので、C++11 で追加された新しい構文です。こちらの構文は、エイリアス宣言 (alias declaration) と呼ばれます。

using 別名 = 元の名前;

typedef指定子を使う方法でも、エイリアス宣言を使う方法でも、宣言される型の別名はまったく同じものです。どちらを使っても問題ありませんが、エイリアス宣言のほうが分かりやすく書けるケースがあったり、エイリアス宣言でなければできないことがあったりするので、エイリアス宣言のほうを選ぶといいでしょう。

【上級】たとえば関数ポインタの別名は、エイリアス宣言のほうが分かりやすく書けます(「関数ポインタとラムダ式」のページを参照)。

【上級】エイリアス宣言では、template <typename T> using vec = std::vector<T>; のようにして、エイリアステンプレートを定義できます(「クラステンプレート」のページを参照)。このあと、vec<int> v; のようにして使用できます。[6]

typedef名は元の型の単なる別の名前であって、あらたに別の型を作り出したわけではありません。したがって、元の名前と比べて表現できる値の範囲が変わることはありませんし、互いの型の値を代入したり、比較したりすることにも影響しません。

#include <iostream>

int main()
{
    using money_t = int;  // int の別名を定義
//  typedef int money_t;  // typedef の場合

    money_t cost {123};

    // money_t型と int型の混在
    if (cost < 0) {
        cost = 0;
    }
    std::cout << cost << "\n";
}

実行結果:

123

size関数で得られる値の型 🔗

このページの話のきっかけになった、std::vector や std::string の size関数(および length関数)が返す符号無し整数ですが、これらの型はそれぞれ、std::vector<T>::size_typestd::string::size_type という名前で表現されます(T には要素の型が入る)。この2つは型の別名であって、元になっている本当の型があるのですが、それが何であるかは実装に任されており、仕様上明確ではありません[7] [8]。この型名のまま使うか、使える場面なら auto を使うのが適切です。

std::vector<int> v {0, 1, 2};
std::string s {"Hello"};

std::vector<int>::size_type vsize {v.size()};
std::string::size_type slen {s.length()};

// あるいは
auto vsize = v.size();
auto slen = s.length();

このページの冒頭にあったプログラムでは、「signed と unsigned の数値を比較している」という警告が出ました。この警告は、変数i の型を、v.size() で得られる値の型に合わせて、std::vector<int>::size_type にすれば解消します。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v {0, 1, 2};

    for (std::vector<int>::size_type i = 0; i < v.size(); ++i) {
        std::cout << v.at(i) << "\n";
    }
}

実行結果:

0
1
2

at関数に渡す値も std::vector<int>::size_type型なので、これが完全に正しい方法ですが、記述が長いことを嫌って、簡易的な方法に逃げているコードを見かけることは多いです(以下の上級者向けコラムを参照)。

【上級】for (auto i = 0u; i < v.size(); ++i) のように変数i を unsigned int型に型推論させる方法や、std::size_t型という符号無し整数型を使って for (std::size_t i = 0; i < v.size(); ++i) とする方法などがあります(多くの場合、std::vector<T>::size_type の本当の型は std::size_tであるため)。いずれにしても、std::vector<T>::size_type の本当の型が実装定義である以上、こうした方法で「signed と unsigned の数値を比較している」という警告は消せるにしても、std::vector<T>::size_type の方が、表現できる値の範囲が広く、正しい比較が行えない恐れがあるという別の問題に触れてしまいます。

【C++23】整数リテラルに、uzサフィックス(大文字でも構わない)を付加することで std::size_t型を表現できるようになったため、for (auto i = 0uz: i < v.size(); ++i) のように表記できます[16]

何度も記述しなければならないのであれば、std::vector<int>::size_type のさらなる別名を作っておくのも一つの方法ではあります。

using vsize_t = std::vector<int>::size_type;

for (vsize_t i = 0; i < v.size(); ++i) {
}

同じ型の変数をまとめて宣言する

変数i の型と v.size() の型を同じにするのなら、for文を書き換えて、v.size() を1回だけ実行するかたちにできます。

型が同じであれば、変数は複数まとめて宣言できます。その際には、, を使って区切りながら、「変数名 初期化子」を書き並べます。初期化子は省略できてしまうため、必要なはずの初期化子を記述し忘れるミスには注意してください。

std::vector」のページで説明したとおり、初期化子なしの変数宣言は、int型などのシンプルな型の場合、初期値が不明な状態になってしまいます。

この記法を使って、次のように書けます。

for (std::vector<int>::size_type i = 0, size = v.size(); i < size; ++i) {

この構文は、for文以外の場所で変数を宣言するときでも使えます。

#include <iostream>

int main()
{
    int v1 {10}, v2 {20}, v3 {};

    std::cout << v1 << ", " << v2 << ", " << v3 << "\n";
}

実行結果:

10, 20, 0

符号付き整数型と符号無し整数型が混在した式

1つの式の中で、符号付き整数型と符号無し整数型が混在した場合の動作を説明します。

整数型と浮動小数点型が混在する場合のルールは、「浮動小数点数」のページで取り上げています。符号の有無は関係ありません。

初期化や代入 🔗

まず、初期化や代入の場合です。v.at(i) など、関数に値を渡す場合のルールも同じです。

符号無し整数型に、符号付き整数型の値を与えようとする場合、暗黙の型変換が行われます。その値が表現できるのなら、値はそのままですが、表現できない値の場合には、表現できる値に変化します(前述したラップアラウンドの動作)[9]

リスト初期化の場合は、暗黙の型変換で値が変化してしまう恐れがある場合にコンパイルエラーになります。

int si {100};
unsigned int ui {-100};   // エラー。表現できない
unsigned int ui = -100;   // OK だが、-100 は表現できないため変換される
unsigned int ui {si};     // エラー。si の実際の値が何であれ、int から unsigned int への変換は危険と判断される
unsigned int ui = si;     // OK だが、si の値を表現できている保証はない

ui = 100;   // OK. 表現可能
ui = -100;  // 危険。-100 ではない値になってしまう

添字を指定する [] や at関数に渡す値は、std::vector<T>::size_type型や std::string::size_type型であり、符号無し整数型です。ここに、v.at(10) のように int型の値を渡せているのは、暗黙の型変換が起きているからです。


符号付き整数型に、符号無し整数型の値を与える場合も、やはり暗黙の型変換が行われます。こちらも表現可能な値なら変化しませんが、表現できない場合は、処理系定義の値になります[10]

unsigned int ui {100};
int si {ui};              // エラー。ui の実際の値が何であれ、unsigned int から int への変換は危険と判断される
int si = ui;              // OK だが、ui の値を表現できている保証はない

演算 🔗

次に、a + b とか a < b のような式の場合です。これは以前、「浮動小数点数」のページで取り上げたとおりで、2つのオペランドを持つ演算子では、演算をおこなう前に、オペランドの型が揃えられます。2つのオペランドが、符号付き整数型と符号無し整数型だった場合のルールはこうなっています(上から順番に適応)[11]

  1. 符号無し整数型の側のランクが、符号付き整数型の側のランク以上であれば、符号無し整数型の側に合わせる
  2. 符号付き整数型の側が、符号無し整数型の側で表現できるすべての値を表現可能ならば、符号付き整数型の側に合わせる
  3. 両者とも、符号付き整数型の側の型の unsigned版に変換する

ランク (rank) というのは、型の順位を定義したものです。まだ登場していない型ばかりになってしまいますが、次のようになっています(上にあるほどランクが高い)[12]

  1. long long int、unsigned long long int
  2. long int、unsigned long int
  3. int、unsigned int
  4. short int、unsigned short int
  5. char、signed char、unsigned char
  6. bool

まだ登場していない整数型は、「整数型」のページで説明します。

si < ui という式で、si が int型、ui が unsigned int型だとすると、両者のランクは同じであるため、前掲したルールの1番のところに該当します。したがって、si の型が unsigned int型に型変換されたうえで、si < ui の判定を行うことになります。こうしてコンパイルが通るわけですが、これには危険な面があります。もし、si の値が -1、ui の値が 0 だったとすると、si < ui とは -1 < 0 であって、true になってほしいはずです。ところが、si が unsigned int型に変換されると、-1 が表現できず、unsigned int型の上限値になります(このルールは前述しました)。したがって、巨大な正の整数 < 0 という判定になり、これは false になってしまいます。

#include <iostream>

int main()
{
    int si {-1};
    unsigned int ui {0};

    std::cout << std::boolalpha
              << (si < ui) << "\n"
              << (si > ui) << "\n";
}

実行結果:

false
true

符号付き整数型と符号無し整数型が混在したときに起きるこの手の問題は、コンパイルエラーにはならないが、結果は想定外なものになってしまうため、非常に厄介です。

原則として、符号付き整数型と符号無し整数型の混在を避けるようにしましょう[13]。また、事情がないかぎりは符号付き整数型(特に int型)を使うようにしましょう[14]

【上級】符号無し整数型を使う場面として代表的なのは、整数値をビットの並びとみなして操作したい場合です。こういう場面では、符号付き整数に含まれている符号ビットが邪魔をすることがあります[15]

ただし、「int型の 100 と、unsigned int型の 200 を加算する」というように、どちらの型でも表現可能な範囲内で行われる演算は安全であり、型の混在が問題になることはありません。

キャスト 🔗

型の混在で発生する警告を強制力によって対処する方法があって、良い方法というわけではないですが、よく使われています。たとえば先ほどの比較のコードで、暗黙の型変換のルールが適用される前に、si と ui を強制的に int型に揃えてしまえば、警告を回避できます。その方法はこうです。

    std::cout << std::boolalpha
              << (si < static_cast<int>(ui)) << "\n"
              << (si > static_cast<int>(ui)) << "\n";

static_cast は「浮動小数点数」のページで少し登場しています。型を一時的に強制変換する記述で、() の内側の式を評価したあとの値が <> で示した型に変換されます。このような、型を強制的に変換する指定を、キャスト (cast) と呼びます。キャストによる値の変化のルールは「初期化と代入」のところで説明したとおりです。

【C言語プログラマー】C++ にはキャストの構文が複数あって、目的に応じて使い分けることになっています。誤ったキャストをエラーにしたり、ソースコード上でキャストしている箇所を発見しやすくしたりする効果があります。C言語のキャスト構文も使えますが、原則として C++ のキャストを使うようにしましょう。

変数ui は unsigned int型ですが、static_cast<int>(ui) となっていることで、このときだけ int型に変換されます。si < static_cast<int>(ui) は、2つのオペランドがともに int型ということになるため、暗黙の型変換を行う必要はなくなり、素直に int型どうしの比較が行われるため、型が一致していないことによる警告がなくなります。ui の値が int型で表現できる値であったのなら、この比較は想定どおりに行えます。

しかし、ui の値が int型で表現できないものであったら(int型の上限値を超える巨大な正の数)、処理系定義の値に化けてしまうため、正しい比較になりません。

#include <limits>
#include <iostream>

int main()
{
    int si {-1};
    unsigned int ui {std::numeric_limits<unsigned int>::max()};  // int では表現できない巨大な正の整数

    std::cout << std::boolalpha
              << (si < static_cast<int>(ui)) << "\n"
              << (si > static_cast<int>(ui)) << "\n";
}

実行結果:

false
false

つまり、キャストによって警告を消し去ることはできますが、問題のないコードにできているわけではありません。

まとめ 🔗


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク 🔗


練習問題 🔗

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

以下のそれぞれの場合に、変数a の型は何になりますか?

  1. auto a = 0;
  2. auto a = 0u;
  3. auto a = 10 + 10u;
  4. auto a = static_cast<int>(100u);

解答・解説

問題2 (確認★)

using キーワードや typedef キーワードを使って、unsigned int型の別名として uint を、std::vector<std::string> の別名として strvec をそれぞれ定義してください。

解答・解説

問題3 (基本★)

要素の値を逆順に出力しようとしている次のプログラムの問題点を指摘してください。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v {0, 1, 2};

    if (v.empty() == false) {
        for (std::vector<int>::size_type i = v.size() - 1; i >= 0; --i) {
            std::cout << v.at(i) << "\n";
        }
    }
}

解答・解説

問題4 (調査★★★)

std::vector<int> の変数v と、int型の変数i があるとき、i < v.size() は、型に関して、どのように取り扱われて、どのように比較されるか説明してください。

解答・解説


解答・解説ページの先頭



更新履歴 🔗




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る