0

Withing a function is there any legal way to interpret a char * parameter, of a given length, as an pointer of a different integral type, and then access said converted pointer? There seem to be plenty of illegal (UB) ways to do it...

For example given the following function prototype:

int32_t sum_32(char *a, int len);

I'd like to know if there is a way of writing something functionally equivalent to the following code, legally:

int32_t sum_32(char *a, int len) {
    assert(len % 4 == 0);
    int32_t total = 0;
    for (int i = 0; i < len / 4; i++) {
        total += ((int32_t *)a)[i]; 
    }
    return total;
}

Of course, one way of doing it just to break down the accesses into character-sized access with shifting to recombine into a larger value (with some assumption about the endianness, here assuming LE):

int32_t sum_32(char *a, int len) {
    assert(len % 4 == 0);
    int32_t total = 0;
    for (int i = 0; i < len; i += 4) {
        int32_t val = (int32_t)
                      (a[i+0] <<  0) +
                      (a[i+1] <<  8) +
                      (a[i+2] << 16) +
                      (a[i+3] << 24) ;
        total += val; 
    }
    return total;
}

... but here I am looking for solutions that access the underlying array one int32_t at a time.

If the answer is "it's not possible", does the answer change if I know the source of the char *a is an allocation function - or, more broadly, are there any additional restrictions I can put on a such that accessing it as a larger type is legal?

BeeOnRope
  • 60,350
  • 16
  • 207
  • 386
  • The first problem is probably alignment. If it comes from an allocation function, you at least know it's properly alignment for anything. – melpomene Dec 26 '16 at 20:41
  • I suggest you take a look at GNUs implementation of stdlib. The string functions contain a lot of this kind of word-at-a-time-processing. (after alignment is taken care of, of course) – wildplasser Dec 26 '16 at 20:42
  • 1
    As you already wrote, you need to care about alignment and endianess. For example, [this answer](http://stackoverflow.com/a/4840428/69809) has a couple of functions for rounding the pointer address modulo 4 or 8. If you need to handle different endianess, then you can as well work with individual bytes. Since this is tagged "performance", are you sure you aren't optimizing too soon? The latter function doesn't have any shifting, btw. – vgru Dec 26 '16 at 20:53
  • 1
    @wildplasser - indeed, but the implementors of GNU stdlib can make assumptions that a plain C author can't - i.e., they can assume the GNU C environment which has many stronger assumptions than the C standard (even if you were writing C targeting this environment, you couldn't necessarily take advantage of those, since they may be undocumented and subject to change, but since glib is bound to a particular compiler version, that's not an issue for them). – BeeOnRope Dec 26 '16 at 20:54
  • @Groo - I'm not optimizing too early since this is in fact just a straw-man example to illustrate the point. I actually know the best/optimal assembly for the routines I'm interested in, and want to know if it's possible to map that back to C in a legal way. This is a sub-question of that exercise. – BeeOnRope Dec 26 '16 at 20:56
  • There are some cases where you may perform the cast. It depends on what is passed to the function. Your question is very light on these details, so I don't know how to answer this. It seems like a broad question. – 2501 Dec 26 '16 at 20:58
  • @BeeOnRope Yes and no. GNU gcc is portable on tenths of different platforms, all with different calling conventions and alignment and size restrictions. And for labray implementors the particular implementation can be taken for granted; it is only one particular implementation. (some platforms may even have instrucions to treat words in either little- or big-endian ways) – wildplasser Dec 26 '16 at 20:59
  • @wildplasser - right, but what I mean is that reading the `glibc` source is informative only for how you might implement other methods _inside glibc_, and not necessarily even how you might implement your own C methods on `gcc` and certainly not indicative of what you can do in portable C. `glibc` is indeed portable to several platforms, but of course they use a lot of conditional compilation to get there - but more importantly the underlying constant factor on all those platforms is that they are still that it's targeting `gcc` which offers stronger guarantees than standardized C. – BeeOnRope Dec 26 '16 at 21:05
  • ... in particular, a conditional answer is fine. As in "in general, you can't [blah, blah] - but in the special case that `a` satisfies [blah blah], then [blarg blarg]". – BeeOnRope Dec 26 '16 at 21:10

2 Answers2

2

If the memory was last written as int32_t or any compatible type, the effective type becomes int32_t, and you can read it with a simple cast. Else it is not possible without breaking the aliasing rules.

alain
  • 11,939
  • 2
  • 31
  • 51
  • What about if the last write to the memory region was from something like `fread` - from a file or socket, or whatever. Then I can assume nothing about the "last write", right? – BeeOnRope Dec 26 '16 at 21:29
  • Also, isn't there an escape hatch in the aliasing rules for `char *` though? – BeeOnRope Dec 26 '16 at 21:31
  • 3
    Yes, you can alias any type through a `char*`, but not the other way around. – alain Dec 26 '16 at 21:32
  • Isn't that what I'm doing though? Aliasing `int` through a `char *`? So the escape hatch should apply here? The "other way around" would be trying to read a char from an `int *`, right? – BeeOnRope Dec 26 '16 at 21:35
  • 1
    In the function you have `char`s and read them as `int32_t`, this is illegal, but having `int` and reading `char` would be legal. – alain Dec 26 '16 at 21:36
  • Makes sense, I had the aliasing rule backwards. – BeeOnRope Dec 26 '16 at 21:56
  • @alain C++ does not have a "last written type" rule ... C does (and that is only for malloc'd space). Although maybe you are referring to a comment that has now been deleted – M.M Dec 26 '16 at 22:01
  • @M.M My answer was completely different and wrong before my last edit. That 'last written' difference between C and C++ is that in C++ you can only read the union member last written to, if I remember correctly. – alain Dec 26 '16 at 22:05
2

To avoid strict aliasing problems, total += ((int32_t *)a)[i]; can be replaced with:

int32_t temp;
memcpy(&temp, a+i*4, sizeof temp);
total += temp;

which the compiler will optimize to not actually call a memcpy library function. Of course, use this only if you want the intended endianness implications; use the bit-shift version otherwise.

(Note: As written in the question, the bit-shift version is wrong due to the possibility of char being signed - you'll need to either change the function to unsigned char * or use equivalent casts).

I used compiler explorer and found that for this code gcc will test if a is aligned, and if so then use XMM instructions, and if not then use old instructions.

M.M
  • 138,810
  • 21
  • 208
  • 365
  • And that *again* is the power of the libc implementation. Inlining? No problem! – wildplasser Dec 27 '16 at 00:06
  • @wildplasser what libc implementation? – M.M Dec 27 '16 at 00:58
  • Why `*4`? Suggest `memcpy(&temp, a + sizeof temp * i, sizeof temp);` or if you prefer magic numbers, then be consistent with `memcpy(&temp, a+i*4, 4);`. – chux - Reinstate Monica Dec 27 '16 at 02:29
  • @chux `i` is the number of 32-bit units, not the number of bytes. I would prefer to have the loop counter be the number of bytes (and change the loop condition accordingly) , but for my answer I decided to stick with OP's loop with condition `i < len / 4` – M.M Dec 27 '16 at 02:31
  • My comment was why use `*4` vs. `*sizeof temp` in the 2nd argument in `memcpy(&temp, a+i*4, sizeof temp);` and then use the opposite `sizeof temp` vs. `4` in the 3rd augment? Nothing to do with `i`, just code consistency in determining size. – chux - Reinstate Monica Dec 27 '16 at 02:38