web-dev-qa-db-ja.com

符号付きオペランドと符号なしオペランドを使用したビット単位の「&」

正しいオペランドの種類によって結果が異なるという興味深いシナリオに直面しましたが、その理由がよくわかりません。

最小限のコードは次のとおりです。

#include <iostream>
#include <cstdint>

int main()
{
    uint16_t check = 0x8123U;

    uint64_t new_check = (check & 0xFFFF) << 16;

    std::cout << std::hex << new_check << std::endl;

    new_check = (check & 0xFFFFU) << 16;

    std::cout << std::hex << new_check << std::endl;

    return 0;
}

Linux64ビットでg ++(gccバージョン4.5.2)を使用してこのコードをコンパイルしました:g ++ -std = c ++ 0x -Wall example.cpp -o example

出力は次のとおりです。

ffffffff81230000

81230000

最初のケースでは、出力の理由がよくわかりません。

ある時点で、時間計算結果のいずれかが符号付き64ビット値(int64_t)にプロモートされ、符号拡張が発生するのはなぜですか?

16ビット値が最初に16ビット左にシフトされてから64ビット値にプロモートされた場合、どちらの場合も「0」の結果を受け入れます。コンパイラが最初にcheckuint64_tにプロモートし、次に他の操作を実行する場合も、2番目の出力を受け入れます。

しかし、どうして& with 0xFFFF(int32_t)vs。0xFFFFU(uint32_t)は、これら2つの異なる出力になるのでしょうか。

29
Alex Lop.

それは確かに興味深いコーナーケースです。アーキテクチャがuint16_tに32ビットを使用する場合、符号なし型にìntを使用するため、ここでのみ発生します。

これは、C++ 14(私の強調)のドラフトn4296からのClause 5 Expressionsからの抜粋です。

10算術型または列挙型のオペランドを期待する多くの二項演算子は変換を引き起こします...このパターンは通常の算術変換と呼ばれ、次のように定義されます。
.。
(10.5.3)—それ以外の場合、符号なし整数型のランクが他のオペランドの型のランク以上のランクの場合、符号付き整数のオペランドtypeは、符号なし整数型のオペランドの型に変換されます。
(10.5.4)—それ以外の場合、符号付き整数型のオペランドの型が符号なし整数型のオペランドの型のすべての値を表すことができるの場合、符号なし整数型は、符号付き整数型のオペランドの型に変換されます。

あなたは10.5.4の場合です:

  • uint16_tはわずか16ビットですが、intは32ビットです
  • intは、uint16_tのすべての値を表すことができます

したがって、uint16_t check = 0x8123Uオペランドは符号付き0x8123に変換され、ビット単位の&の結果は0x8123のままです。

ただし、シフト(ビット単位で表現レベルで発生)により、結果は中間の符号なし0x81230000になり、intに変換すると負の値になります(技術的には実装定義ですが、この変換は一般的な使用法です)

5.8シフト演算子[expr.shift]
.。
それ以外の場合、E1が符号付きタイプで負でない値を持ち、E1×2の場合E2 結果タイプの対応する符号なしタイプで表現可能の場合、結果タイプに変換されたその値が結果の値になります。

そして

4.7積分変換[conv.integral]
.。
3宛先タイプが署名されている場合、宛先タイプで表すことができれば、値は変更されません。それ以外の場合、値はimplementation-definedです。

(これはC++ 11の真の未定義の動作であることに注意してください...)

したがって、signed int0x81230000をuint64_tに変換して終了します。これにより、予想どおり0xFFFFFFFF81230000が得られます。

4.7積分変換[conv.integral]
.。
2宛先タイプが符号なしの場合、結果の値はソース整数と合同な最小の符号なし整数です(モジュロ2n、nは符号なしタイプを表すために使用されるビット数)。

TL/DR:ここでは未定義の動作はありません。結果の原因は、符号付き32ビットintから符号なし64ビットintへの変換です。 未定義の振る舞いである唯一の部分は、符号オーバーフローを引き起こすシフトですが、すべての一般的な実装はこれを共有し、それはC++ 14標準で定義された実装

もちろん、2番目のオペランドを強制的に符号なしにすると、すべてが符号なしになり、明らかに正しい0x81230000結果が得られます。

[編集] MSaltersによって説明されているように、シフトの結果はC++ 14以降実装定義のみですが、実際にはC++ 11の未定義の振る舞い。シフト演算子の段落は次のように述べています。

.。
それ以外の場合、E1が符号付きタイプで負でない値を持ち、E1×2の場合E2結果タイプで表現可能の場合、それが結果の値です。 それ以外の場合、動作は未定義です

21
Serge Ballesta

見てみましょう

uint64_t new_check = (check & 0xFFFF) << 16;

ここで、0xFFFFは符号付き定数であるため、(check & 0xFFFF)は、整数拡張の規則に従って符号付き整数を提供します。

あなたの場合、32ビットのintタイプでは、左シフト後のこの整数のMSビットは1であるため、64ビットの符号なしへの拡張は符号拡張を行い、左側のビットを次のように埋めます。 1の。同じ負の値を与える2の補数表現として解釈されます。

2番目のケースでは、0xFFFFUが符号なしであるため、符号なし整数が取得され、左シフト演算子は期待どおりに機能します。

ツールチェーンが最も便利な機能である__PRETTY_FUNCTION__をサポートしている場合、コンパイラが式の型をどのように認識するかをすばやく判断できます。

#include <iostream>
#include <cstdint>

template<typename T>
void typecheck(T const& t)
{
    std::cout << __PRETTY_FUNCTION__ << '\n';
    std::cout << t << '\n';
}
int main()
{
    uint16_t check = 0x8123U;

    typecheck(0xFFFF);
    typecheck(check & 0xFFFF);
    typecheck((check & 0xFFFF) << 16);

    typecheck(0xFFFFU);
    typecheck(check & 0xFFFFU);
    typecheck((check & 0xFFFFU) << 16);

    return 0;
}

出力

void typecheck(const T &) [T = int]
65535
void typecheck(const T &) [T = int]
33059
void typecheck(const T &) [T = int]
-2128412672
void typecheck(const T &) [T = unsigned int]
65535
void typecheck(const T &) [T = unsigned int]
33059
void typecheck(const T &) [T = unsigned int]
2166554624
10
Rishikesh Raje

0xFFFFはsignedintです。したがって、&操作では、32ビットの符号付き値があります。

#include <stdint.h>
#include <type_traits>

uint64_t foo(uint16_t a) {
  auto x = (a & 0xFFFF);
  static_assert(std::is_same<int32_t, decltype(x)>::value, "not an int32_t")
  static_assert(std::is_same<uint16_t, decltype(x)>::value, "not a uint16_t");
  return x;
}

http://ideone.com/tEQmbP

次に、元の16ビットが左シフトされ、上位ビットセット(0x80000000U)で32ビット値になるため、負の値になります。 64ビット変換中に符号拡張が発生し、上位ワードに1が入力されます。

2
kfsone

プラットフォームには32ビットのintがあります。

あなたのコードはまったく同等です

#include <iostream>
#include <cstdint>

int main()
{
    uint16_t check = 0x8123U;
    auto a1 = (check & 0xFFFF) << 16
    uint64_t new_check = a1;
    std::cout << std::hex << new_check << std::endl;

    auto a2 = (check & 0xFFFFU) << 16;
    new_check = a2;
    std::cout << std::hex << new_check << std::endl;
    return 0;
}

a1a2のタイプは何ですか?

  • a2の場合、結果はunsigned intにプロモートされます。
  • さらに興味深いことに、a1の場合、結果はintにプロモートされ、uint64_tに拡張されると符号拡張されます。

符号付きタイプと符号なしタイプの違いが明らかになるように、10進数で短いデモを示します。

#include <iostream>
#include <cstdint>

int main()
{
    uint16_t check = 0;
    std::cout << check
              << "  " << (int)(check + 0x80000000)
              << "  " << (uint64_t)(int)(check + 0x80000000) << std::endl;
    return 0;
}

私のシステム(32ビットintも)では、

0  -2147483648  18446744071562067968

プロモーションと符号拡張が行われる場所を示します。

1
Toby Speight

これは整数拡張の結果です。 &操作が発生する前に、オペランドがint(そのアーキテクチャーの場合)よりも「小さい」場合、コンパイラーは両方のオペランドをintにプロモートします。これは、両方がsigned intに適合するためです。

これは、最初の式が(32ビットアーキテクチャの場合)と同等になることを意味します。

// check is uint16_t, but it fits into int32_t.
// the constant is signed, so it's sign-extended into an int
((int32_t)check & (int32_t)0xFFFFFFFF)

もう一方のオペランドは、次のようにプロモートされます。

// check is uint16_t, but it fits into int32_t.
// the constant is unsigned, so the upper 16 bits are zero
((int32_t)check & (int32_t)0x0000FFFFU)

checkunsigned intに明示的にキャストすると、結果はどちらの場合も同じになります(unsigned * signedunsignedになります)。

((uint32_t)check & 0xFFFF) << 16

に等しくなります:

((uint32_t)check & 0xFFFFU) << 16
1
Groo

&演算には2つのオペランドがあります。 1つ目はunsignedshortで、通常のプロモーションを経てintになります。 2つ目は定数で、1つのケースではint型、もう1つのケースではunsignedint型です。したがって、&の結果は、一方の場合はintであり、もう一方の場合はunsignedintです。その値は左にシフトされ、符号ビットが設定されたintまたは符号なしintになります。負の整数をuint64_tにキャストすると、大きな負の整数が得られます。

もちろん、常にルールに従う必要があります。何かをしたときに結果がわからない場合は、それを行わないでください。

0
gnasher729