1. ホーム
  2. c

[解決済み] 暗黙の型推進ルール

2023-01-10 12:54:53

質問

この投稿は、C 言語の暗黙の整数プロモーション、特に通常の算術変換や整数プロモーションによる暗黙のプロモーションに関する FAQ として使用されることを意図しています。

例 1)

なぜ255ではなく、奇妙な大きな整数が出るのでしょうか?

unsigned char x = 0;
unsigned char y = 1;
printf("%u\n", x - y); 

例2)

なぜ、"-1 is larger than 0"と出るのでしょうか?

unsigned int a = 1;
signed int b = -2;
if(a + b > 0)
  puts("-1 is larger than 0");

例3)

なぜ、上記の例で型を short に変更すると問題が解決するのはなぜですか?

unsigned short a = 1;
signed short b = -2;
if(a + b > 0)
  puts("-1 is larger than 0"); // will not print

(これらの例は、16ビットショートの32ビットまたは64ビットコンピュータを対象としています)。

どのように解決するのですか?

C 言語は、式で使用されるオペランドの整数型を暗黙のうちに変更するように設計されています。言語がコンパイラーにオペランドをより大きな型に変更するか、またはその符号を変更するように強制するいくつかのケースが存在します。

この背後にある論理的根拠は、演算中の偶発的なオーバーフローを防ぐためだけでなく、異なる符号を持つオペランドが同じ式に共存できるようにするためでもあります。

残念ながら、暗黙の型推進ルールは、C 言語における最大の欠陥の 1 つと言えるほど、善よりも害の方がはるかに多いのです。これらのルールは、平均的な C プログラマーにさえ知られていないことが多く、したがって、非常に微妙なバグのすべての方法を引き起こしています。

典型的な例としては、プログラマが「x 型にキャストすれば動作する」と言っても、その理由が分からないというシナリオがあります。または、そのようなバグは、一見シンプルで単純なコードの中で、まれで断続的な現象が発生することがあります。C のほとんどのビット演算子は、符号付きオペランドを与えられたときに定義が不十分な動作をするため、暗黙の昇格はビット操作のコードで特に厄介です。


整数の型と変換順位

C言語の整数の型は char , short , int , long , long longenum .

_Bool / bool も、タイププロモーションの際には整数型として扱われます。

すべての整数は、指定された 変換順位 . C11 6.3.1.1、最も重要な部分に重点を置いています。

すべての整数型は、以下のように定義された整数変換ランクを持ちます。

- 2つの符号付き整数型は、たとえ同じ表現であっても、同じランクを持ってはならない。

- 符号付き整数型のランクは、より精度の低い符号付き整数型のランクより大きくなければならない。

- のランクは long long int のランクより大きくなければならない。 long int の順位よりも大きいものとします。 int のランクよりも大きいこと。 short int のランクよりも大きいこと。 signed char .

- 符号なし整数型のランクは、対応する符号付き整数型がある場合はそのランクと等しくなければならない。


- 任意の標準整数型の階数は、同じ幅を持つ任意の拡張整数型の階数より大きくなければならない。

- charのランクは、signed charとunsigned charのランクに等しくなければならない。

- Boolのランクは、他のすべての標準的な整数型のランクより小さくなければならない。

- 任意の列挙型のランクは,互換性のある整数型のランクと等しくなければならない(6.7.2.2参照)。

からの型は stdint.h の型はここでも、与えられたシステム上でたまたま対応した型と同じランクでソートされます。例えば int32_tint と同じランクです。

さらに、C11 6.3.1.1では、どのような型が 小整数型 (正式な用語ではありません) とみなされる型を指定しています。

があるところでは、式の中で次のように使うことができます。 int または unsigned int が使えます。 が使われます。

- 整数型を持つオブジェクトまたは式(ただし int または unsigned int のランク以下である)の整数変換を行う。 intunsigned int .

このやや不可解な文章が実際に意味するのは、次のようなことです。 _Bool , charshort (また int8_t , uint8_t など)は、"small integer types"と呼ばれるものです。これらは特別な方法で扱われ、以下に説明するように暗黙の昇進の対象となります。


整数のプロモーション

式の中で小さな整数型が使われた場合、暗黙のうちにそれが int に暗黙的に変換され、常に符号付きになります。これは 整数プロモーション または 整数プロモーションルール .

形式的には、このルールは次のようになります(C11 6.3.1.1)。

もし int が元の型の全ての値を表すことができる場合(ビットフィールドの場合、幅で制限されます)、その値は int に変換され、それ以外の場合は unsigned int . これらは 整数プロモーション .

これは、すべての小整数型が、符号の有無にかかわらず、暗黙のうちに(符号あり)に変換されることを意味します。 int に暗黙的に変換されることを意味します。

このテキストはしばしば次のように誤解されます: "すべての小さい符号付き整数型は符号付きintに変換され、すべての小さい符号なし整数型は符号なしintに変換される"。これは誤りです。ここでいう符号なしとは、たとえば unsigned short というオペランドがあり int と同じサイズになります。 short と同じサイズであった場合、その unsigned short オペランドは次のように変換されます。 unsigned int . というように、実際には何も起こりません。しかし、場合によっては short よりも小さいタイプである場合は int に変換され、常に(符号付き) int , が符号付きか符号なしかに関係なく、常に !

整数のプロモションによる厳しい現実は、C言語ではほとんどすべての演算が char または short . 操作は常に int またはそれ以上の型に対して行われます。

これはナンセンスに聞こえるかもしれませんが、幸運なことにコンパイラはコードを最適化することが許されています。例えば、2つの unsigned char というオペランドを含む式では、オペランドは int に昇格し、演算は int . しかし、コンパイラはこの式を最適化し、実際には期待どおり8ビット演算として実行されるようにすることが許されています。しかし、ここで問題が発生します。コンパイラは ではなく コンパイラは整数昇格による暗黙の符号変化を最適化することができないのです。なぜなら、プログラマーが意図的に暗黙の昇格に依存しているのか、それとも意図的でないのかをコンパイラーが判断する方法がないからです。

これが、問題の例1が失敗する理由です。両方の unsigned char オペランドは、型 int に昇格しているため、演算が行われるのは int の結果であり x - y は型が int . ということは -1 ではなく 255 という、予想されるようなコードが生成されます。コンパイラは、8ビット命令で実行される機械語を int の代わりに8ビット命令で実行する機械語を生成するかもしれませんが、符号の変更を最適化しないかもしれません。つまり、負の結果になってしまい、その結果、次のような奇妙な数になってしまいます。 printf("%u が呼び出されたときに奇妙な数になってしまうということです。例 1 は、演算の結果を型 unsigned char .

のようないくつかの特殊なケースを除いて ++sizeof 演算子を使用する場合、単項演算子、二項演算子(または三項演算子)に関係なく、C のほぼすべての演算に整数のプロモートが適用されます。


通常の算術変換

C言語で二項演算(オペランドが2つの演算)が行われる場合、演算子のオペランドは両方とも同じ型でなければならない。したがって、オペランドが異なる型の場合、Cは一方のオペランドを他方のオペランドの型に暗黙的に変換することを強制します。これがどのように行われるかのルールは、次のように名付けられています。 通常の算術変換 (非公式にバランシングと呼ばれることがあります)。これらはC11 6.3.18で指定されています。

(このルールは、長いネストされた if-else if 文だと思えば、読みやすいかもしれません) )

<ブロッククオート

6.3.1.8 通常の算術演算変換

算術型のオペランドを期待する多くの演算子は、変換を引き起こし、同様の方法で結果 を生成します。その目的はオペランドと結果の共通の実数型を決定することです。 と結果の共通の実数型を決定することです。指定されたオペランドに対して、各オペランドは、タイプ ドメインは変更されず、各オペランドは対応する実数型が共通の実数型となる型に変換される。特に断りのない限り への変換を行う。特に明示しない限り、共通の実数型は、その実数型に対 の対応する実数型でもあり、その型領域はオペランドが同じであればその型領域である。 それ以外は複素数である。このパターンは 通常の算術変換 :

  • まず、どちらかのオペランドに対応する実数型が long double である場合、もう一方のオペランドは型領域の変更なしに、対応する実数型が long double .
  • それ以外の場合、どちらかのオペランドに対応する実数型が double である場合、もう一方のオペランドは型領域の変更なしに、対応する実数型が double .
  • それ以外の場合、どちらかのオペランドに対応する実数型が float である場合、もう一方のオペランドは、型領域の変更なしに、対応する実数型が float である型に変換されます。
  • そうでない場合は、両方のオペランドで整数のプロモションが実行されます。そして、昇格されたオペランドに以下のルールが適用されます。 昇格されたオペランドに次のルールが適用されます。

    • 両方のオペランドが同じ型を持っている場合、それ以上の変換は必要ありません。
    • そうでない場合、オペランドが両方とも符号付き整数型であるか、両方とも符号なし整数型である場合 を持つ場合、整数の変換順位の低い方のオペランドは、より大きな順位のオペランドに変換されます。 より大きい順位のオペランドの型に変換されます。
    • それ以外の場合、符号なし整数型を持つオペランドがランクが大きいか等しい場合 もう一方のオペランドの型のランクと同じであれば、符号付き整数型を持つオペランドは 符号付き整数型を持つオペランドは、符号なし整数型を持つオペランドの型に変換されます。 の型に変換されます。
    • それ以外の場合、符号付き整数型のオペランドの型が、符号付き整数型のオペランドの型のすべての値を表すことができる場合 符号なし整数型のオペランドの型のすべての値を表すことができる場合は 符号なし整数型のオペランドは、符号あり整数型のオペランドの型に変換されます。 の型に変換されます。
    • そうでなければ、両方のオペランドは符号なし整数型に変換されます。 符号付き整数型のオペランドの型に対応する符号なし整数型に変換されます。

ここで注目すべきは、通常の算術変換が浮動小数点と整数の両方の変数に適用されることです。整数の場合、通常の算術変換の中から整数のプロモションが呼び出されることにも注目です。そしてその後,両方のオペランドが少なくとも int を持つ場合、演算子は同じ型、同じ符号で釣り合わされます。

このため a + b が奇妙な結果になる理由です。オペランドは両方とも整数で、少なくともランクが int であるため、整数のプロモションは適用されません。オペランドは同じ型でなく - aunsigned int であり bsigned int . したがって、演算子 b は一時的に型 unsigned int . この変換の間に符号の情報が失われ、大きな値として終了します。

に型を変更する理由は short に変更すると問題が解決するのは short が小整数型であるためです。つまり、両方のオペランドが整数の型に昇格され int に整数昇格し、符号付きであることを意味します。整数昇格後、両オペランドは同じ型( int )、さらなる変換は必要ありません。そして、期待通りに符号付き型に対して演算を実行することができます。