定义了与 24 位和 8 位 var 联合的行为

Defined behaviour for union with 24-bit and 8-bit vars

提问人:Jan Kuhlmann 提问时间:10/9/2023 更新时间:10/9/2023 访问量:70

问:

我正在尝试找到将 24 位和 8 位无符号整数打包成 32 位的最佳方法,而无需位移来提取数据。工会立即想到了一种简单的方法,如下所示:

union {
    uint32_t u24;
    uint8_t u8[4]; // use only u8[3]
}

但是,这种方法会导致基于系统字节序的未定义行为,因此我想出了以下方法,该方法使用 c++20 功能在编译时使用 std::endian 和 constexpr 检测系统的字节序:

#include <bit>
struct UnionTest {
    union {
        uint32_t u24;
        uint8_t u8[4];
    };
    
    inline constexpr uint8_t get_u8_index() const noexcept {
        if constexpr (std::endian::native == std::endian::little) return 0;
        else if constexpr (std::endian::native == std::endian::big) return 3;
        else // crap the bed
    }
};

// use like this:
int main() {
    UnionTest test;
    test.u24 = 0xffffff;
    test.u8[test.get_u8_index()] = 0xff;
}

这可能仍然有点冗长,但这不是问题所在。我纯粹对这种方法的可行性感兴趣,假设我们从不将大于 24 位的值写入 u24。

另一种方法是使用位字段:

struct UnionTest {
    uint32_t u24 : 24;
    uint32_t u8 : 8;
}

但这可能会导致 64 位而不是 32 位(尽管在大多数情况下应该预期为 32 位)。

我的问题是 A) 关于联合方法在性能和潜在未定义行为方面的可行性,以及 B) 建议的联合方法与 c++ 位域的使用之间的实际区别

C++ 联合 位域

评论

3赞 Some programmer dude 10/9/2023
字节序并不是这种结合的唯一问题。在 C++ 中,您只能从上次写入的成员中读取。不允许在 C++ 中通过联合进行类型双关语。
1赞 Some programmer dude 10/9/2023
至于位字段,它也很麻烦,因为编译器可能会将两个成员按它喜欢的任何顺序排列,没有明确定义的成员共享位的顺序。
0赞 Jan Kuhlmann 10/9/2023
@Someprogrammerdude 这意味着我最终会被迫简单地使用单个 32 位值并像往常一样使用位移进行打包/提取?或者它有编译时常量方法吗?
2赞 Some programmer dude 10/9/2023
不幸的是,这仍然是最安全、最便携的处理方式。
0赞 user17732522 10/9/2023
@JanKuhlmann 位移到底是什么问题?只需在访问器成员函数中键入一次。

答:

1赞 Serge Ballesta 10/9/2023 #1

C++语言允许访问任何对象上的字节表示。它显式用于允许简单可复制类型的字节复制。此外,如果定义了字节序,则可以预期 24 位值将 3 个高阶字节用于小端序,将 3 个低阶字节用于大端序。它仍然需要一个掩码来访问 24 位值,但 8 位值可以直接访问,并且从未使用过移位。

下面是一个可能的代码,演示了这一点:

#include <iostream>
#include <bit>

namespace {
    inline constexpr uint8_t get_u8_index() noexcept {
        if constexpr (std::endian::native == std::endian::little) return 3;
        else if constexpr (std::endian::native == std::endian::big) return 0;
        else {}// crap the bed
    }
}

class pack_24_8 {
    uint32_t value;

    static const int u8_index = get_u8_index();  // locally scoped constant

public:
    uint8_t get_u8() const {
        return ((const uint8_t*)(&value))[u8_index]; // extract one single byte
    }

    void set_u8(uint8_t c) {
        ((uint8_t*)(&value))[u8_index] = c;  // set one single byte
    }

    uint32_t get_u24() const {
        return value & 0xffffff;      // get the less significant 24 bits
    }

    void set_u24(uint32_t u24) {
        uint8_t u8 = get_u8();    // save the u8 part
        value = u24;
        set_u8(u8);               // and restore it
    }
};

// use like this:
int main() {
    pack_24_8 test;
    test.set_u8(0x5a);
    test.set_u24(0xa5a5a5);

    std::cout << std::hex << (unsigned int) test.get_u8() << " - " <<
        std::hex << test.get_u24() << '\n';

    return 0;
}

注意:正如@Caleth在评论中所说,这依赖于作为无符号字符的别名。 AFAIK 这适用于每个常见架构,但每个标准都不需要它......uint8_t

评论

0赞 Jan Kuhlmann 10/9/2023
这是一个非常好的解决方案,特别是如果您只经常访问 32 位的一部分,在本例中为 8 位。还教过我关于匿名命名空间的知识,以前从未见过这些!
0赞 Caleth 10/9/2023
这依赖于作为定义行为的别名uint8_tunsigned char
0赞 Serge Ballesta 10/9/2023
@Caleth:你当然是对的。我已经习惯了,以至于我没有想到它会有所不同......我已经用你的评论编辑了我的帖子。