数値型で扱える値の最小値や最大値を取得する - C++ プログラミング

PROGRAM


数値型で扱える値の最小値や最大値を取得する

C++ では、int 型や char 型などの数値型のサイズは処理系に依存するため、コンパイラによってそれらが扱う値の範囲が違ってくる可能性があります。特に char は、8 ビットとは限らなかったり、signed 扱いか unsigned 扱いかも違ってきます。

そういった違いを吸収できるように、C++ では扱える値の範囲や桁数を取得する機能が用意されています。

 

std::numeric_limits<> を使用する方法

値の範囲や桁数を知る方法の一つとして std::numeric_limits<> というテンプレートクラスを使用する方法があります。

これを使うためには、まず <limits> ヘッダーをインクルードします。

#include <limits>

こうすることで std::numeric_limits<> が使えるようになります。

 

これを使って、たとえば char 型が扱える情報の範囲は次のようにして取得することができます。

char 型ではなく int 型の情報を取得したい場合は <char> のところを <int> に、unsigned long の情報を取得したい場合は <unsigned long> にすれば同じように取得できます。

std::numeric_limits<char>::min() char 型の最小値を取得します。値は char 型で取得されます。
std::numeric_limits<char>::max() char 型の最大値を取得します。値は char 型で取得されます。
std::numeric_limits<char>::is_signed char 型が符号を扱えるとき (signed char) に true を、符号を扱えないとき (unsigned char) に false を返します。
std::numeric_limits<char>::digits char 型で数値として扱えるビット数を int 型で取得します。符号用のビットは含みません。たとえば 8 ビットなら、signed char は 7 に、unsigned char は 8 になります。(厳密にはビット数ではなく std::numeric_limits<char>::radix を基数として「確実に」扱える桁数を取得しますが、標準の数値型は基数が 2 なのでビット数と同じです)
std::numeric_limits<char>::digits10 char 型で「確実に」扱える 10 進数での桁数を取得します。符号は含みません。この型で扱える最大値の桁数ではありません。

 

注意したいところとしては、まず、std::numeric_limits<>::digits10 が返す値についてです。

これは「この変数に格納されている値が 10 進数で最大何桁の値を表現するか」ではなく、「この変数に代入しても値が壊れない最大の桁数」を意味しています。

これらの違いや仕組みについては 数値型の変数で扱える値の桁数を求める で詳しく記してありますが、たとえば、符号なし 2 バイト整数であれば、表現できる数字の範囲は 0 から 65,535 までなので、最大桁数は 5 桁ですけど、たとえばこの変数に 90,000 を代入すると、桁数は 5 桁であるにも関わらず、表現し切れずに値が壊れてしまいます。

よって、符号なし 2 バイト整数であれば 4 桁が、その変数で問題なく扱うことのできる 10 進数の最大桁数ということになります。

ちなみに std::numeric_limits<>::digits についても考え方は同じなのですが、2 進数の場合はそういう中途半端な値が出てこないので、格納されている値の最大桁数と、変数が表現できる最大の桁数とが一致します。

 

また、最小値を取得する min 関数や最大値を取得する max 関数は、戻り値として目的の型と同じ型で値を返すところにも注意です。

たとえば std::numeric_limits<char>::min() なら char 型で値が帰ってくるので、目的の型の値を正確に取得できる反面、別の型に変換するときには char と int の変換キャストの留意点 でお話したような値の範囲による影響を受けがちです。

同じ型で符号の有り無し(たとえば signed char と unsigned char など)を相互変換したり、符号なし変数(たとえば size_t など)に代入したりすると、値がおかしくなることがあることには注意です。

もっとも、こういった注意は常に必要なところですけど。

 

この std::numeric_limits<> を使うメリットは、テンプレート引数によって調べたい型を変更できるところです。

クラステンプレート関数テンプレート ととても相性が良いので、さまざまな型の差異を中和しながら汎用的なテンプレートを実装をすることも可能になります。

 

定義済みマクロを使う方法

値の範囲を知る方法として、もうひとつ、定義済みのマクロを使う方法があります。

値の範囲が定義されたマクロは <climits> ヘッダーに定義されているので、まずはそれをインクルードする必要があります。

#include <climits>

これで、次のマクロを使って、それぞれの型の最小値や最大値を取得できます。

対象の型 最小値 最大値 補足
char CHAR_MIN CHAR_MAX char 型が何ビットで構成されているかは CHAR_BIT で取得できます。この CHAR_BIT は std::numeric_limits<char>::digit とは違って、符号ビットも含んだ「バイト当たりのビット数」です。
signed char SCHAR_MIN SCHAR_MAX  
unsigned char   UCHAR_MAX UCHAR_MIN は定義されていませんが、最小値は 0 です。
short SHRT_MIN SHRT_MAX  
unsigned short   USHRT_MAX USHRT_MIN は定義されていませんが、最小値は 0 です。
int INT_MIN INT_MAX  
unsigned int   UINT_MAX UINT_MIN は定義されていませんが、最小値は 0 です。
long LONG_MIN LONG_MAX  
unsigned long   ULONG_MAX ULONG_MIN は定義されていませんが、最小値は 0 です。
long long LLONG_MIN LLONG_MAX  
unsigned long long   ULLONG_MAX ULLONG_MIN は定義されていませんが、最小値は 0 です。

 

また、次のように <cstdint> をインクルードすることで、もう少し細かな判定もできるようになります。

#include <cstdint>

対象の型 最小値 最大値 補足
std::int8_t INT8_MIN INT8_MAX  
std::int16_t INT16_MIN INT16_MAX  
std::int32_t INT32_MIN INT32_MAX  
std::int64_t INT64_MIN INT64_MAX  
std::uint8_t   UINT8_MAX UINT8_MIN は定義されていませんが、最小値は 0 です。
std::uint16_t   UINT16_MAX UINT16_MIN は定義されていませんが、最小値は 0 です。
std::uint32_t   UINT32_MAX UINT32_MIN は定義されていませんが、最小値は 0 です。
std::uint64_t   UINT64_MAX UINT64_MIN は定義されていませんが、最小値は 0 です。
std::int_least8_t INT_LEAST8_MIN INT_LEAST8_MAX  
std::int_least16_t INT_LEAST16_MIN INT_LEAST16_MAX  
std::int_least32_t INT_LEAST32_MIN INT_LEAST32_MAX  
std::int_least64_t INT_LEAST64_MIN INT_LEAST64_MAX  
std::uint_least8_t   UINT_LEAST8_MAX UINT_LEAST8_MIN は定義されていませんが、最小値は 0 です。
std::uint_least16_t   UINT_LEAST16_MAX UINT_LEAST16_MIN は定義されていませんが、最小値は 0 です。
std::uint_least32_t   UINT_LEAST32_MAX UINT_LEAST32_MIN は定義されていませんが、最小値は 0 です。
std::uint_least64_t   UINT_LEAST64_MAX UINT_LEAST64_MIN は定義されていませんが、最小値は 0 です。
std::int_fast8_t INT_FAST8_MIN INT_FAST8_MAX  
std::int_fast16_t INT_FAST16_MIN INT_FAST16_MAX  
std::int_fast32_t INT_FAST32_MIN INT_FAST32_MAX  
std::int_fast64_t INT_FAST64_MIN INT_FAST64_MAX  
std::uint_fast8_t   UINT_FAST8_MAX UINT_FAST8_MIN は定義されていませんが、最小値は 0 です。
std::uint_fast16_t   UINT_FAST16_MAX UINT_FAST16_MIN は定義されていませんが、最小値は 0 です。
std::uint_fast32_t   UINT_FAST32_MAX UINT_FAST32_MIN は定義されていませんが、最小値は 0 です。
std::uint_fast64_t   UINT_FAST64_MAX UINT_FAST64_MIN は定義されていませんが、最小値は 0 です。
std::intptr_t INTPTR_MIN INTPTR_MAX  
std::uintptr_t   UINTPTR_MAX UINTPTR_MIN は定義されていませんが、最小値は 0 です。
std::intmax_t INTMAX_MIN INTMAX_MAX  
std::uintmax_t   UINTMAX_MAX UINTMAX_MIN は定義されていませんが、最小値は 0 です。
std::ptrdiff_t PTRDIFF_MIN PTRDIFF_MAX  
std::sig_atmic_t SIG_ATOMIC_MIN SIG_ATOMIC_MAX  
wchar_t WCHAR_MIN WCHAR_MAX  
std::wint_t WINT_MIN WINT_MAX  

また、余談ですけど std::int8_t 型などの数値リテラルを記述するためのマクロも用意されるようでした。

対象の型 リテラル表記
(たとえば 100)
std::int8_t INT8_C(100)
std::int16_t INT16_C(100)
std::int32_t INT32_C(100)
std::int64_t INT64_C(100)
std::uint8_t UINT8_C(100)
std::uint16_t UINT16_C(100)
std::uint32_t UINT32_C(100)
std::uint64_t UINT64_C(100)
std::intmax_t INTMAX_C(100)
std::uintmax_t UINTMAX_C(100)

これらのマクロでやっていることは、指定された値に適切なサフィックスを追加しています。型のサイズは処理系によって依存するので、これらのマクロで中和している感じですね。

 

さて、これらのマクロで注意したいところは、これらのマクロがプリプロセッサによって数値リテラルに置き換えられるところでしょうか。

適切なサフィックスは付けられているものの、たとえば CHAR_MIN なら -128 に置き換えられます。厳密な型情報が抜け落ちるので、たとえばテンプレート関数やオーバーロードされた関数の引数に直接これらを指定する必要がある場合などには、思っているのと違う動作をするかもしれません。

型を厳密に扱うためには "(char)CHAR_MAX" などと、明示的にキャストする必要があります。

 

マクロを使う利点としては、プリプロセッサで処理できる点が挙げられます。

std::numeric_limits<> は実行時の処理になりますけど、マクロならコンパイル前のプリプロセスの段階で処理されるので、処理系に依る差異を吸収したいときには、マクロを使った方法の方が最適な場合が考えられます。

 

たとえば char 型が符号を持つかどうかを知りたい場合は、次のように判定できます。

#if CHAR_MIN != 0

// 符号付き char の場合の処理をここに記載します。

 

#else

// 符号なし char の場合の処理をここに記載します。

 

#endif

このとき、条件に合った方のコードしかコンパイルされないため、実行時に判定処理をしなくて済み、バイナリレベルではコードがスマートになります。ソースコードのレベルでは見難くなりますけれど。

処理系依存を中和したい場合、実行時に処理系のルールが変わることは普通はないでしょうから、マクロを使う方が最適でしょう。


[ もどる ]