2

Is there any guarantee that references to sibling members will be optimized out?

Motivation:

struct using_union
{
    union
    {
        std::uint32_t i;
        std::uint8_t by[ 4 ];
    } data;
};

struct using_alias
{
    // shorter notation, no need for .data.i or .data.by
    std::uint32_t i;
    std::uint8_t (&by)[4] = reinterpret_cast< std::uint8_t(&)[4] >( i );
};

More generally, when is a reference guaranteed to be optimized out? Is it just when the reference's lifespan is shorter than that which it refers to?

j__
  • 632
  • 4
  • 18
  • The compiler generates assembly code. Members are abstract objects. –  Jul 27 '20 at 09:54
  • It is unspecified whether any reference uses any storage. There are no guarantees in any situation. – molbdnilo Jul 27 '20 at 09:56
  • "Shorter notation" isn't always desirable. The `using_alias` structure is more complex, harder to read and understand, and therefore harder to maintain or pass on to other developers. I recommend your first focus should be on good, readable, maintainable and working code. – Some programmer dude Jul 27 '20 at 09:56
  • Funny thing, it's UB: https://stackoverflow.com/questions/63112631/accessing-private-data-with-reinterpret-cast , as this minute-old answer suggests. –  Jul 27 '20 at 10:01
  • There is nothing like `guarantee .. optimized out`. "As if rule" gives compiler right to optimize lots of things, but nothing is obligatory. – Marek R Jul 27 '20 at 10:04
  • This is undefined behavior just because there different endianess will lead to different result. – Marek R Jul 27 '20 at 10:09

3 Answers3

1

There is no such guarantee.

Be careful about strict aliasing, std::uint8_t[4] is not one of the allowed aliasing types for reinterpret_cast.

Also, there is no reason for a workaround at all since if you leave out the name of the union, its members are moved to the outer namespace, i.e the following works:

#include <cstdint>

struct using_union
{
    union
    {
        std::uint32_t i;
        std::uint8_t by[ 4 ];
    };
};

int main(){
    using_union a;
    a.i=10;
}

But this still does not allow you to access any inactive member of an union in standard C++.

Strict aliasing

reinterpret_cast ( cppreference.com ) has many rules. Feel free to look at cppref as it is more friendly than the standard.

Relevant parts

A glvalue of type T1, designating an object x, can be cast to the type “reference to T2” if an expression of type “pointer to T1” can be explicitly converted to the type “pointer to T2” using a reinterpret_­cast. The result is that of *reinterpret_­cast<T2 *>(p) where p is a pointer to x of type “pointer to T1”. No temporary is created, no copy is made, and no constructors ([class.ctor]) or conversion functions ([class.conv]) are called. [expr.reinterpret.cast][7.6.1.9.11]

An object pointer can be explicitly converted to an object pointer of a different type.64 When a prvalue v of object pointer type is converted to the object pointer type “pointer to cv T”, the result is static_­cast<cv T*>(static_­cast<cv void*>(v)). [ Note: Converting a prvalue of type “pointer to T1” to the type “pointer to T2” (where T1 and T2 are object types and where the alignment requirements of T2 are no stricter than those of T1) and back to its original type yields the original pointer value. — end note ] [expr.reinterpret.cast][7.6.1.9.7]

If a program attempts to access ([defns.access]) the stored value of an object through a glvalue whose type is not similar ([conv.qual]) to one of the following types the behavior is undefined

  • (11.1) the dynamic type of the object,
  • (11.2) a type that is the signed or unsigned type corresponding to the dynamic type of the object, or
  • (11.3) a char, unsigned char, or std​::​byte type.

[basic.lval][7.2.1]

The types are not similar, and std::uint8_t[4] is not guaranteed to be any of the types below -> UB. I do not think that std::uint8_t(*)[4] decays into std::uint8_t*, but I am not 100% sure. Either way it is definitely UB because even if it did decay, std::uint8_t is not the same as unsigned char(but in practice it almost always will be).

The proper way how to deal with this in C++ is memcpy

Union type punning

See these very great answers.

TLDR: It is UB to access inactive members in C++ and it is explicitly allowed in C.

If you want to inspect values reinterpret_cast<std::byte*>(&value) is safe. Use memcpy to copy to/from std::byte buffer[] for translating between two incompatible types. Do not worry about the performance, compilers can spot these patterns.

Quimby
  • 17,735
  • 4
  • 35
  • 55
0

Is there any guarantee that references to sibling members will be optimized out?

No, there is no such guarantee.

In fact, while there might not be an explicit guarantee one way or another, you can almost count on the reference not being optimised out in practice. For example, if one creates an instance where the reference doesn't refer to the member, such optimisation would be broken:

int another = 42;
using_alias an_example {
    .i = 1337,
    .by = another,
};
eerorika
  • 232,697
  • 12
  • 197
  • 326
0

Looks like you are trying do conversion from std::uint32_t to some array of std::uint8_t which will represent that value in little endian (since this is used on x86).

Problem is that your code will generate different result depending on platform endianes. That is why this code falls in to one of C++ undefined behaviors (both versions).

To solve this without UB you need different code, then inspect it how compilers are handling that. Possible C++14 solution may look like this:

template<typename T, size_t ...indexes>
constexpr auto to_le_array_helper(T x, std::integer_sequence<size_t, indexes...>) -> 
    std::enable_if_t<std::is_integral_v<T>, 
                    std::array<std::uint8_t, sizeof(T)>>
{
    return { static_cast<std::uint8_t>((x >> (indexes * 8)) & 0xffu) ... };
}

template<typename T>
constexpr auto to_le_array(T x) -> 
    std::enable_if_t<std::is_integral_v<T>, 
                    std::array<std::uint8_t, sizeof(T)>>
{
    return to_le_array_helper<T>(x, std::make_index_sequence<sizeof(T)>{});
}

Note it forces little endian on any platform.

Here is a godbolt.
As you can see compiler is able to optimize his code to:

        mov     eax, dword ptr [rsp + 8]
        mov     dword ptr [rsp + 12], eax

Registry value is just copied to memory where array is stored.

Disclaimer:
example above assumes 8 bit byte. This doesn't have to be alwasy fulfilled.

Marek R
  • 32,568
  • 6
  • 55
  • 140