5

Let's say you have a function taking a string as an argument:

void foo(char *arg);

If we know for certain that the array (not to be confused with string length, thanks chux) will always have a certain size, let's say 8, then we can instead do:

void bar(char (*arg)[8]);

and then call it like this:

char str[8] = "Hello";
bar(&str);

We need to add the & for this to work properly, but the above code will emit a warning if you pass an array of wrong size or type, which is exactly what I want to achieve. But we will obviously need to modify the body a bit. So my question is simply if this wrapper technique would work:

void bar(char (*arg)[8]) {
    char *tmp = (char*) arg;
    foo(tmp);
}

What I'm trying to achieve here is that warnings should be emitted if called with an array of wrong size. Is the above solution safe? Is it safe to cast pointer to array of char to pointer to char? I tried it, and it works, and emits no warnings with -Wall -Wextra -pedantic. And as soon as I change the size of str I get:

<source>: In function 'main':
<source>:18:9: warning: passing argument 1 of 'bar' from incompatible pointer type [-Wincompatible-pointer-types]
   18 |     bar(&str);
      |         ^~~~
      |         |
      |         char (*)[9]
<source>:9:17: note: expected 'char (*)[8]' but argument is of type 'char (*)[9]'
    9 | void bar(char (*arg)[8]) {
      |          ~~~~~~~^~~~~~~

which is exactly what I want. But is it safe, or is it UB? I would like to do this, not only via a wrapper, but also by rewriting the original function, like

void foo(char (*argaux)[8]) {
    char *arg = *argaux;
    // Copy body of original foo

I know that I can achieve basically the same thing using structs, but I wanted to avoid that.

Runnable code: https://godbolt.org/z/GnaP5ceMr

klutt
  • 30,332
  • 17
  • 55
  • 95

2 Answers2

3

char *tmp = (char*) arg; is wrong, these are not compatible pointer types. You can fix this easily though:

char *tmp = *arg;

*arg gives a char[8] which then decays into a pointer to its first element. This is safe and well-defined. And yes, pointers have much stronger "typing" in C than pass-by-value, so the compiler will recognize if an array of wrong size is passed.

Please note however that this leads to other problems: you can no longer have const correctness.
See Const correctness for array pointers?

Lundin
  • 195,001
  • 40
  • 254
  • 396
  • Very interesting link there. And this has not been fixed in C18? – klutt Aug 26 '21 at 12:13
  • @klutt I think fixing it would mean making lots of changes to the whole type system. – Lundin Aug 26 '21 at 12:39
  • Converting a pointer to any kind of object into a character pointer and using that pointer to access all the bytes of the object is an idiom that was established long before the C Standard was written, and I'm unaware of any intention to deprecate it. In what sense it it "wrong"? – supercat Aug 26 '21 at 20:48
  • @supercat It's wrong as in a pointless incompatible type conversion, when you could just de-reference the array pointer and get a compatible pointer type instead. I agree that it will work regardless due to the special rule in 6.3.2.3/7, but it's better to keep the code clear of casts when they aren't needed. The method describes here works for any type, not just char. – Lundin Aug 27 '21 at 06:32
  • @Lundin: Normally when constructs are described as "wrong", the term implies that the constructs might at least sometimes fail to work as intended. While there's a fair argument that dereferencing the "pointer to array" argument is cleaner, I find the idea of using the dereferencing operator to yield a pointer to an object which is going to decay into the address of its first element to be a bit clunky. If one wants to avoid the cast,, I think `char *tmp = &(*arg)[0];` would better convey that it `tmp` will receive the address of first element of an array identified by `arg`. – supercat Aug 27 '21 at 17:31
2

This is not safe:

char *tmp = (char*) arg;

Because you're attempting to convert a char (*)[8] to a char *. While you might get away with it since a pointer to an array will (at least on x86-64) have the same numeric value as a pointer to the first member of an array, the standard doesn't guarantee that it will work. You would first need to dereference the parameter:

char *tmp = *arg;

In theory you should be able to do this:

void foo(char arg[static 8]);

This means that arg must be an array of at least that size.

The description of this syntax is in section 6.7.6.3p7 of the C standard:

A declaration of a parameter as ‘‘array of type’’ shall be adjusted to ‘‘qualified pointer to type’’, where the type qualifiers (if any) are those specified within the [ and ] of the array type derivation. If the keyword static also appears within the [ and ] of the array type derivation, then for each call to the function, the value of the corresponding actual argument shall provide access to the first element of an array with at least as many elements as specified by the size expression.

However, most implementations don't enforce this restriction and it doesn't prevent you from passing an array larger than expected.

dbush
  • 205,898
  • 23
  • 218
  • 273
  • If a pointer identifies any type of object, converting it into a `char*` will yield a pointer to the first byte of that object. Outside of the One Program Rule, which would give a deficient-but-conforming implementations almost unlimited latitude to process any program in almost any fashion, what latitude would an implementation to treat `(char*)arg` differently from `*arg` in cases where the latter would be defined? – supercat Aug 26 '21 at 20:42