59

In my project we have a piece of code like this:

// raw data consists of 4 ints
unsigned char data[16];
int i1, i2, i3, i4;
i1 = *((int*)data);
i2 = *((int*)(data + 4));
i3 = *((int*)(data + 8));
i4 = *((int*)(data + 12));

I talked to my tech lead that this code may not be portable since it's trying to cast a unsigned char* to a int* which usually has a more strict alignment requirement. But tech lead says that's all right, most compilers remains the same pointer value after casting, and I can just write the code like this.

To be frank, I'm not really convinced. After researching, I find some people against use of pointer castings like above, e.g., here and here.

So here are my questions:

  1. Is it REALLY safe to dereference the pointer after casting in a real project?
  2. Is there any difference between C-style casting and reinterpret_cast?
  3. Is there any difference between C and C++?
asynts
  • 2,213
  • 2
  • 21
  • 35
Eric Z
  • 14,327
  • 7
  • 45
  • 69
  • 17
    Many CPUs will trap on unaligned accesses. Is that enough of a reason for you? –  Dec 14 '12 at 15:24
  • This isn't unaligned access though – Minion91 Dec 14 '12 at 15:24
  • @Fanael, as long as my customers can see it, yes! – Eric Z Dec 14 '12 at 15:26
  • 5
    Why not *start* with an `int[4]` and then cast *that* to an `unsigned char *`? – Kerrek SB Dec 14 '12 at 15:47
  • 1
    An x86 will be OK with unaligned int access. You can make it portable by putting a union with an int member around the char array. – brian beuning Dec 14 '12 at 15:53
  • @Kerrek SB, it's a raw buffer received over the cable. – Eric Z Dec 14 '12 at 15:53
  • 1
    @Minion91, this can still result in unaligned access, because the `data` array can be aligned on an address that is not suitable for an `int`. I have seen this happen in practice (with a stack-allocated array). – Bart van Ingen Schenau Dec 14 '12 at 15:54
  • 2
    @EricZ: Well, then you could copy the wire data *into* an existing int array... basically you cannot get around at least one copy if you want to be portable. – Kerrek SB Dec 14 '12 at 15:55
  • @brian, that way it's always `sizeof(int)` bytes aligned? – Eric Z Dec 14 '12 at 15:55
  • @Kerrek, thanks. I know how to write a portable version of it. Just curious about the given original code in a legacy project. – Eric Z Dec 14 '12 at 15:59
  • @Bart, an unaligned access is probably better, at least you can correctly dereference the pointer and get right data, at cost of performance. Aligned access will inevitably (if original pointer is not properly aligned) change the pointer value thus dereferencing the wrong pointer, right? – Eric Z Dec 14 '12 at 16:02
  • 1
    @EricZ, in this case, I was working with an ARM processor which crashes the application on an unaligned access. Note that with 'unaligned access', I mean dereferencing an incorrectly aligned pointer value. – Bart van Ingen Schenau Dec 14 '12 at 16:15
  • 5
    I'm old enough to have seen environments with `sizeof(int) == 2` and I'm probably young enough to see environments with `sizeof(int) > 4`. I hope you aren't hard-coding `sizeof(int)` to `4` in your real code. – Klas Lindbäck Dec 14 '12 at 16:22
  • so does it mean it's hard or impossible to profile alignement tests ? – jokoon Dec 15 '12 at 00:05
  • @EricZ Yes. The alignment of a union is the alignment of the strictest member variable. – brian beuning Dec 16 '12 at 13:18
  • @BartvanIngenSchenau Didn't know that :x seems very strange. Some sort of extreme memory optimization perhaps – Minion91 Dec 17 '12 at 10:18

7 Answers7

43

1. Is it REALLY safe to dereference the pointer after casting in a real project?

If the pointer happens to not be aligned properly it really can cause problems. I've personally seen and fixed bus errors in real, production code caused by casting a char* to a more strictly aligned type. Even if you don't get an obvious error you can have less obvious issues like slower performance. Strictly following the standard to avoid UB is a good idea even if you don't immediately see any problems. (And one rule the code is breaking is the strict aliasing rule, § 3.10/10*)

A better alternative is to use std::memcpy() or std::memmove if the buffers overlap (or better yet bit_cast<>())

unsigned char data[16];
int i1, i2, i3, i4;
std::memcpy(&i1, data     , sizeof(int));
std::memcpy(&i2, data +  4, sizeof(int));
std::memcpy(&i3, data +  8, sizeof(int));
std::memcpy(&i4, data + 12, sizeof(int));

Some compilers work harder than others to make sure char arrays are aligned more strictly than necessary because programmers so often get this wrong though.

#include <cstdint>
#include <typeinfo>
#include <iostream>

template<typename T> void check_aligned(void *p) {
    std::cout << p << " is " <<
      (0==(reinterpret_cast<std::intptr_t>(p) % alignof(T))?"":"NOT ") <<
      "aligned for the type " << typeid(T).name() << '\n';
}

void foo1() {
    char a;
    char b[sizeof (int)];
    check_aligned<int>(b); // unaligned in clang
}

struct S {
    char a;
    char b[sizeof(int)];
};

void foo2() {
    S s;
    check_aligned<int>(s.b); // unaligned in clang and msvc
}

S s;

void foo3() {
    check_aligned<int>(s.b); // unaligned in clang, msvc, and gcc
}

int main() {
    foo1();
    foo2();
    foo3();
}

http://ideone.com/FFWCjf

2. Is there any difference between C-style casting and reinterpret_cast?

It depends. C-style casts do different things depending on the types involved. C-style casting between pointer types will result in the same thing as a reinterpret_cast; See § 5.4 Explicit type conversion (cast notation) and § 5.2.9-11.

3. Is there any difference between C and C++?

There shouldn't be as long as you're dealing with types that are legal in C.


* Another issue is that C++ does not specify the result of casting from one pointer type to a type with stricter alignment requirements. This is to support platforms where unaligned pointers cannot even be represented. However typical platforms today can represent unaligned pointers and compilers specify the results of such a cast to be what you would expect. As such, this issue is secondary to the aliasing violation. See [expr.reinterpret.cast]/7.

bames53
  • 86,085
  • 15
  • 179
  • 244
  • 4
    @StackedCrooked Makes you wonder if someone decided patching the compiler to handle his legacy code was easier than fixing alignment issues when he tried porting it to a new platform... – Dan Is Fiddling By Firelight Dec 14 '12 at 19:22
  • The first paragraph reads as if the alignment problem is some implication of the strict aliasing rule; however aliasing and alignment are two completely separate rules – M.M Jun 08 '16 at 23:55
  • @M.M In the code in question, the strict aliasing rule is what gives the implementation license to crash with a bus error. There is no separate 'alignment rule' which that code is violating. – bames53 Jun 09 '16 at 21:18
  • @bames53 There is a separate 'alignment rule'. You could delete the strict aliasing rule from the standard (and say all aliasing is allowed) and this code would still be undefined behaviour due to the alignment rule. In C11 it is 6.3.2.1/7 (unaligned cast causes UB). In C++ slightly more complicated – M.M Jun 09 '16 at 22:09
  • @M.M Okay, I see that in C (6.3.2.3/7), but my concern is C++. Where's the rule in C++? – bames53 Jun 10 '16 at 09:27
  • Well. In C++ the result of the cast is *unspecified*. So it could be `nullptr`, or anything else. (If one possible choice for *unspecified* leads to UB then the code is considered to have UB). – M.M Jun 10 '16 at 09:31
  • @M.M 1) That's not an 'alignment rule'. 2) I don't think it actually can do that and meet the other pointer conversion requirements. Anyway, say the code checks for nullptr. In fact say it checks to ensure that the cast pointers actually are pointing to the same memory addresses. What's the alignment rule in C++ that's being violated? I maintain that 3.10/10 is the relevant rule, not a separate 'alignment rule'. – bames53 Jun 10 '16 at 10:32
  • It is an alignment rule: if the pointer would be unaligned then the result is unspecified. There are no other pointer conversion requirements for unaligned pointers. There is nothing mentioned about alignment in the strict aliasing rule. – M.M Jun 10 '16 at 10:55
  • @M.M Anyway, say the code checks for nullptr. In fact say it checks to ensure that the cast pointers actually are pointing to the same memory addresses. What's the alignment rule in C++ that's being violated? Please cite the specific paragraphs so I can read them myself. – bames53 Jun 10 '16 at 11:51
29

It's not alright, really. The alignment may be wrong, and the code may violate strict aliasing. You should unpack it explicitly.

i1 = data[0] | data[1] << 8 | data[2] << 16 | data[3] << 24;

etc. This is definitely well-defined behaviour, and as a bonus, it's also endianness-independent, unlike your pointer cast.

Puppy
  • 144,682
  • 38
  • 256
  • 465
  • 2
    ...and it doesn't violate strict aliasing. +1 – netcoder Dec 14 '12 at 15:33
  • 2
    `std::uint32_t` would be better – Pubby Dec 14 '12 at 15:36
  • 10
    Whether this is endian independent depends on where the bytes come from. – bames53 Dec 14 '12 at 15:49
  • 1
    Is your answer "definitely well-defined behaviour"? C99 section 6.3.1.3(3) says "[if] the new type is signed and the value cannot be represented in it ... the result is implementation-defined ...". – Joseph Quinsey Dec 14 '12 at 18:29
  • 1
    This may cause undefined behaviour; you'll need to cast `data[3]` to `uint32_t` before shifting (and should also take measures against the case of the code being compiled on a system with 16-bit int). – M.M Jun 08 '16 at 23:58
  • 3
    How is this endianness-independent? I would say this assumes the data stores little endian integers. – Petr Skocik Aug 28 '16 at 16:36
  • 1
    Like others have already mentioned, this only works for little endian machines. I'll add more info on why that is. Your code assumes data[0] is the least significant byte (LSB), which is only true for little endian machines. If the machine was big endian, you would do i1 = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3]; – ZeZNiQ Mar 09 '20 at 16:50
8

In the example you're showing here what you do will be safe on almost all modern CPUs iff the initial char pointer is correctly aligned. In general this is not safe and not guaranteed to work.

If the initial char pointer is not correctly aligned, this will work on x86 and x86_64, but can fail on other architectures. If you're lucky it will just give you a crash and you'll fix your code. If you're unlucky, the unaligned access will be fixed up by a trap handler in your operating system and you'll have terrible performance without having any obvious feedback on why it is so slow (we're talking glacially slow for some code, this was a huge problem on alpha 20 years ago).

Even on x86 & co, unaligned access will be slower.

If you want to be safe today and in the future, just memcpy instead of doing the assignment like this. A modern complier will likely have optimizations for memcpy and do the right thing and if not, memcpy itself will have alignment detection and will do the fastest thing.

Also, your example is wrong on one point: sizeof(int) isn't always 4.

Art
  • 19,807
  • 1
  • 34
  • 60
  • When you say the char pointer is correctly aligned in the example, how can you tell that? It's 4 byte aligned? – Eric Z Dec 14 '12 at 15:42
  • It depends on your architecture. On almost anything that you'll find in a server or desktop today 4 byte alignment is good enough. And by "server" I don't mean weird supercomputers, I mean things that are likely to serve you web pages or relay your mail. On everything else the answer is: whatever malloc returned. This is btw. only valid for `int`. Most modern architectures have data types that require better alignment. – Art Dec 14 '12 at 15:46
6

The correct way to unpack char buffered data is to use memcpy:

unsigned char data[4 * sizeof(int)];
int i1, i2, i3, i4;
memcpy(&i1, data, sizeof(int));
memcpy(&i2, data + sizeof(int), sizeof(int));
memcpy(&i3, data + 2 * sizeof(int), sizeof(int));
memcpy(&i4, data + 3 * sizeof(int), sizeof(int));

Casting violates aliasing, which means that the compiler and optimiser are free to treat the source object as uninitialised.

Regarding your 3 questions:

  1. No, dereferencing a cast pointer is in general unsafe, because of aliasing and alignment.
  2. No, in C++, C-style casting is defined in terms of reinterpret_cast.
  3. No, C and C++ agree on cast-based aliasing. There is a difference in the treatment of union-based aliasing (C allows it in some cases; C++ does not).
ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • 2
    "the compiler and optimiser are free to treat the source object as uninitialised": the compiler is also free to assume that the source object is an orange. Or that `2+2 == "Elvis"` is true. –  Dec 14 '12 at 15:38
  • @Fanael I don't understand; what do you mean by that? – ecatmur Dec 14 '12 at 15:43
  • 1
    It's undefined behavior, so the compiler is free to assume *anything*. –  Dec 14 '12 at 15:50
  • @Fanael right, and it's undefined behaviour *because* there does not exist an object of the appropriate type at that address - i.e., it's uninitialised. – ecatmur Dec 14 '12 at 15:56
2

Update: I overlooked the fact that indeed smaller types may be unaligned relatively to a larger one, like it may be in your example. You can aleviate that issue by reversing the way you cast your array : declare your array as an array of int, and cast it to char * when you need to access it that way.

// raw data consists of 4 ints
int data[4];

// here's the char * to the original data
char *cdata = (char *)data;
// now we can recast it safely to int *
i1 = *((int*)cdata);
i2 = *((int*)(cdata + sizeof(int)));
i3 = *((int*)(cdata + sizeof(int) * 2));
i4 = *((int*)(cdata + sizeof(int) * 3));

There won't be any issue on array of primitives types. The issues of alignment occur when dealing with arrays of structured data (struct in C), if the original primitve type of the array is larger than the one it is casted to, see the update above.

It should be perfectly ok to cast an array of char to an array of int, provided you replace the offset of 4 with sizeof(int), to match the size of int on the platform the code is supposed to run on.

// raw data consists of 4 ints
unsigned char data[4 * sizeof(int)];
int i1, i2, i3, i4;
i1 = *((int*)data);
i2 = *((int*)(data + sizeof(int)));
i3 = *((int*)(data + sizeof(int) * 2));
i4 = *((int*)(data + sizeof(int) * 3));

Note that you will get endianness issues only if you share that data somehow from one platform to another with a different byte ordering. Otherwise, it should be perfectly fine.

didierc
  • 14,572
  • 3
  • 32
  • 52
  • Issues of alignment occur when you use indirection through a pointer that is not aligned properly to its type. An int often has a stricter alignment than a char (which has the least strict alignment), and implementations often make these casts be identity operations, leaving potential for unaligned pointers to ints. – R. Martinho Fernandes Dec 14 '12 at 15:37
2

You may want to show him how things can differ depending on the compiler version:

Apart from alignment there is a second problem: the standard allows you to cast an int* to char* but not the other way around (unless the char* was originally casted from an int*). See this post for more details.

Community
  • 1
  • 1
StackedCrooked
  • 34,653
  • 44
  • 154
  • 278
1

Whether you have to worry about alignment depends on the alignment of the object from which the pointer originated.

If you cast to a type which has stricter alignment requirements, it is not portable.

The base of a char array, like in your example, is not required to have any stricter alignment than that for the element type char.

However, a pointer to any object type can be converted to a char * and back, regardless of alignment. The char * pointer preserves the stronger alignment of the original.

You can use a union to create a char array which is more strongly aligned:

union u {
    long dummy; /* not used */
    char a[sizeof(long)];
};

All the member of a union start at the same address: there is no padding at the beginning. When a union object is defined in storage, it must therefore have an alignment which is suitable for the most strictly aligned member.

Our union u above is aligned strictly enough for objects of type long.

Violating the alignment restrictions can cause the program to crash when it is ported to some architectures. Or it may work, but with mild to severe performance impact, depending on whether misaligned memory accesses are implemented in hardware (at the cost of some extra cycles) or in software (traps to the kernel, where software emulates the access, at a cost of many cycles).

Kaz
  • 55,781
  • 9
  • 100
  • 149