【问题标题】:Explicitly setting active member in union in C++在 C++ 中显式设置联合中的活动成员
【发布时间】:2017-11-15 15:28:24
【问题描述】:

以下代码是经典的、臭名昭著的类型双关语联合。类似的代码在 C99/C11 中是合法的,但在 C++ 中是非法的,因为必须从非活动成员中读取。

要激活非活动成员,必须写入(此处不合适)或通过placement-new 构造它。

下面的代码正是这样做的:它初始化成员a,然后通过placement-new 激活成员b。在原始类型数组的情况下,构造实际上是一个 noop,并且位值保持不变。

我从未在这个话题的许多其他问题/答案中看到这个解决方案,所以我想知道下面的代码是否真的绕过了 UB。

#include <cstdint>
#include <cstddef>
#include <utility>
#include <array>
#include <tuple>
#include <cassert>

union ByteUnion32 {
public:
    explicit ByteUnion32(uint32_t v) : a{v}{
        new (&b) std::byte[sizeof(uint32_t)]; // set b active, but prevents constexpr, actually a noop
    }
    std::byte operator[](uint8_t i) const {
            return b[i];
    }
private:
    uint32_t a;
    std::byte b[sizeof(uint32_t)];
};
int main(){
    ByteUnion32 u10{0x01020304}; // initialise and activate byte-memebers
    auto v10 = std::to_integer<uint8_t>(u10[0]);    
    assert(v10 == 4);

}

【问题讨论】:

标签: c++ type-conversion language-lawyer unions placement-new


【解决方案1】:

来自[dcl.init]/12:

如果没有为对象指定初始化程序,则该对象是默认初始化的。当获得具有自动或动态存储持续时间的对象的存储时,该对象具有不确定值,如果没有对对象执行初始化,则该对象将保留一个不确定值,直到该值被替换( [expr.ass])。 [...]如果评估产生不确定的值,则行为是不确定的,除非在以下情况下:[...]

在这段代码中:

assert(v10 == 4);

v10 的初始化本身是未定义的行为,因为不符合任何项目符号(我对此并不完全确定),或者v10 的初始化很好,只是被认为具有不确定的值,此时与4 的最终比较是未定义的行为。

无论如何,这都是未定义的行为。

【讨论】:

    【解决方案2】:

    除了使用联合来提供两个转换方向之外,以下可能是 C++ 中的一个解决方案,它使用两个特化将方向std::byte 转换为更大的整数类型或整数类型到std::byte。

    #include <cstdint>
    #include <cstddef>
    #include <utility>
    #include <array>
    #include <tuple>
    #include <cassert>
    #include <cstring>
    
    namespace std {
        enum class endian { // since c++20
    #ifdef _WIN32
            little = 0,
            big    = 1,
            native = little
    #else
            little = __ORDER_LITTLE_ENDIAN__,
            big    = __ORDER_BIG_ENDIAN__,
            native = __BYTE_ORDER__
    #endif
        };
        namespace literals {
            constexpr byte operator"" _byte(unsigned long long v) { // <> Literal-Operator for suffix `_byte`
                return byte{static_cast<uint8_t>(v)};
            }
        }
    }
    
    template<typename T>
    requires (std::is_same<uint64_t, T>::value || std::is_same<uint32_t, T>::value || std::is_same<uint16_t, T>::value) && (sizeof(T) > 1)
    struct ByteUnion<ByteToIntegral, T> {
    public:
        template<typename... B>
        requires (std::is_same<B, std::byte>::value || ...) && (sizeof...(B) == sizeof(T))
        explicit ByteUnion(B... vv) : ByteUnion{endian_type{}, std::index_sequence_for<B...>{}, vv...} {}
    
        const T& value() const {return a;}     
    private:
        template<size_t... II, typename... B>
        explicit ByteUnion(LittleEndian, std::index_sequence<II...>, B&&... vv) {
            uint8_t* p = reinterpret_cast<uint8_t*>(&a);
            ((p[sizeof...(II) - 1 - II] = static_cast<uint8_t>(vv)),...);    
        }
        template<size_t... II, typename... B>
        explicit ByteUnion(BigEndian, std::index_sequence<II...>, B&&... vv) {
            uint8_t* p = reinterpret_cast<uint8_t*>(&a);
            ((p[II] = static_cast<uint8_t>(vv)),...);
        }
        T a;
    };
    
    using namespace std::literals;
    
    int main(){
        ByteUnion<IntegralToByte, uint64_t> u1{0x01020304}; // initialise and activate byte-memebers
    //    ByteUnion<ByteToIntegral, uint64_t> u1x{0x01020304}; // not possible
        auto v1 = std::to_integer<uint8_t>(u1[0]);    
    //    u1.value(); // not possible, who access inactive member
        assert(v1 == 4);
    
        ByteUnion<ByteToIntegral, uint32_t> u2{0x01_byte, 0x02_byte, 0x03_byte, 0x04_byte}; // initialise and activate integral value member
    //    ByteUnion<IntegralToByte, uint32_t> u2x{0x01_byte, 0x02_byte, 0x03_byte, 0x04_byte}; // not possible
        auto v2 = u2.value(); 
        assert(v2 == 0x01020304);
    //    u2[0];
    
    }
    

    【讨论】:

    • 据我所知,这是UB。您以uint8_t 的形式写入a,然后以不同的类型读取它。您可以将底层字节读取为std::byte 或(unsigned) char,但不能将其写入byte/char,然后读取为不同的类型。它只能以一种方式工作。
    • 在 BigEndian 的情况下,我可以用 memcpy 替换折叠表达式,那么它应该没问题。在 LittleEndian 的情况下,我的想法是,折叠表达式本质上与 memcpy 相同。
    • 你到底想达到什么目的?可能你的方法不理想,有更好的解决方案……
    • 目标是实现与 C99/C11 中通过联合的类型双关语相同的实现,另外还涉及字节顺序问题。也许我要专注于联合方法。
    • 但是你为什么要通过类型双关语来实现这一点?您选择这种方式的原因是什么?与工会?大局是什么,为什么需要这个?也许最好在int 中进行字节顺序转换。例如,在 x86 上,有一个用于此的指令,也可以从 gcc 和 msvc 访问。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-10-11
    • 2023-04-04
    • 2021-12-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多