web-dev-qa-db-ja.com

実装定義の動作を回避する効率的な符号なしから符号付きキャスト

引数として_unsigned int_を取り、引数にintモジュロUINT_MAX + 1を法とする関数を定義したい。

最初の試行は次のようになります。

_int unsigned_to_signed(unsigned n)
{
    return static_cast<int>(n);
}
_

しかし、あらゆる言語の弁護士が知っているように、INT_MAXより大きい値の符号なしから符号付きへのキャストは実装定義です。

これを実装して、(a)仕様で規定されている動作のみに依存するようにします。 (b)最新のマシンおよび最適化コンパイラーでコンパイルして、no-opになります。

奇妙なマシンについては... UINT_MAX + 1を法とする符号なしintに一致する符号付きintがなければ、例外をスローしたいとしましょう。複数ある場合(これが可能かどうかはわかりません)、最大のものが欲しいとしましょう。

OK、2回目の試行:

_int unsigned_to_signed(unsigned n)
{
    int int_n = static_cast<int>(n);

    if (n == static_cast<unsigned>(int_n))
        return int_n;

    // else do something long and complicated
}
_

私の典型的な2の補数システムではないとき、効率についてはあまり気にしません。そして、私のコードが2050年の遍在するサインマグニチュードシステムのボトルネックになった場合、誰かがそれを見つけて最適化できると思います。

さて、この2回目の試みは、私が望むものにかなり近いものです。 intへのキャストは、一部の入力に対して実装定義されていますが、unsignedへのキャストバックは、UINT_MAX + 1を法とする値を保持するために規格によって保証されています。したがって、条件式は私が望むものを正確にチェックし、遭遇する可能性のあるシステムでは何にもコンパイルされません。

ただし...実装定義の動作を呼び出すかどうかを最初にチェックせずに、intにキャストしています。 2050年の一部の仮想システムでは、誰が何を知っているかがわかります。だからそれを避けたいとしましょう。

質問:「3回目の試行」はどのようなものですか?

要約すると、私は:

  • 符号なし整数から符号付き整数へのキャスト
  • 値mod UINT_MAX + 1を保持します
  • 標準必須の動作のみを呼び出す
  • 最適化コンパイラを使用して、一般的な2の補数マシンでノーオペレーションにコンパイルします

[更新]

これが些細な問題ではない理由を示す例を挙げましょう。

次のプロパティを持つ仮想のC++実装を検討してください。

  • sizeof(int)は4に等しい
  • sizeof(unsigned)は4に等しい
  • _INT_MAX_は32767に等しい
  • _INT_MIN_は-2に等しい32 + 32768
  • _UINT_MAX_は2に等しい32 -1
  • intの算術はモジュロ2です32 (_INT_MIN_から_INT_MAX_の範囲)
  • _std::numeric_limits<int>::is_modulo_はtrue
  • 符号なしnをintにキャストすると、0 <= n <= 32767の値が保持され、それ以外の場合はzeroが生成されます。

この仮想的な実装では、各int値に一致するunsigned値が1つ(mod UINT_MAX + 1)あります。したがって、私の質問は明確に定義されます。

この架空のC++実装は、C++ 98、C++ 03、およびC++ 11の仕様に完全に準拠していると主張しています。私はそれらのすべての言葉をすべて覚えていないことを認めます...しかし、私は関連するセクションを注意深く読んだと思います。したがって、答えを受け入れてほしい場合は、(a)この仮想的な実装を除外する仕様を引用するか、(b)正しく処理する必要があります。

実際、正解はevery標準で許可されている仮想的な実装を処理する必要があります。それが、定義により「標準で義務付けられた動作のみを呼び出す」ことの意味です。

ちなみに、_std::numeric_limits<int>::is_modulo_は複数の理由でここではまったく役に立ちません。 1つには、符号なしから符号付きへのキャストが大きな符号なしの値に対して機能しない場合でも、trueになります。もう1つは、算術が整数範囲全体をモジュロで単純にモジュロしている場合、補数または符号の大きさのシステムであってもtrueになります。等々。答えが_is_modulo_に依存している場合、それは間違っています。

[更新2]

hvdの答え 何かを教えてくれました:整数に対する私の仮想C++実装はnot現代のCで許可されています。C99およびC11標準は符号付き整数の表現について非常に具体的です。実際、2の補数、1の補数、および符号の大きさのみが許可されています(セクション6.2.6.2段落(2);)。

しかし、C++はCではありません。結局のところ、この事実は私の質問の中心にあります。

オリジナルのC++ 98標準は、はるかに古いC89に基づいており、次のように記述されています(セクション3.1.2.5)。

符号付き整数型ごとに、対応する(ただし異なる)符号なし整数型(unsignedキーワードで指定)があり、同じ量のストレージ(符号情報を含む)を使用し、同じアライメント要件を持っています。符号付き整数型の負でない値の範囲は、対応する符号なし整数型の部分範囲であり、各型の同じ値の表現は同じです。

C89は、1つの符号ビットのみを使用すること、または2の補数/ 1の補数/符号の大きさのみを許可することについて何も述べていません。

C++ 98標準はこの言語をほぼ逐語的に採用しました(セクション3.9.1段落(3)):

符号付き整数型ごとに、対応する(ただし異なる)符号なし整数型:「_unsigned char_」、「_unsigned short int_」、「_unsigned int_」が存在します、および「_unsigned long int_」、それぞれが同じ量のストレージを占有し、対応する符号付き整数型と同じアライメント要件(3.9)を持ちます。つまり、各符号付き整数タイプは、対応する符号なし整数タイプと同じオブジェクト表現を持ちます。符号付き整数型の非負値の範囲は、対応する符号なし整数型の部分範囲であり、対応する各符号付き/符号なし型の値表現は同じです。

C++ 03標準は、C++ 11と同様に本質的に同一の言語を使用します。

私が知る限り、標準のC++仕様は、その符号付き整数表現をC仕様に制約しません。そして、シングルサインビットまたはそのようなものを強制するものは何もありません。それが言うことは、非負符号付き整数は、対応する符号なしの部分範囲でなければならないということです。

したがって、INT_MAX = 32767でINT_MIN = -2であると再度​​主張します。32+32768が許可されます。あなたの答えがそうでないと仮定する場合、C++標準を引用しない限り、それは間違っています。

82
Nemo

User71404の答えを拡張:

_int f(unsigned x)
{
    if (x <= INT_MAX)
        return static_cast<int>(x);

    if (x >= INT_MIN)
        return static_cast<int>(x - INT_MIN) + INT_MIN;

    throw x; // Or whatever else you like
}
_

_x >= INT_MIN_(プロモーションルールを念頭に置いて、_INT_MIN_がunsignedに変換される場合、_x - INT_MIN <= INT_MAX_なので、オーバーフローは発生しません。

それが明らかでない場合は、「If _x >= -4u_、then _x + 4 <= 3_。」というクレームを見て、_INT_MAX_が少なくとも-の数学的な値に等しいことに注意してくださいINT_MIN-1。

!(x <= INT_MAX)が_x >= INT_MIN_を意味する最も一般的なシステムでは、オプティマイザーは2番目のチェックを削除し(そして私のシステムでは)、2つのreturnステートメントは同じコードにコンパイルでき、最初のチェックも削除できます。生成されたアセンブリリスト:

___Z1fj:
LFB6:
    .cfi_startproc
    movl    4(%esp), %eax
    ret
    .cfi_endproc
_

あなたの質問の仮想的な実装:

  • INT_MAXは32767に等しい
  • INT_MIN = -232 + 32768

不可能なので、特別な考慮は必要ありません。 _INT_MIN_は、_-INT_MAX_または_-INT_MAX - 1_に等しくなります。これは、Cの整数型(6.2.6.2)の表現に基づいており、nビットが値ビットであり、1ビットが符号ビットであり、1つのトラップ表現のみを許可します(無効な表現は含みません)パディングビットのため)、すなわち、そうでなければ負のゼロ/ _-INT_MAX - 1_を表すものです。 C++は、Cが許可する以上の整数表現を許可しません。

更新:Microsoftのコンパイラは、_x > 10_と_x >= 11_が同じことをテストしていることに気付いていないようです。 _x >= INT_MIN_が_x > INT_MIN - 1u_で置き換えられた場合にのみ、目的のコードを生成します。これは、(このプラットフォーム上で)_x <= INT_MAX_の否定として検出できます。

[質問者(Nemo)からの更新、以下の議論の詳細]

私は今、この答えはすべての場合に機能すると信じていますが、複雑な理由があります。私はこの解決策の恩恵を授与する可能性がありますが、誰かが気にした場合に備えて、すべての厄介な詳細をキャプチャしたいと思います。

C++ 11のセクション18.3.3から始めましょう。

表31は、ヘッダー_<climits>_を説明しています。

...

内容は、標準Cライブラリヘッダー_<limits.h>_と同じです。

ここで、「標準C」とはC99を意味し、その仕様は符号付き整数の表現を厳しく制限します。それらは符号なし整数に似ていますが、「署名」専用の1ビットと「パディング」専用の0個以上のビットがあります。パディングビットは整数の値に寄与せず、符号ビットは2の補数、1の補数、または符号の大きさとしてのみ寄与します。

C++ 11は_<climits>_マクロをC99から継承するため、INT_MINは-INT_MAXまたは-INT_MAX-1のいずれかであり、hvdのコードが機能することが保証されています。 (パディングにより、INT_MAXはUINT_MAX/2よりもはるかに小さくなる可能性があることに注意してください。しかし、signed-> unsignedキャストの動作のおかげで、この答えはそれをうまく処理します。)

C++ 03/C++ 98はややこしい。 「標準C」から_<climits>_を継承するために同じ文言を使用しますが、現在では「標準C」はC89/C90を意味します。

これらすべて(C++ 98、C++ 03、C89/C90)には、質問で述べた文言がありますが、これも含まれています(C++ 03セクション3.9.1段落7)。

整数型の表現は、純粋な2進記数法システムを使用して値を定義するものとします。(44)[Example:この国際規格は、2の補数、1の補数を許可します整数型の符号付き振幅表現。]

脚注(44)は、「純粋な2進記数法」を定義しています。

2進数の0と1を使用する整数の位置表現で、連続するビットで表される値は加算され、1で始まり、おそらく最上位のビットを除き、連続する2のべき乗で乗算されます。

この表現の興味深い点は、「純粋な2進記数法」の定義では符号/大きさの表現が許可されていないため、矛盾していることです。上位ビットの値、たとえば-2を許可しますn-1 (2の補数)または-(2n-1-1)(1の補数)。ただし、符号/大きさになる高ビットの値はありません。

とにかく、この「仮想実装」は、この定義では「純粋なバイナリ」としての資格がないため、除外されています。

ただし、高ビットが特別であるという事実は、小さな正の値、大きな正の値、小さな負の値、または大きな負の値など、あらゆる値に寄与することを想像できることを意味します。 (符号ビットが寄与できる場合-(2n-1-1)、なぜ-(2n-1-2)?等。)

したがって、「符号」ビットに奇抜な値を割り当てる符号付き整数表現を想像してみましょう。

符号ビットの小さな正の値は、intの正の範囲(おそらくunsignedと同じくらい大きい)になり、hvdのコードはそれをうまく処理します。

符号ビットに大きな正の値を指定すると、intの最大値がunsignedより大きくなりますが、これは禁止されています。

符号ビットに大きな負の値を指定すると、intが値の不連続な範囲を表し、仕様のその他の表現が除外されます。

最後に、小さな負の量に寄与する符号ビットはどうですか? intの値に-37のように、「符号ビット」に1を含めることができますか?したがって、INT_MAXは(たとえば)2になります。31-1およびINT_MINは-37ですか?

これにより、いくつかの数値は2つの表現を持つことになります...しかし、1の補数は2つの表現をゼロにし、「例」に従って許可されます。仕様では、ゼロが2つの表現を持つ可能性のあるonly整数であるとは述べていません。したがって、この新しい仮説は仕様で許可されていると思います。

実際、-1から_-INT_MAX-1_までの負の値は「符号ビット」の値として許容されるように見えますが、それより小さくはなりません(範囲は非連続です)。つまり、_INT_MIN_は、_-INT_MAX-1_から-1までのいずれかです。

さて、何だと思いますか?実装定義の動作を回避するhvdのコードの2番目のキャストでは、_INT_MAX_以下のx - (unsigned)INT_MINが必要です。 _INT_MIN_が少なくとも_-INT_MAX-1_であることを示しました。明らかに、xは最大で_UINT_MAX_です。負の数を符号なしにキャストすることは、_UINT_MAX+1_を追加することと同じです。すべてまとめてください:

_x - (unsigned)INT_MIN <= INT_MAX
_

場合にのみ

_UINT_MAX - (INT_MIN + UINT_MAX + 1) <= INT_MAX
-INT_MIN-1 <= INT_MAX
-INT_MIN <= INT_MAX+1
INT_MIN >= -INT_MAX-1
_

最後に示したのは、先ほど示したものです。したがって、このひねくれた場合でも、コードは実際に機能します。

それはすべての可能性を使い果たし、この非常にアカデミックな演習を終了します。

結論:C89/C90の符号付き整数には、C++ 98/C++ 03によって継承された、真剣に指定不足の動作がいくつかあります。 C99で修正されており、C++ 11は、C99から_<limits.h>_を組み込むことで修正を間接的に継承します。しかし、C++ 11でさえ、矛盾した「純粋なバイナリ表現」という表現を保持しています...

66
user743382

このコードは、仕様で規定されている動作のみに依存しているため、要件(a)は簡単に満たされます。

int unsigned_to_signed(unsigned n)
{
  int result = INT_MAX;

  if (n > INT_MAX && n < INT_MIN)
    throw runtime_error("no signed int for this number");

  for (unsigned i = INT_MAX; i != n; --i)
    --result;

  return result;
}

要件(b)ではそれほど簡単ではありません。これは、gcc 4.6.3(-Os、-O2、-O3)およびclang 3.0(-Os、-O、-O2、-O3)でno-opにコンパイルされます。 Intel 12.1.0はこれを最適化することを拒否します。そして、私はVisual Cに関する情報を持っていません。

17
Evgeny Kluev

コンパイラーに実行したいことを明示的に伝えることができます。

int unsigned_to_signed(unsigned n) {
  if (n > INT_MAX) {
    if (n <= UINT_MAX + INT_MIN) {
      throw "no result";
    }
    return static_cast<int>(n + INT_MIN) - (UINT_MAX + INT_MIN + 1);
  } else {
    return static_cast<int>(n);
  }
}

gcc 4.7.2x86_64-linuxg++ -O -S test.cpp)でコンパイルします

_Z18unsigned_to_signedj:
    movl    %edi, %eax
    ret
3
user71404

私のお金はmemcpyを使用しています。まともなコンパイラーは、それを最適化することを知っています:

#include <stdio.h>
#include <memory.h>
#include <limits.h>

static inline int unsigned_to_signed(unsigned n)
{
    int result;
    memcpy( &result, &n, sizeof(result));
    return result;
}

int main(int argc, const char * argv[])
{
    unsigned int x = UINT_MAX - 1;
    int xx = unsigned_to_signed(x);
    return xx;
}

私にとって(Xcode 8.3.2、Apple LLVM 8.1、-O3)、それは以下を生成します:

_main:                                  ## @main
Lfunc_begin0:
    .loc    1 21 0                  ## /Users/Someone/main.c:21:0
    .cfi_startproc
## BB#0:
    pushq    %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    ##DEBUG_VALUE: main:argc <- %EDI
    ##DEBUG_VALUE: main:argv <- %RSI
Ltmp3:
    ##DEBUG_VALUE: main:x <- 2147483646
    ##DEBUG_VALUE: main:xx <- 2147483646
    .loc    1 24 5 prologue_end     ## /Users/Someone/main.c:24:5
    movl    $-2, %eax
    popq    %rbp
    retq
Ltmp4:
Lfunc_end0:
    .cfi_endproc
2
Someone

xが入力の場合...

_x > INT_MAX_の場合、_0_ <_x - k*INT_MAX_ <_INT_MAX_であるような定数kを検索します。

これは簡単です-_unsigned int k = x / INT_MAX;_。次に、_unsigned int x2 = x - k*INT_MAX;_

これで_x2_をintに安全にキャストできます。 int x3 = static_cast<int>(x2);

_UINT_MAX - k * INT_MAX + 1_の場合、_x3_から_k > 0_のようなものを減算したいと思います。

現在、2の補数システムでは、_x > INT_MAX_である限り、次のようになります。

_unsigned int k = x / INT_MAX;
x -= k*INT_MAX;
int r = int(x);
r += k*INT_MAX;
r -= UINT_MAX+1;
_

_UINT_MAX+1_はC++保証ではゼロであり、intへの変換は何もしないので、_k*INT_MAX_を引いてから「同じ値」に追加し直したことに注意してください。したがって、受け入れ可能なオプティマイザーは、そのすべてのごまかしを消去できるはずです!

それは_x > INT_MAX_の問題を残します。さて、2つのブランチを作成します。1つは_x > INT_MAX_で、もう1つはなしです。なしの場合は、コンパイラーがnoopに最適化するストレートキャストを行います。 ...のあるものは、オプティマイザーが完了した後、何もしません。スマートオプティマイザーは、同じものへの両方のブランチを実現し、ブランチをドロップします。

問題:_UINT_MAX_が_INT_MAX_に対して本当に大きい場合、上記は機能しない可能性があります。 _k*INT_MAX <= UINT_MAX+1_を暗黙的に仮定しています。

おそらく次のような列挙型でこれを攻撃できます。

_enum { divisor = UINT_MAX/INT_MAX, remainder = UINT_MAX-divisor*INT_MAX };
_

私が信じる2s補数システムで2と1になります(その数学が動作することは保証されていますか?それはトリッキーです...).

これにより、例外ケースも開かれます。 UINT_MAXが(INT_MIN-INT_MAX)よりもはるかに大きい場合にのみ可能です。そのため、例外コードを何らかの方法で正確に質問するifブロックに入れることができ、従来のシステムでは速度が低下しません。

それを正しく処理するために、これらのコンパイル時定数をどのように構築するのか、正確にはわかりません。

Int型は少なくとも2バイトだと思うので、INT_MINとINT_MAXは異なるプラットフォームで変わる可能性があります。

基本的なタイプ

≤climits≥header

1
user679937