0

When doing a code review I found that the programmer had accidentally passed a typedef object directly foo(foo) when the function took a pointer as an argument foo(&foo). For some reason it still works as long as the typedef is an array and not e.g. a struct. Can someone explain why? See the following code:

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
typedef uint8_t foo_t[3];
typedef uint8_t bar_t[2];
typedef struct __attribute__((packed))
{
    foo_t foo;
    bar_t bar;
} foobar_t;

void change_foo(foo_t foo)
{
    foo_t bar = {1,2,3};
    memcpy(foo, bar, sizeof(foo_t));
}

void change_foo_p(foo_t *foo)
{
    foo_t bar = {1,2,3};
    memcpy(foo, bar, sizeof(foo_t));
}

void change_foobar(foobar_t foobar)
{
    foo_t bar = {1,2,3};
    memcpy(foobar.foo, bar, sizeof(foo_t));
}    

void change_foobar_p(foobar_t *foobar)
{
    foo_t bar = {1,2,3};
    memcpy(foobar->foo, bar, sizeof(foo_t));
}

int main()
{
    printf("Hello, World!\n");
    foobar_t foobar;
    foobar_t foobar2;
    foo_t foo;
    foo_t foo2;
    change_foo(foo);
    change_foo_p(&foo2);
    change_foobar(foobar);
    change_foobar_p(&foobar2);

    // Prints 1101 since the only method not working is change_foobar()
    printf("%d%d%d%d\n", foo[0], foo2[0], foobar.foo[0], foobar2.foo[0]);        
    return 0;
}
Viktor S
  • 761
  • 1
  • 5
  • 13
  • 7
    It's not the `typedef` but a general rule, that arrays decay to pointers when passed to functions. – Eugene Sh. Sep 17 '18 at 15:30
  • Apparently, passing `foo` instead of `&foo` should trigger a compiler warning, as the type of the pointer is wrong. But it's *value* is correct, and it is converted implicitly to the correct type, yet considered to be incompatible. – Eugene Sh. Sep 17 '18 at 15:39
  • 1
    Where does your sample code pass `foo` to a function expecting a pointer to `foo`? In `change_foo(foo)`, `change_foo` is declared to take a `foo_t`. In `change_foo_p(&foo2)`, a pointer to a `foo_t` is passed. In `change_foobar(foobar)`, `change_foobar` is declared to take a `foobar_t`. In `change_foobar_p(&foobar2)`, a pointer to a `foobar_t` is passed. None of these matches your question about passing an apparent non-pointer where a pointer is required. Are you asking about the arguments in the `memcpy` calls? – Eric Postpischil Sep 17 '18 at 15:41
  • Like Eugene Sh. says, it should trigger a warning at the very least. If you don't see a warning, up your warning level. This particular warning (about incompatible pointer types) I consider worthy of upgrading to an error. Let the compiler catch it instead of code review. – StoryTeller - Unslander Monica Sep 17 '18 at 15:48
  • 2
    Possible duplicate of [Is an array name a pointer?](https://stackoverflow.com/questions/1641957/is-an-array-name-a-pointer) It's not perfect, but it explains why the various uses of arrays are actually using pointers. [What's the purpose of this \[1\] at the end of struct declaration?](https://stackoverflow.com/q/47086406/364696) is also useful for reference on why you'd use a `typedef` like this on purpose, knowing it will degrade to pointers in most use cases. – ShadowRanger Sep 17 '18 at 15:49

3 Answers3

0

In C, an array is often automatically converted to a pointer to its first element. C 2018 6.3.2.1 3 says:

Except when it is the operand of the sizeof operator, or the unary & operator, or is a string literal used to initialize an array, an expression that has type "array of type" is converted to an expression with type "pointer to type" that points to the initial element of the array object and is not an lvalue. If the array object has register storage class, the behavior is undefined.

Thus, when the type foo_t is an array of three uint8_t, and x is an object of type foo_t, then, in a function call such as function(x), x is automatically converted so that the function call is equivalent to function(&x[0]).

However, in this case, the result is that x becomes a pointer to uint8_t. The argument must still satisfy the rules about matching argument types in the call to parameter types in the function declaration. If the function’s parameter were declared to have type pointer to foo_t (so it is a pointer to an array of three uint8_t), then the compiler should give a warning or error that function(x) is passing a pointer to uint8_t.

No such call appears in the code you included in the question, so it is unclear what you are asking about. The memcpy calls do contain arguments that are arrays, which are converted to pointers. These are permitted as arguments to memcpy because its parameters are declared to be void * or const void *, and pointers to any types of objects may be converted to void *.

(Additionally, a parameter declaration that is an array is automatically adjusted to be a pointer to an array element.)

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
0

Because foo and &foo have the same value, but different type inside a function call.

The type of foo is foo_t, which is uint8_t[2]. This is an array type, so it decays to a pointer to the first element of the array when being passed to a function (change_foo(foo) is equivalent to change_foo(&foo[0])).

The type of &foo is foo_t*, which is uint8_t(*)[2], i.e. a pointer that points to the entire array. As it points to the entire array, the address that is stored in the pointer, is the address of the first byte of its first element, which unsurprisingly is also the address of the first byte of the array's first element.

Thus, the calls change_foo(foo) and change_foo_p(&foo) compile to the same machine code. change_foo_p() invokes undefined behavior when it calls memcpy() with two non-matching pointers, but since it gets the same address passed in as change_foo() does, the undefined behavior happens to be the one you intended.

cmaster - reinstate monica
  • 38,891
  • 9
  • 62
  • 106
-2

In C when you define an array you actually store the starting address of array.

int array[25]; // stores the starting address of array. ex: 0x00ffabcd

array[10] == *(array + 10) // index operator works as pointer arithmetic.>

Note that in the above example compiler knows the pointer type and pointer arithmetic knows that it should jump to 10 * sizeof(int) from the starting address of array. In some situations, like when you pass array to memset, you might need to give sizeof operator result to the pointer arithmetic.

I will leave the answer cause the comments points to why it is wrong.

Yakup Türkan
  • 576
  • 2
  • 6
  • 21
  • 1
    No, you don't store the starting address. But in most cases, *using* the name of the array causes it to degrade to a pointer to its first element, and declaring a function argument to be an array is equivalent to declaring it to be a pointer. – ShadowRanger Sep 17 '18 at 15:41
  • May you explain your comment? – Yakup Türkan Sep 17 '18 at 15:42
  • `int array[25];` reserves space for 25 `int`-sized values (on the stack at function scope). It does not store the address of it directly, though most uses of the array make it behave as if it were a pointer. It's the difference between `char *foo = "123";` (stores address of static string) and `char foo[] = "123";` (initializes an array of size four with the data `'1', '2', '3', '\0'`, but isn't storing the address of the array itself). – ShadowRanger Sep 17 '18 at 15:46
  • More details: [Is an array name a pointer?](https://stackoverflow.com/q/1641957/364696) – ShadowRanger Sep 17 '18 at 15:48
  • 2
    @YakupTürkan This is one of the most frequent misconceptions about C, I had it too when I joined Stack-Overflow. However, arrays are not pointers. Instead, arrays **decay** to pointers. This is quite a subtle point, yet it is quite enlightening to learn the difference. And it will help you to make full use of C's multidimensional array features. I suggest that you try to read up a bit on array-pointer-decay. Google it, read on Stack-Overflow, wherever. But be weary that many author's don't know the difference, and listen to those that do. – cmaster - reinstate monica Sep 17 '18 at 16:04
  • This is a nice detail actually. In school, and in our books they were simply saying the array types are actually pointers to the starting address with some type information about the arrays. But these informations are used only when compiling, and not when in execution. Most of the time people points to execution time I guess. – Yakup Türkan Sep 17 '18 at 16:09