0

I have a uint8_t array. I sometimes need to treat two sequential elements as uint16_t, and copy them into another uint16_t variable. The elements may not necessarily start on a uint16_t word boundary.

At present I'm using memcpy to do so:

uint8_t bytes[] = { 0x1A, 0x2B, 0x3C, 0x4D };
uint16_t word = 0;
memcpy(&word, &bytes[1], sizeof(word));

gdb shows this works as expected:

(gdb) x/2bx &word
0x7fffffffe2e2: 0x2b    0x3c

Casting a reference to an array element to uint16_t causes only the array element being directly referenced to be copied:

word = (uint16_t)bytes[1];

(gdb) x/2bx &word
0x7fffffffe2e2: 0x2b    0x00

Using a uint16_t pointer and pointing it at an array element address cast to uint16_t * results in two sequential elements being referenced, but not copied:

uint16_t *wordp = (uint16_t *)&bytes[1];

(gdb) x/2bx wordp
0x7fffffffe2e5: 0x2b    0x3c

bytes[1] = 0x5E;

(gdb) x/2bx wordp
0x7fffffffe2e5: 0x5e    0x3c

Assigning a regular uint16_t variable the value dereferenced from the uint16_t pointer copies both sequential array elements:

word = *wordp;
bytes[1] = 0x6F;

(gdb) x/2bx wordp
0x7fffffffe2e5: 0x6f    0x3c
(gdb) x/2bx &word
0x7fffffffe2e2: 0x5e    0x3c

gcc produces no warnings when using the -Wall option.

How can I achieve the same result without the intermediate pointer?

Are there any concerns with using a pointer as described above, or with attempting to do so without the intermediate pointer?

Would using memcpy be considered preferable under certain scenarios?

The ultimate use of this is processing a Big Endian byte stream, so I'm using byteorder(3) functions as appropriate.

retrodev
  • 2,323
  • 6
  • 24
  • 48
  • 1
    Is there any reason why you don't want to use the `memcpy` version? It's the safest in terms of avoiding *undefined behavior* and I'd expect any decent optimizer to optimize out copies if possible – UnholySheep Nov 30 '20 at 22:33
  • 3
    `memcpy` is considered preferable due to the strict aliasing rule. Another option is to shift and OR to create the `uint16_t`. That eliminates the endianness problems as well. – user3386109 Nov 30 '20 at 22:33
  • There's no particular reason for not wanting to use `memcpy`; it's what I'm using in 99% of cases. I originally used shift and OR, but replaced it with `memcpy`. Then I noticed chained uses of `memcpy` across chained function calls, and the doubt set in. I think I'm aware of the options, I just don't feel I have enough experience to compare them and evaluate which I should use and why. – retrodev Nov 30 '20 at 22:36
  • It's worth noting that compiler optimizers which see a constant `2` as the size for a `memcpy` call will nearly always rewrite to involve no function call or loop at all, and just manipulate bytes using registers. The cost of that is then extremely negligible, no worry about function overhead or unnecessary memory access outside the caches. – aschepler Nov 30 '20 at 22:48

2 Answers2

2
uint8_t bytes[] = { 0x1A, 0x2B, 0x3C, 0x4D };
uint16_t word = 0;
memcpy(&word, &bytes[1], sizeof(word));

The above is good. Copies the bytes directly. If you are happy to copy the bytes over in order and not concerned about endianness, this is the way to do it.

See P__J supports women in Poland's answer for the proof that compilers are smart enough to optimize this usage so you don't have to worry about optimizing it youself.

word = (uint16_t)bytes[1];

The above simply zero-extends your 8-bit value into a 16-bit value (0x002B instead of 0x2B); presumably not what you want.

uint16_t *wordp = (uint16_t *)&bytes[1];
word = *wordp;

Do not do the above. That results in undefined behavior. A uint16_t has stricter alignment requirements than a uint8_t, and you can get an invalid address for the pointer type. That is, being a two-byte data type, it requires addresses that exist on multiple-of-two boundaries (so 0x2, 0x4, 0x6, etc.) while uint8_t doesn't have such a restriction and can exist at 0x1, 0x2, 0x3, etc.. (See section 6.2.8 of C11 Working Draft for more on alignment)

Upon dereferencing it, you have also broken the strict aliasing rule by dereferencing an object with an incompatible type. (See section 6.2.7 of C11 Working Draft for more on compatible types)

Christian Gibbons
  • 4,272
  • 1
  • 16
  • 29
  • Thanks Christian. In response to your statement on endianness. It depends on what I'm doing with the data. For situations where I care about the endianness I am using `ntohs()` to convert the bytes after copying them into the `uint16_t` variable. – retrodev Nov 30 '20 at 23:10
  • Could you also please explain further about the strict alignment requirements, and how I could end up with an invalid address? Is there some way to get gcc to warn me? Or is the cast me firmly telling it I'm happy to shoot myself in the foot? – retrodev Nov 30 '20 at 23:12
  • Updated with a bit more on alignment and strict aliasing. – Christian Gibbons Nov 30 '20 at 23:33
2

memcpy is the safest way. And using modern compilers the most efficient as memcpy will not be called.

Example (volatile to orevent optimizations):

#include <stdint.h>
#include <string.h>

int main()
{
    volatile uint8_t bytes[4];
    volatile uint16_t word = 0;
    memcpy(&word, &bytes[1], sizeof(word));
}

and the memcpy is translated to

        movzx   eax, WORD PTR [rsp-3]
        mov     WORD PTR [rsp-6], ax

https://godbolt.org/z/57n879

retrodev
  • 2,323
  • 6
  • 24
  • 48
0___________
  • 60,014
  • 4
  • 34
  • 74