std::memcpy or explicit char value assignment - equal and legal in C++

Viewed 333

In the following example the value-representation of an uint32_t is copied to an uint8_t array. This is done by std::memcpy. As I understand the C++ standard this is totally legal: we are accessing an object of type T via T* casted to a unsigned char*. No aliasing problem, no alignment problem.

The other way round is less obvious. We are accessing the object representation of T via unsigned char*, which is legal. But does the term accessing include changing?

Of course there are no aliasing and no aligment problems. There are however problems if the values in buffer s origin from a foreign source: we must ensure the right endianness and omit trap representations. The right endianness can be checked, so that can be solved. But what about trap representations? How can we avoid that? Or do uint-types not have trap representations as opposed to say double?

I know that another (more compliant?) way would be shifting the uint8_t values into the uint_t object. We still have to obey the endianness but this should safely omit trap representations.

But shifts of large types on small µC (8-Bit) can be quite expensive!

The next question is if the second try (see below in the code) is equivalent to the memcpy approach with respect to legality and functionality? Well, it looks like the memcpy version is more optimizer friendly.

#include <cstdint>
#include <cstring>
#include <cassert>

typedef uint32_t utype;

constexpr utype value = 0x01020304;

int main() {
    utype a{value};
    utype b{0};
    uint8_t s[sizeof(utype)]{};

    // first     
    std::memcpy(s, &a, sizeof(utype));
    assert(s[0] == (value & 0xff));

    std::memcpy(&b, s, sizeof(utype));
    assert(b == value);

    // second    

    const uint8_t* ap = reinterpret_cast<const uint8_t*>(&a);
    s[0] = ap[0]; // explicitly legal in C++
    s[1] = ap[1];
    s[2] = ap[2];
    s[3] = ap[3];
    assert(s[0] == (value & 0xff));

    uint8_t* bp = reinterpret_cast<uint8_t*>(&b);
    bp[0] = s[0]; // same as memcpy or ist this UB ?
    bp[1] = s[1];
    bp[2] = s[2];
    bp[3] = s[3];
    assert(b == value);
}
1 Answers

But does the term accessing include changing?

Yes.

Note: In fact that's what memcpy does conceptionally. It modifies bytes as if they were narrow character objects. If this weren't possible, then memcpy couldn't be implemented in standard c++.

But what about trap representations? How can we avoid that?

This is quite tricky. If you know of a trap representation, then you would have to test for it using the narrow character view of the object, before trying to use the value in the type that has a trap representation. I don't know if there are any standard ways of dealing with trap representations.

Maybe there should be a std::is_trap<T>(void*) trait to solve this, but there isn't as far as I know.

I know that another (more compliant?) way would be shifting the uint8_t values into the uint_t object. We still have to obey the endianness but this should safely omit trap representations.

If the foreign value is a trap representation, then that value probably isn't representable anyway, so this shifting might have some other problem such as overflow in such case.

The difference between shifting and memcpy is that shifting allows converting known endianness to native endianness while memcpy works when the source already has native endianness.


If it were guaranteed that uint8_t were an alias of unsigned char, then the second snippet would be well defined and functionally equivalent to memcpy. I don't know if it is guaranteed, but it is certainly common. Only narrow character type has exceptions to pointer aliasing rules.


assert(s[0] == (value & 0xff));

This assert relies on endianness of the cpu.

Related