4

一部のデータのバイト単位のサイズを見つけることは、一般的な操作です。

考案された例:

char *buffer_size(int x, int y, int chan_count, int chan_size)
{
    size_t buf_size = x * y * chan_count * chan_size;  /* <-- this may overflow! */
    char *buf = malloc(buf_size);
    return buf;
}

ここでの明らかなエラーは、int がオーバーフローすることです (たとえば、23171x23171 RGBA バイト バッファ)。

3 つ以上の 値を乗算する場合の昇格のルールは何ですか?
(値のペアを乗算するのは簡単です)

安全にプレイして、キャストするだけです。

size_t buf_size = (size_t)x * (size_t)y * (size_t)chan_count * (size_t)chan_size;

もう 1 つの方法は、括弧を追加して、乗算と昇格の順序が予測可能であることを確認することです (ペア間の自動昇格は期待どおりに機能します) ...

size_t buf_size = ((((size_t)x * y) * chan_count) * chan_size;

...うまくいきますが、私の質問は.


3 つ以上の値を乗算して自動的に昇格させる決定論的な方法はありますか?
(オーバーフローを避けるため)

それとも、これは未定義の動作ですか?


ノート...

  • ここで使用してもオーバーフローは防止size_tされません。その型の最大値のオーバーフローが防止されるだけです。
  • 与えられた例では、引数もそうsize_tである可能性があることは理にかなっていますが、それはこの質問のポイントではありません.
4

1 に答える 1

5

C (および C++) では、算術演算子の型は次のように決定されます。

  1. 両方のオペランドは、「通常の算術変換」を使用して同じ型に変換されます。

  2. それが結果のタイプです。

算術型または列挙型のオペランドを期待する多くの二項演算子は、同様の方法で変換を行い、結果の型を生成します。目的は、結果の型でもある共通の型を生成することです。このパターンは、通常の算術変換と呼ばれます[注 1] [注 2]

他にルールはないので、2 つ以上の演算子を使用する式に特別なケースはありません。各操作は、構文に従って個別に入力されます。

オーバーフローの可能性を回避または削減するために、結果の型が自動的に拡張されることはありません。オペランドは両方とも「結果の型でもある」共通の型に変換されます。したがって、2 つintの s を乗算すると、結果は になり、intオーバーフローにより未定義の動作が発生します。[注3]

言語の構文は、完全な式をグループ化する方法を正確に定義しており、構文に準拠するには評価が必要です。構文ではグループ化が必要なため、式a + b + cは式と同じ結果になる必要があります。(a + b) + cコンパイラは、結果がすべての有効な入力に対して意味的に同一であることを示すことができる場合、適切と思われるように計算を再配置できます。ただし、演​​算子の結果の型を変更することはできません。は、通常の算術変換を と の型に適用し、その型との型に再度適用しa + b + cた結果の型を持たなければなりません。【注4】abc

通常の算術変換は、C 標準の §6.3.1.8 (「通常の算術変換」) と、C++ の §5 (式) の概要の段落 10 で詳しく説明されています。大まかに言えば、次のようになります。

  1. 両方のオペランドが浮動小数点の場合、両方のオペランドは 2 つの型のうち幅の広い方に変換されます。一方のオペランドが浮動小数点の場合、もう一方はその浮動小数点型に変換されます。

  2. それ以外の場合、両方のオペランドが符号付き整数型である場合、それらは両方とも と の 2 つの型のうち最も広いものに変換されますint

  3. それ以外の場合、両方のオペランドが少なくとも と同じ大きさの符号なし整数型である場合unsigned int、それらは両方とも 2 つの型のうち幅の広い方に変換されます。

【注5】

ここでa * b * c * dabcおよびdがすべてintであり、目的は を生成することsize_tです。

構文的には、その式は と同等(((a * b) * c) * d)であり、通常の算術変換はそれに応じて操作ごとに適用されます。キャスト ( ) で変換a すると、括弧で囲まれたように変換が適用されます。したがって、オペランドと の結果はになり、したがって の結果も同様になります。つまり、すべてのオペランドが符号なしの値に変換され、すべての乗算が符号なしの乗算として実行されます。これは明確に定義されていますが、値のいずれかが負の場合はおそらく意味がありません。size_t(size_t)a * b * c * d(size_t)a * bsize_t(size_t)a * b * c(size_t)a * b * c * dsize_tsize_t

2 番目または 3 番目の乗算は a の容量を超える可能性がありますsize_tが、size_tは符号なしであるため、計算はモジュロ 2 Nで実行されます。 ここNで、 は の値のビット数ですsize_t。したがって、キャストはオーバーフローを回避するという意味では安全ではありませんが、少なくとも未定義の動作は回避します。


ノート

  1. 引用は C++ 標準の §5、パラグラフ 10 からのものです。C11 には複雑な算術型が含まれているため、C 標準の §6.3.1.8 にはもう少し複雑なバージョンがあります。整数 (および非複素数浮動小数点) オペランドの場合、C と C++ のセマンティクスは同じです。

  2. シフト演算子は例外です。そのため、「多くの二項演算子」と表示されています。シフト演算子の結果の型は、右のオペランドの型に関係なく、正確に左のオペランドの (おそらく昇格された) 型です。すべてのビット演算子は整数に制限されているため、実数を含む「通常の算術変換」の部分はこれらの演算子には適用されません。

  3. 2 つの を乗算unsigned intすると、結果は になりunsigned int、計算はすべての値に対して定義されます。

    結果の符号なし整数型で表現できない結果は、結果の型で表現できる最大値よりも 1 大きい数値を法として減らされるため、符号なしオペランドを含む計算はオーバーフローすることはありません。(C §6.2.5/9)

  4. C と C++ の両方の標準は、この点で非常に明確であり、理解を深めるための例が含まれています。一般に、符号付き整数演算子も浮動小数点演算子も結合的ではないため、計算に符号なし整数演算のみが含まれる場合にのみ、計算を再グループ化および再配置できます。

    整数演算の再グループ化が禁止されるケースの例は、C 標準の §5.1.2.3 の例 6 および C++ 標準の §1.9 の段落 9 として表示されます。(同じ例です。) 16 ビットの s を持つマシンがintあり、符号付きオーバーフローがトラップになるとします。その場合、次のa = a + 32760 + b + 5;ように書き換えることはできませんa = (a + b) + 32765;

    a と b の値がそれぞれ -32754 と -15 の場合、a + b の合計はトラップを生成しますが、元の式は生成しません。

  5. それらは単純で問題のないケースです。通常、他のものは避けるようにしてください。

    a. 上記が発生する前に、いずれかのオペランドの型が より狭い場合int、そのオペランドは または のいずれかに昇格されintますunsigned intint通常、署名されていない場合でも、に昇格されます。int型のすべての値を表すのに十分な幅がない場合にのみ、オペランドは に昇格されunsigned intます。たとえば、ほとんどのアーキテクチャでは、オペランドはではなくunsigned charに昇格されます(とが同じ幅のアーキテクチャは可能ですが、一般的ではありません)。intunsigned intcharint

    b. 最後に、一方の型が署名され、もう一方が署名されていない場合、それらは両方とも次のように変換されます。

    • 少なくとも符号付きタイプと同じ幅の場合は、符号なしタイプ。(例: unsigned int* int=> unsigned int)

    • unsigned 型のすべての値を保持するのに十分な幅がある場合は、signed型。(例: unsigned int* long long=>が より広いlong long場合)long longint

    • 上記のケースのいずれも当てはまらない場合は、符号付きの型に対応する符号なしの型。

于 2015-05-19T00:19:16.783 に答える