簡體   English   中英

在 C++20 中輸入雙關 uint64_t 作為兩個 uint32_t

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM