14

Following this comment, I've tried to google why, but my google-fu failed.

Comment from link:

[...] But the important thing is that arrays and pointers are different things in C.

Assuming you're not using any compiler extensions, you can't generally pass an array itself to a function, but you can pass a pointer, and index a pointer as if it were an array.

You're effectively complaining that pointers have no length attached. You should be complaining that arrays can't be passed as function arguments, or that arrays degrade to pointers implicitly.

4
  • I am not sure it is the answer, but part of the type of an array is its size, so I believe you would have to define a function for every size you want to accept.
    – clcto
    Jun 20, 2014 at 16:56
  • Do you mean as function pointers? Please clarify the question.
    – user949300
    Jun 20, 2014 at 16:57
  • 2
    @user949300 No, from the context of the comment it's pretty clear; you can't pass an array to a function because it becomes a pointer, and he wants to know why this is the case.
    – Doval
    Jun 20, 2014 at 17:03
  • @DocBrown rlemon proposed an edit for this. Jun 20, 2014 at 17:09

4 Answers 4

21

My first guess for the reason was simply because of performance and memory saving reasons, and for the ease of compiler implementation as well (especially for the kind of computers at the time when C was invented). Passing huge arrays "by value" seemed to have a huge impact at the stack, it needs a full array copy operation for each function call, and probably the compiler must be smarter to output the correct assembly code (though the last point is debatable). It would also be more difficult to treat dynamically allocated arrays the same way as statically allocated arrays (from the viewpoint of the language's syntax).

EDIT: after reading some parts from this link, I think the real reason (and the reason why arrays in structs are treated as value types, while sole arrays are not) is the backward compatibility to C's predecessor B. Here is the cite from Dennis Ritchie:

[...} The solution constituted the crucial jump in the evolutionary chain between typeless BCPL and typed C. It eliminated the materialization of the pointer in storage, and instead caused the creation of the pointer when the array name is mentioned in an expression. The rule, which survives in today's C, is that values of array type are converted, when they appear in expressions, into pointers to the first of the objects making up the array.

This invention enabled most existing B code to continue to work, despite the underlying shift in the language's semantics. [..]

7
  • 6
    A struct Foo { int array[N]; } can be passed by value. And the last bit about treating dynamic and static allocations the same seems fishy (an array in the strictest sense always includes a size, the unifying concepts for things like array indexing are pointers coupled with array-to-pointer decay), could you elaborate?
    – user7043
    Jun 20, 2014 at 17:30
  • @delnan: I think the general principles stated here are sound. Clearly, if you wrap your array in a struct, you're specifying your intent. In the general case, you're almost always going to pass by reference. Jun 20, 2014 at 17:33
  • I also find the comment referenced in the OP unnecessarily pedantic. Clearly you're passing a pointer rather than an array by value. What's equally true, though, is that you're effectively passing an array by reference. If the objection is that there's no length attached, it's easy enough to pass that as well. Jun 20, 2014 at 17:37
  • @RobertHarvey That's still an asymmetry in the type system: Everything is passed by value, except array types (even though arrays that are part of a struct type are passed by value), and it even uses the exact same notation (both at call site and in the function signature).
    – user7043
    Jun 20, 2014 at 17:48
  • @delnan: Why is that relevant, other than you have to remember it? Jun 20, 2014 at 18:54
9

A PDP minicomputer with only 8 kB of memory can't allocate a very large stack. So, on such a machine, one has to be careful in a language design (or evolution) to be able to minimize what needs to go on the stack for expected common subroutine call usage. C is still used today to program extremely memory constrained (a few kB) embedded systems, so the trade-off is usually a good one.

On a processor architecture which has very few registers, passing any array by pointer rather than by value more often allows a register to be used as a subroutine call optimization.

1
  • 2
    I have some boards that have 256 bytes of RAM for the data and 2K EEPROM for the code. You don't want to make a copy of an array there. Mar 16, 2016 at 21:39
0

Expanding on Doc Brown's answer a bit...

In B, when you declared an array as

auto a[10];

the compiler would set aside an extra word to store an offset to the first element of the array (essentially a pointer):

   +–––+      +–––+
a: |   | –––> |   | a[0]
   +–––+      +––-+
              |   | a[1]
              +–––+
               ...
              +–––+
              |   | a[9]
              +–––+

The array subscript operation a[i] was defined as *(a + i); given the starting address stored in a, offset i words and dereference the result.

When he was designing C, Ritchie wanted to keep B's a[i] == *(a + i) array behavior, but he didn't want to set aside storage for that explicit pointer. When you declare an array in C as:

T a[10]; // for any type T

what you get in memory is:

   +–––+
a: |   | a[0]
   +–––+
   |   | a[1]
   +–––+
    ...
   +–––+
   |   | a[9]
   +–––+

No storage is set aside for a pointer to the first element. Instead, he came up with the rule that unless it is the operand of the sizeof, _Alignof, or the unary & operators, an expression of type ”N-element array of T" will converted, or "decay" to an expression of type "pointer to T" and its value will be the address of the first element of the array.

This way, a[i] == *(a + i) still works, without needing to actually store a pointer value anywhere. a is not a pointer, but under most circumstances evaluates to a pointer value (although you can use the subscript operator on a pointer variable as well).

The downside of this rule is that array expressions lose their array-ness under most circumstances, including when passed as function arguments.

This behavior is unique to arrays; structs use a different mechanism to compute member access.

0

Arrays can be passed to functions; they just have to be wrapped in a struct type, which is slightly inconvenient:

struct array {
  int a[42];
};

struct array fun(struct array in)
{
   in.a[0]++;
   return in;
}

So, there is way to do this, and that way has a binary interface. Why isn't there a syntax for writing the same thing, which gets rid of the wrapping structure?

The likely historic reason is that passing fixed-size arrays is very limiting, and has performance implications, since there will be situations in which a copy of the array cannot be avoided in by-value passing.

What you want is to be able to pass an array object to a function, such that the object has a variable size, and the function can ask that object what the size is (and out-of-bounds accesses to the array can terminate the program with a diagnostic message).

It sounds simple, but it requires complexity in the compiler and run-time support that the C designers weren't willing to commit to.

Suppose someone wants to dynamically allocate such an array. malloc will no longer do; the object has state, and requires construction. So you need syntax for that. You need to solve the ownership problem. When the array gets passed around here and there, who will free the memory; should there be reference counting or garbage collection ...

That's a considerable abstraction that requires complications in the run-time support.

Speaking of dynamic allocations, I recall from Dennis Ritchie's article on the history of C that at one point, C arrays had a pointer representation: they were objects with a header which pointed to the data. Even such a paltry representation caused a problem because whereas the pointer could be initialized for a defined object (global or local variable), that wouldn't happen for something coming from malloc. That might have been the motivating change to drop the pointer. And, of course, the pointer didn't disappear; it just moved into the type system, which was equipped with rules to calculate the pointer to the first element of the array in certain situations.


By the way, if you're ever working on the FFI for a programming langauge which interoperates with C, you can design things so that in your language, you can pass a vector or array to a function like the above fun, without the structure wrapping.

So at least from the other language, you can pass arrays to C functions and obtain them; the syntactic limitation of C doesn't have to leak into neighboring languages that are interfacing with it.

Obligatory demo:

$ cat array.c
struct array {
  int a[42];
};

struct array inc_zero(struct array in)
{
  in.a[0]++;
  return in;
}
$ gcc array.c --shared -o array.so
$ txr
This is the TXR Lisp interactive listener of TXR 289.
Quit with :quit or Ctrl-D on an empty line. Ctrl-X ? for cheatsheet.
Everything you type here can and will be used against you in comp.lang.lisp.
1> (with-dyn-lib "./array.so"
     (deffi inc-zero "inc_zero" (array 42 int) ((array 42 int))))
inc-zero
2> (inc-zero #(1 2 3))
#(2 2 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0 0 0 0)

In the Lisp code, we defined inc-zero as binding to the inc_zero function in array.so. The return value type is (array 42 int), as is the argument type. No struct definition.

We can pass a vector literal to the function (even though it's smaller than 42 elements). We obtain a new vector of 42 elements in which the zero element is incremented.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.