暗黙の型変換

暗黙の型変換と聞いて、多くの C プログラマーは「ああ、それなら知ってるよ」と思うことでしょう。そして思い浮かべるのは、次のような場面ではないでしょうか。

確かに型が異なる二者間での初期化・代入・返却で発生するのは、わかりやすい暗黙の型変換です。

ですが、通常の C や C++ のプログラムの中で実際に発生している暗黙の型変換の頻度は、こんなものではありません。その様子は、あたかも「ほとんどすべての変数が int になりたがっている」かのようです。

結論

C や C++ のプログラマーは、初心者を脱する頃になると int を毛嫌いし始める傾向にあります。まるで int を撲滅すれば移植性が上がるとでも言うように。

ですが、特別な事情がない限り、整数の型には int か unsigned int のどちらかを選びましょう。特に、値が大きくないからという理由だけで char や short を選んではいけません。

それでも整数の型に int か unsigned int 以外を選ぶなら、これから説明する「汎整数拡張」をしっかり理解したうえで、ソースコード上で暗黙に展開されるそれらを脳内でトレースできなければなりません。私なら、その煩わしさに苛まれるよりも、少し気をつけて int を使うほうを選びます。

前提

話を簡単にするために、ここではプリミティブな整数型のサイズがそれぞれ次のように扱われる処理系を想定して話を進めます。

バイト数
int 4
char 1
short 2
long 4

汎整数拡張

C の規格では、整数に対して算術演算子またはビット演算子を適用しようとしたとき、コンパイラは先にすべての項の型を「昇格」させてから演算子を適用するというルールがあります。これを汎整数拡張といいます。

昇格

汎整数拡張では、すべての整数型は符号の有無にかかわらず、収容できる限り int に変換されます。つまり次の型はすべて、演算子を当てた瞬間 int に変換されてしまうということです。

  • signed char
  • unsigned char
  • short
  • unsigned short

符号拡張

符号あり整数の昇格では、符号の維持のため、最上位ビットを上位側に敷き詰めるような拡張がなされます。これを符号拡張といいます。以下に実例を挙げます。

昇格前の型 昇格前の値 昇格後の値
signed char 0x40 0x00000040
signed char 0x80 0xFFFFFF80
unsigned char 0x80 0x00000080
signed short 0x4000 0x00004000
signed short 0x8000 0xFFFF8000
unsigned short 0x8000 0x00008000

演算子

汎整数拡張が発生するトリガとなる演算子をすべて列挙すると、次のようになります。

  • 単項演算子
    • +a
    • -a
    • ~a
  • 二項演算子
    • a * b
    • a / b
    • a % b
    • a + b
    • a - b
    • a << b
    • a >> b
    • a & b
    • a ^ b
    • a | b

ここで再度、型の昇格は演算の「前に」行われるという点を思い出してください。それを考えれば、変数というものはいわば空気に触れただけで int に早変わりするような存在だということがお分かりいただけるのではないかと思います。

たとえば次のコードには一見、型変換が存在しないかのように見えます。

ですが、実際は汎整数拡張によって、コンパイラは次のコードに相当するような型変換を暗黙的に行っているのです。

このように、char や short を多用するプログラムでは、水面下で昇格と降格がひっきりなしに行われているということを、組み込みプログラマーは自覚しなければなりません。このことが、予想もしないバグにつながることもあるのですから。

ソフトウェアの設計・実装がうまく行かなくてお困りではありませんか? 組込屋にお任せください。リモート案件は、常駐の半額でお受けしています。

6 Replies to “暗黙の型変換”

  1. なるほど、以下のようなコードを書いていて、nのキャストが必要だったかどうだったか自身が無くて調べておりました。
    unsignedな変数であっても式の評価中はsigned扱いとなり、n==0 の時、(n-1)%15はキャストなしでも -1 となるということですね。

    void foo(uint8_t n)
    {
    // funcList[ (((long)n-1)%15 + 1) ](n);
    funcList[ ((n-1)%15 + 1) ](n); // n==0の時のみ例外的な処理をしたい
    }

    1. そうですね。n は uint8_t なので、-1 を当てた瞬間に signed int に拡張されます。n が uint32_t だとされないんですが。

      ところで負の数の余りを取る演算にはご注意ください。実際に走らせて -1 % 15 が -1 になっていれば問題ないですが、CPU によっては -1 % 15 の結果が 14 になるものもありますので。

  2. 環境依存ということですね。ありがとうございます。

    普段ならここまで見通しの悪くなるコーディングはしないのですが、お遊びで色々な予約語の使用を禁止したコーディングチャレンジをしている中での疑問でした。

  3. 符号拡張で下記説明がありますが、
    signed char :0x80 ->0xFFFFFF80

    そもそも、上記:signed char :0x80は、マイナス表現を超えている内容になります。signed char :0x81であれば、符号拡張として話がわかります。

    認識が間違っているでしょうか?

    1. signed char が表現できる範囲は -128 ~ +127 ですよね。0x80 は、このときの -128 に相当するので、これは単なるマイナス表現だと理解しています。

      ▼signed char が表現する値
      0x80: -128
      0x81: -127

      0xFF: -1
      0x00: 0
      0x01: +1
      :
      0x7E: +126
      0x7F: +127

      マイナス表現を超えている、の意味するところを私が正しく受け止められていないだけかもしれませんが……。

  4. 見事にハマりました笑
    センサから同じ値の反転値を交互に読んで異常がないかをチェックするのを家で書いておりまして、

    uint8_t rcv, rcv_old;
    :
    if(~rcv != rcv_old) std::cout << "NG T_T" << std::endl;

    反転後のキャストを忘れてNGのままという…。

    空気に触れただけでintに。
    心にしみる、いい言葉であります。

コメントを残す

メールアドレスが公開されることはありません。

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)