繁体   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