現在主流のプロセッサは、異なる型の数値同士を直接演算することはできません。必ず型を揃える必要があります。
C/C++は暗黙の型変換を許すパターンが多数ある危険な言語なんですが、中でも異なる整数型、特に符号付きと符号無しの整数型を混ぜると、暗黙的な型変換により意図しない結果をもたらすことが多々あります。
最も有名なパターンが以下です。
#include <stdio.h> int main(void) { signed int x = -1; unsigned int y = 1; printf("%d\n", x < y); return 0; }
一見すると、正数を保持しているy
よりも負数を保持しているx
のほうが小さいので、比較結果は真(1
)となるように思えます。
しかし、C/C++では汎整数拡張(汎整数昇格)のルールに基づき、実際には暗黙的にunsigned int
に揃えられてから比較されるため、-1
はUINT_MAX
(規格上は少なくとも65,535以上、現在主流の32bit/64bit向けの処理系では4,294,967,295)に暗黙変換され、比較結果は偽(0
)となります。
C/C++ではこのような直感に反する動作がしょっちゅう発生するので、注意深く扱わなければならない危険な言語なんですが、大抵のプログラマーはこんな細かい仕様にまで気を配らないので、いとも簡単に潜在バグを埋め込んでしまいます。上記の例では、x
とy
の値がともにゼロ以上の場合は不具合が発覚せず、負数を含むテストを実施しないとバグが顕在化しないところが厄介です。
代表的なコンパイラはこういった危険なコードに対して警告を出してくれますが、警告ではなくエラー扱いにしておくのが安全です。
- Compiler Warning (level 3) C4018 | Microsoft Learn
- Diagnostic flags in Clang — Clang documentation (
-Wsign-compare
)
条件演算子(三項演算子)
条件演算子(三項演算子)cond ? x : y
にも同様の罠が潜んでいます。
#include <stdio.h> int main(void) { signed int x = -1; unsigned int y = 1; long long z = ((long long)x < (long long)y) ? x : y; printf("%lld\n", z); return 0; }
前回の罠を踏まえ、今回はちゃんと比較前にキャストして型を揃えています。また、結果を格納する変数z
の型を、signed int
およびunsigned int
の両方を格納するのに十分な長さを持つlong long
型にしました。なので、結果は-1
になるような気がしますよね?
残念ながら結果はUINT_MAX
となります。上記の例では、条件演算子の結果は暗黙的にunsigned int
に揃えられるからです。
もし上記の例でz
の型指定にC++11以降のauto
型推論を使った場合、unsigned int
型になります。
なお、C++で単に小さいほうの値や大きいほうの値を選択したい場合、このようなつまらない凡ミスを防ぐためにも、わざわざ自前で式を書くようなことはせず、std::min()
やstd::max()
を使うべきです。
C#
ちなみに、C#は上記のような直感に反する危険な動作をしません。32bit符号付きのint
と32bit符号無しのuint
を比較する場合、いったん64bit符号付きのlong
に揃えられてから比較されるので、直感に則した正しい結果が得られます。さらに64bit符号付きのlong
と64bit符号無しのulong
を直接比較することはできません。明示的な型変換が必要となります。
また、int
の式とuint
の式を条件演算子の第2項と第3項に混在させることはできず、コンパイルエラーになります(C#では情報が失われる暗黙変換は許可されないため)。
int x = -1; uint y = 1; System.Console.WriteLine(x < y); // True var z = (x < y) ? x : y; // コンパイルエラー。 System.Console.WriteLine(z); System.Console.WriteLine(z.GetType());
C#でも、単に小さいほうの値や大きいほうの値を選択したい場合、Math.Min()
やMath.Max()
を使うべきです。
安全なコードを書く自信がない場合、過去のしがらみにとらわれた時代遅れのC/C++はさっさと捨てて、他のモダンな言語に移行しましょう。