[英]convert a long string of characters to uint32_t or uint64_t in c++
[英]Type-pun uint64_t as two uint32_t in C++20
由於嚴格的別名規則,這段將uint64_t
讀取為兩個uint32_t
代碼是 UB:
uint64_t v;
uint32_t lower = reinterpret_cast<uint32_t*>(&v)[0];
uint32_t upper = reinterpret_cast<uint32_t*>(&v)[1];
同樣,這段寫uint64_t
上下部分的代碼也是UB,原因相同:
uint64_t v;
uint32_t* lower = reinterpret_cast<uint32_t*>(&v);
uint32_t* upper = reinterpret_cast<uint32_t*>(&v) + 1;
*lower = 1;
*upper = 1;
如何在現代 C++20 中以安全和干凈的方式編寫此代碼,可能使用std::bit_cast
?
使用std::bit_cast :
#include <bit>
#include <array>
#include <cstdint>
#include <iostream>
int main() {
uint64_t x = 0x12345678'87654321ULL;
// Convert one u64 -> two u32
auto v = std::bit_cast<std::array<uint32_t, 2>>(x);
std::cout << std::hex << v[0] << " " << v[1] << std::endl;
// Convert two u32 -> one u64
auto y = std::bit_cast<uint64_t>(v);
std::cout << std::hex << y << std::endl;
}
輸出:
87654321 12345678
1234567887654321
std::bit_cast僅在 C++20 中可用。 在 C++20 之前,您可以通過std::memcpy手動實現std::bit_cast
,但有一個例外,即此類實現不像 C++20 變體那樣constexpr
:
template <class To, class From>
inline To bit_cast(From const & src) noexcept {
//return std::bit_cast<To>(src);
static_assert(std::is_trivially_constructible_v<To>,
"Destination type should be trivially constructible");
To dst;
std::memcpy(&dst, &src, sizeof(To));
return dst;
}
對於整數的這種特定情況,非常理想的只是進行位移/或算術以將一個 u64 轉換為兩個 u32 並再次返回。 std::bit_cast
更通用,支持任何可簡單構造的類型,盡管 std::bit_cast 解決方案應該與具有高級優化的現代編譯器上的位算術相同。
與 std::bit_cast 不同,位算術的一個額外好處是它可以正確處理字節序。
#include <cstdint>
#include <iostream>
int main() {
uint64_t x = 0x12345678'87654321ULL;
// Convert one u64 -> two u32
uint32_t lo = uint32_t(x), hi = uint32_t(x >> 32);
std::cout << std::hex << lo << " " << hi << std::endl;
// Convert two u32 -> one u64
uint64_t y = (uint64_t(hi) << 32) | lo;
std::cout << std::hex << y << std::endl;
}
輸出:
87654321 12345678
123456788765432
以安全和清潔的方式
不要使用 reinterpret_cast。 不要依賴於依賴於某些特定編譯器設置和可疑的、不確定的行為的不清楚的代碼。 使用具有眾所周知的定義結果的精確算術運算。 類和運算符重載都在等着你。 例如一些全局函數:
#include <iostream>
struct UpperUint64Ref {
uint64_t &v;
UpperUint64Ref(uint64_t &v) : v(v) {}
UpperUint64Ref operator=(uint32_t a) {
v &= 0x00000000ffffffffull;
v |= (uint64_t)a << 32;
return *this;
}
operator uint64_t() {
return v;
}
};
struct LowerUint64Ref {
uint64_t &v;
LowerUint64Ref(uint64_t &v) : v(v) {}
/* as above */
};
UpperUint64Ref upper(uint64_t& v) { return v; }
LowerUint64Ref lower(uint64_t& v) { return v; }
int main() {
uint64_t v;
upper(v) = 1;
}
或接口對象:
#include <iostream>
struct Uint64Ref {
uint64_t &v;
Uint64Ref(uint64_t &v) : v(v) {}
struct UpperReference {
uint64_t &v;
UpperReference(uint64_t &v) : v(v) {}
UpperReference operator=(uint32_t a) {
v &= 0x00000000ffffffffull;
v |= (uint64_t)a << 32u;
}
};
UpperReference upper() {
return v;
}
struct LowerReference {
uint64_t &v;
LowerReference(uint64_t &v) : v(v) {}
};
LowerReference lower() { return v; }
};
int main() {
uint64_t v;
Uint64Ref r{v};
r.upper() = 1;
}
使用std::memcpy
#include <cstdint>
#include <cstring>
void foo(uint64_t& v, uint32_t low_val, uint32_t high_val) {
std::memcpy(reinterpret_cast<unsigned char*>(&v), &low_val,
sizeof(low_val));
std::memcpy(reinterpret_cast<unsigned char*>(&v) + sizeof(low_val),
&high_val, sizeof(high_val));
}
int main() {
uint64_t v = 0;
foo(v, 1, 2);
}
使用O1
,編譯器將foo
為:
mov DWORD PTR [rdi], esi
mov DWORD PTR [rdi+4], edx
ret
這意味着沒有額外的副本, std::memcpy
只是作為編譯器的提示。
單獨的std::bit_cast
是不夠的,因為結果會因系統的字節序而異。
幸運的是<bit>
還包含std::endian
。
請記住,優化器通常會在編譯時解析if
總是為真或為假,我們可以只測試字節序並采取相應的行動。
我們事先只知道如何處理大端或小端。 如果不是其中之一,則 bit_cast 結果不可解碼。
另一個可以破壞事物的因素是填充。 使用 bit_cast 假設數組元素之間填充為 0。
所以我們可以檢查是否沒有填充和字節序是大還是小,看看它是否是可鑄造的。
big
——只返回 bit_cast 的結果。little
,我們需要顛倒順序。 與 c++23 字節交換不同,因為我們交換元素。我任意決定大端序在 x[0] 處的高位具有正確的順序。
#include <bit>
#include <array>
#include <cstdint>
#include <concepts>
template <std::integral T>
auto split64(uint64_t x) {
enum consts {
BITS=sizeof(uint64_t)*8,
ELEM=sizeof(uint64_t)/sizeof(T),
BASE=BITS-ELEM,
MASK=~0ULL >> (BITS-(BITS/ELEM))
};
using split=std::array<T, ELEM>;
static const bool is_big=std::endian::native==std::endian::big;
static const bool is_little=std::endian::native==std::endian::little;
static const bool can_cast=((is_big || is_little)
&& (sizeof(uint64_t) == sizeof(split)));
// All ifs can be eliminated at compile time
// since they are always true or always false
if (!can_cast)
{
split ret;
for (int e = 0; e < ret.size(); ++e)
{
ret[e]=(x>>(BASE-e*ELEM)) & MASK;
}
return ret;
}
split tmp=std::bit_cast<split>(x);
if (is_big)
{
return tmp;
}
split ret;
for (int e=0; e < ELEM; ++e)
{
ret[e]=tmp[ELEM-(e+1)];
}
return ret;
}
uint16_t tst(uint64_t x, int y)
{
return split64<uint16_t>(x)[y];
}
我相信這應該是定義的行為。
不要打擾,因為無論如何算術都更快:
uint64_t v;
uint32_t lower = v;
uint32_t upper = v >> 32;
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.