syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

C/C++の汎整数拡張(昇格)と条件(三項)演算子

現在主流のプロセッサは、異なる型の数値同士を直接演算することはできません。必ず型を揃える必要があります。
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に揃えられてから比較されるため、-1UINT_MAX(規格上は少なくとも65,535以上、現在主流の32bit/64bit向けの処理系では4,294,967,295)に暗黙変換され、比較結果は偽(0)となります。

C/C++ではこのような直感に反する動作がしょっちゅう発生するので、注意深く扱わなければならない危険な言語なんですが、大抵のプログラマーはこんな細かい仕様にまで気を配らないので、いとも簡単に潜在バグを埋め込んでしまいます。上記の例では、xyの値がともにゼロ以上の場合は不具合が発覚せず、負数を含むテストを実施しないとバグが顕在化しないところが厄介です。
代表的なコンパイラはこういった危険なコードに対して警告を出してくれますが、警告ではなくエラー扱いにしておくのが安全です。

条件演算子三項演算子

条件演算子三項演算子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++はさっさと捨てて、他のモダンな言語に移行しましょう。