4

This question already has an answer here:

I've got a question about the semantics of C and was wondering why the powers that be decided to make arrays different from every other type in the language.

I understand that arrays and pointers are different compile-time abstractions in C. An array has a fixed length and sizeof returns stride * length for an array. For a pointer it just returns the width of the pointer itself. However, arrays cannot be passed as values into functions or returned from functions directly. Effectively I'm wondering why you can't do the equivalent of the following.

int sum(int array[10]) {
    const size_t length = sizeof(array)/sizeof(array[0]);
    int out = 0;
    for (int i = 0; i < length; i++) {
        out += array[i];
    }
    return out;
}

Instead it seems to be the case that arrays are implicitly converted to pointers when passed into or returned from functions and the array length is simply discarded, thus preventing information about the length of the array from ever leaving the scope that the array was declared in.

Would there be unforeseen side-effects of extending the language to permit passing arrays into and out of functions in this way, other than perhaps breaking backward compatibility.

marked as duplicate by gnat, Doc Brown, Community Mar 16 '16 at 21:48

This question has been asked before and already has an answer. If those answers do not fully address your question, please ask a new question.

  • 7
    Because Assembly doesn't have first class arrays. – Telastyn Mar 16 '16 at 18:06
  • 1
    I’d guess it’s just because you don’t want to inadvertently pass a large array by value. – Jon Purdy Mar 16 '16 at 18:12
  • @Telastyn C seems to have some features that some assemblers lack like a type system and support for structured programming. The type system also tracks the type of thing that a pointer points to int * vs void *. @JonPurdy I guess in that case you could use int (* array)[100]? Maybe? – Gregory Nisbet Mar 16 '16 at 18:16
  • 10
    Anytime a question is asked "Why doesn't C have [blah]" just remember: 1969. – whatsisname Mar 16 '16 at 18:18
  • 2
    "... other than perhaps breaking backward compatibility" would be an entirely foreseen and entirely unacceptable side effect :-) – gnasher729 Mar 16 '16 at 19:50
3

This design decision shall be put in the historical context. C was created for implementing UNIX on a PDP11, a 16 bit CPU with less computing power than any smartphone of today, and max 4MB of memory. Optimizing compilers were far less advanced than today. Implementing an operating system on such machines required to avoid any unnecessary overhead, and as this was the primary purpose of the language, it's not unsurprising that language features favor performance.

Passing an array of known size by value would require pushing all the array content on the stack. This feature would encourage memory consumption, when memory was sparse, and moving the memory was slow. Passing an array of variable size would have required pushing the size and the content, and calculate dynamically where to find the remaining arguments on the stack. An unacceptable overhead for an OS developer !

Another important factor was that it was preferred to pass arguments in the CPU registers rather than on the stack. At that time, the keyword register was an important to fine tune code optimization. It was common practice to declare function arguments as register variables, to avoid push/pop operations. Fortunately, you can pass easily pointers via registers but not long values like arrays.

All this were sufficient reasons, to pass anything except the build-in types by address instead of by value. This was also the reason to constraint passing by address for any struct in the initial K&R.

Of course, nowadays, nobody uses register anymore. COmpiler have outperformed humans in the astute use of this resource. The keyword could be kept for backward compatibility, allowing an easy transition.

Unfortunately, passing arrays by value would break backwards compatibility, as the current semantic is used in millions of lines of code. For example what would you do with:

int a[10]; 
foo (a, 10);    // work on whole array
foo (a+1, 9);   // work on part of the array
foo (a+2, 3);   // work on a reduced part of the array
foo (a,3);      // work on the three first elements

If we take the last example, it's not easy to define a pass by value semantic. If you'd write foo(a[3]), it would conflict with the current way of passing the 4th int of an array.

If you like passing arrays by value, there are 2 options:

1) the limitation of passing struct by address is no longer in force (I think it's since C99). So you can perfectly do the folowing:

struct wrapper { 
    int a[10]; 
}; 

void foo(struct wrapper w);  // pass the whole struct including the array by value. 

2) use C++ with vectors, which enable everything one can dream of first class arrays.

  • (2) is very important in that there is no reason to change C because there are many, many higher level languages that have things like real arrays. It makes far more sense for the world to have a variety of languages, that do things in different ways, so the appropriate one for the environment can be chosen. – Steven Burnap Mar 16 '16 at 20:59
2

It is less than obvious what this hypothetical code would be supposed to mean. (I've fixed a typo in your for loop and changed the index type to size_t.)

// Hypothetical code, does not work
int sum(int array[10]) {
    const size_t length = sizeof(array) / sizeof(array[0]);
    int out = 0;
    for (size_t i = 0; i < length; ++i) {
        out += array[i];
    }
    return out;
}

One interpretation would be that sum accepts an array of 10 ints by-value. (If so, why compute the size again using sizeof(array)/sizeof(array[0])? It's already hard-coded to 10.) But why would you like a function that sums the elements of an array to be called with a copy of the array? And if you don't want to make a copy, C already lets you do this.

// Valid C99 code, does what you expect
int sum(const int (*array)[10]) {
  int out = 0;
  for (size_t i = 0; i < 10; ++i) {
    out += (*array)[i];
  }
  return out;
}

But you probably didn't mean to hard-code the array size. (Early Pascal had this and C hackers of the time hated it for this very reason.)

If you want to be able to call the function with a array of any size and have access to that size inside the function, this information somehow has to be passed in. This leads to problems.

  • The compiler would have to generate code to pass an additional parameter (the array size) to the function. This would violate the “zero overhead” philosophy.
  • If you want pass by-value semantics, calling a function becomes even trickier because the callee side does not know how many arguments it was passed. It could of course inspect the passed length argument and then figure out where to find the rest of the data. I'm not saying that this is impossible to do but significantly more complex than the other abstractions provided by C and doesn't translate trivially to assembly code for the machines of the time.
  • Even if the compiler generated code to pass in the array size to the function at run-time, your sizeof expression still wouldn't work because the sizeof operator evaluates to a compile-time constant which has to be the same for all calls to the function. So we would need an additional operator to query the size of an array passed as a parameter. This would mean even more complexity.

It also seems that there is not much need for such a feature. If you want to pass an array of arbitrary length by-reference, the

int sum(const int * array, size_t length);

idiom does just that. And if you want to pass a fixed-size array by-value, it can be emulated easily via structs.

struct coordinate
{
  double xyz[3];
};

double norm(struct coordinate c);

In C++, templates allow to move the run-time information to compile-time.

// Valid C++98, does what you really expect
template <typename T, std::size_t N>
T sum(const T (&array)[N]) {
  T out = 0;
  for (std::size_t i = 0; i < N; ++i) {
      out += array[i];
  }
  return out;
}

Here, N is a fixed constant when sum is instantiated and need not be passed as an additional run-time parameter. But unless C gets an equivalent to templates in C++, the concept of passing native arrays to functions doesn't fit naturally into the language.

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