正しいオペランドの種類によって結果が異なるという興味深いシナリオに直面しましたが、その理由がよくわかりません。
最小限のコードは次のとおりです。
#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」の結果を受け入れます。コンパイラが最初にcheck
をuint64_t
にプロモートし、次に他の操作を実行する場合も、2番目の出力を受け入れます。
しかし、どうして&
with 0xFFFF(int32_t
)vs。0xFFFFU(uint32_t
)は、これら2つの異なる出力になるのでしょうか。
それは確かに興味深いコーナーケースです。アーキテクチャが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結果タイプで表現可能の場合、それが結果の値です。 それ以外の場合、動作は未定義です。
見てみましょう
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
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;
}
次に、元の16ビットが左シフトされ、上位ビットセット(0x80000000U)で32ビット値になるため、負の値になります。 64ビット変換中に符号拡張が発生し、上位ワードに1が入力されます。
プラットフォームには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;
}
a1
とa2
のタイプは何ですか?
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
プロモーションと符号拡張が行われる場所を示します。
これは整数拡張の結果です。 &
操作が発生する前に、オペランドが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)
check
をunsigned int
に明示的にキャストすると、結果はどちらの場合も同じになります(unsigned * signed
はunsigned
になります)。
((uint32_t)check & 0xFFFF) << 16
に等しくなります:
((uint32_t)check & 0xFFFFU) << 16
&演算には2つのオペランドがあります。 1つ目はunsignedshortで、通常のプロモーションを経てintになります。 2つ目は定数で、1つのケースではint型、もう1つのケースではunsignedint型です。したがって、&の結果は、一方の場合はintであり、もう一方の場合はunsignedintです。その値は左にシフトされ、符号ビットが設定されたintまたは符号なしintになります。負の整数をuint64_tにキャストすると、大きな負の整数が得られます。
もちろん、常にルールに従う必要があります。何かをしたときに結果がわからない場合は、それを行わないでください。