符号無し整数 解答ページ | Programming Place Plus 新C++編

トップページ新C++編符号無し整数

このページの概要

このページは、練習問題の解答例や解説のページです。



解答・解説

問題1 (確認★)

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

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


変数宣言時の型の指定が auto の場合、初期化子から型が決定されます。

1番は、0 という整数リテラルは int型なので(本編解説)、int型になります。

2番は、0u は uサフィックスが付加されており、unsigned int型と判断されるため(本編解説)、unsigned int型になります。

3番は、10 + 10u となっています。10 は int型、10u は unsigned int型です。演算子(ここでは +)の2つのオペランドの型が異なる場合、どちらかの型に合わせるような変換がはたらきます(本編解説)。このケースでは unsigned int型の方に合わせられることになり、10 + 10u を計算した結果の型も unsigned int型ということになります。よって、変数a の型も unsigned int型になります。

4番は、static_cast<int>(100u) となっています。static_cast は () の中の式の型を <> の中の型に強制変換します(本編解説)。したがって、この式の型は int型であり、変数a の型は int型になります。

問題2 (基本★)

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


次のように書けます。

using uint = unsigned int;
typedef unsigned int uint;

using strvec = std::vector<std::string>;
typedef std::vector<std::string> strvec;

それぞれの構文はこうです(本編解説)。

using 別名 = 元の名前;
typedef 元の名前 別名;

別名と元の名前の順序が反対になっていることに注意してください。

問題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";
        }
    }
}


変数i の型が std::vector<int>::size_type になっています。これは符号無し整数型であるため、負の値にはなりません。値が 0 のときに、--i を実行した結果は、std::vector<int>::size_type型で表現できる最大の正の数です(本編解説)。

そのため、i >= 0 という条件式は必ず true であり、この for文から抜け出すことができません。実際には、変数i の値が巨大な正の数になったとき、v.at(i) が範囲外アクセスを起こすため、プログラムは強制終了させられることになります。

なお、このようなプログラムでは、要素数が 0 である可能性があることを忘れてはなりません。要素数が 0 のとき、v.size() は 0 になるため、0 - 1 という式が実行されることになり、やはり巨大な正の数を生み出すことになります。ここでは、empty関数を使って回避しています。


ここでの問題は、変数宣言を int i = v.size() - 1; のように変えるか、条件式の方を static_cast<int>(i) >= 0 のように変えるかすれば回避できるかもしれませんが、別の問題を引き起こす可能性があります。いずれの場合でも、符号無し整数型の値を int型に変換しているため、int型の上限値を超える値を変換しようとする可能性があるためです。そのような場合の結果は処理系定義ですが(本編解説)、おそらく負の数に化けることになります。いずれにしても正しい値を表現できない以上、正しい動作にはなりません。

この方法を使うのなら、int型よりも表現できる数が大きい型(たとえば long long int型)を選んだ方が良いでしょう。

添字を登場させてしまうと難しくなるので、イテレータ(ここでは逆イテレータ)を使って書くのが安全です(「イテレータ」のページを参照)。

#include <iostream>
#include <iterator>
#include <vector>

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

    for (auto it {std::crbegin(v)}; it != std::crend(v); ++it) {
        std::cout << *it << "\n";
    }
}

実行結果:

2
1
0

問題4 (調査★★★)

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


オペランドが2つある演算子においては、演算が行われる前にオペランドの型を揃えられます。そのときのルールは本編で解説したとおりです(本編解説)。

i < v.size() の場合ですが、i は問題文から int型であるとわかっています。問題は v.size() の方で、この型は std::vector<int>::size_type型ですが、これは別名であって、具体的な型は実装に任されています。確実なのは、符号無し整数型であるということだけです(本編解説)。そのため、int型のランク(変換順位)よりも高いのか低いのかは分からないということになります(実際にはまず間違いなく、int型以上のランクではありますが)。

int型以上のランクであると仮定すると、変換のルール上、符号付き整数型である i の方が、相手側の符号無し整数型に変換されることになります。したがって、static_cast<std::vector<int>::size_type>(i) < v.size() のように比較しているのと同じです。

一方、int型未満のランクであったとすると、v.size() で得られる値が int型に変換されて、i < static_cast<int>(v.size()) と同じになるか、両者ともに unsigned int型に変換されて、static_cast<unsigned int>(i) < static_cast<unsigned int>(v.size()) と同じになるかのいずれかということになります。

3つのケースのいずれになったとしても、int型から符号無し整数型、あるいは符号無し整数型から int型への暗黙の型変換が起こることに注目しなければなりません。このときに元の値が表現できなくなり、比較の結果が想定外のものになる恐れがあります。たとえば、変数i の値が負数の場合、符号無し整数型に変換されると巨大な正の数になってしまうため、i < v.size() が true になってしまうかもしれません。一方、v.size() が int型で表現できないほど大きな値だった場合、int型に変換されると負の数になってしまうため、i < v.size() は false になってしまうかもしれません。


このように、符号付き整数型と符号無し整数型の混在はやっかいでしかありません。そのうえ、std::vector<int>::size_type型 のように、具体的な型がわからないものが混じることもあって、正確に把握し、つねに正しく動作するコードを書くことは困難です。原則として、型を混在させないことが重要です。たとえば、変数i の型を std::vector<int>::size_type型にしていれば、i < v.size() は想定どおりに動作します。また、そもそもこうした比較を避けられないかということも考えるべきです(範囲for文やイテレータなどを使えないだろうか?)。


参考リンク



更新履歴




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