Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What does impossibility to return arrays actually mean in C?

Tags:

c

I'm not trying to replicate the usual question about C not being able to return arrays but to dig a bit more deeply into it.

We cannot do this:

char f(void)[8] {     char ret;     // ...fill...     return ret; }  int main(int argc, char ** argv) {     char obj_a[10];     obj_a = f(); } 

But we can do:

struct s { char arr[10]; };  struct s f(void) {     struct s ret;     // ...fill...     return ret; }  int main(int argc, char ** argv) {     struct s obj_a;     obj_a = f(); } 

So, I was skimming the ASM code generated by gcc -S and seems to be working with the stack, addressing -x(%rbp) as with any other C function return.

What is it with returning arrays directly? I mean, not in terms of optimization or computational complexity but in terms of the actual capability of doing so without the struct layer.

Extra data: I am using Linux and gcc on a x64 Intel.

like image 545
Dario Rodriguez Avatar asked Jun 12 '18 03:06

Dario Rodriguez


People also ask

Why can't you return an array in C?

C returns by value. Arrays cannot be passed by value, therefore they cannot be returned.

What's the secret to returning an array from a function?

C programming does not allow to return an entire array as an argument to a function. However, you can return a pointer to an array by specifying the array's name without an index.

What does an array return in C?

It returns a local variable, but it is an illegal memory location to be returned, which is allocated within a function in the stack. Since the program control comes back to the main() function, and all the variables in a stack are freed.

Why array of reference is not possible?

An array of references is illegal because a reference is not an object. According to the C++ standard, an object is a region of storage, and it is not specified if a reference needs storage (Standard §11.3. 2/4). Thus, sizeof does not return the size of a reference, but the size of the referred object.


2 Answers

First of all, yes, you can encapsulate an array in a structure, and then do anything you want with that structure (assign it, return it from a function, etc.).

Second of all, as you've discovered, the compiler has little difficulty emitting code to return (or assign) structures. So that's not the reason you can't return arrays, either.

The fundamental reason you cannot do this is that, bluntly stated, arrays are second-class data structures in C. All other data structures are first-class. What are the definitions of "first-class" and "second-class" in this sense? Simply that second-class types cannot be assigned.

(Your next question might be, "Other than arrays, are there any other second-class data types?", and I think the answer is "Not really, unless you count functions".)

Intimately tied up with the fact that you can't return (or assign) arrays is that there are no values of array type, either. There are objects (variables) of array type, but whenever you try to take the value of one, you immediately get a pointer to the array's first element. [Footnote: more formally, there are no rvalues of array type, although an object of array type can be thought of as an lvalue, albeit a non-assignable one.]

So, quite aside from the fact that you can't assign to an array, you can't even generate a value to try to assign. If you say

char a[10], b[10]; a = b; 

it's as if you had written

a = &b[0]; 

So we've got an array on the left, but a pointer on the right, and we'd have a massive type mismatch even if arrays somehow were assignable. Similarly (from your example) if we try to write

a = f(); 

and somewhere inside the definition of function f() we have

char ret[10]; /* ... fill ... */ return ret; 

it's as if that last line said

return &ret[0]; 

and, again, we have no array value to return and assign to a, merely a pointer.

(In the function call example, we've also got the very significant issue that ret is a local array, perilous to try to return in C. More on this point later.)

Now, part of your question is probably "Why is it this way?", and also "If you can't assign arrays, why can you assign structures containing arrays?"

What follows is my interpretation and my opinion, but it's consistent with what Dennis Ritchie describes in his paper The Development of the C Language.

The non-assignability of arrays arises from three facts:

  1. C is intended to be syntactically and semantically close to the machine hardware. An elementary operation in C should compile down to one or a handful of machine instructions taking one or a handful of processor cycles.

  2. Arrays have always been special, especially in the way they relate to pointers; this special relationship evolved from and was heavily influenced by the treatment of arrays in C's predecessor language B.

  3. Structures weren't initially in C.

Due to point 2, it's impossible to assign arrays, and due to point 1, it shouldn't be possible anyway, because a single assignment operator = shouldn't expand to code that might take N thousand cycles to copy an N thousand element array.

And then we get to point 3, which really ends up leading to a contradiction.

When C got structures, they initially weren't fully first-class either, in that you couldn't assign or return them. But the reason you couldn't was simply that the first compiler wasn't smart enough, at first, to generate the code. There was no syntactic or semantic roadblock, as there was for arrays.

And the goal all along was for structures to be first-class, and this was achieved relatively early on. The compiler caught up, and learned how to emit code to assign and return structures, shortly around the time that the first edition of K&R was going to print.

But the question remains, if an elementary operation is supposed to compile down to a small number of instructions and cycles, why doesn't that argument disallow structure assignment? And the answer is, yes, it's a contradiction.

I believe (though this is more speculation on my part) that the thinking was something like this: "First-class types are good, second-class types are unfortunate. We're stuck with second-class status for arrays, but we can do better with structs. The no-expensive-code rule isn't really a rule, it's more of a guideline. Arrays will often be large, but structs will usually be small, tens or hundreds of bytes, so assigning them won't usually be too expensive."

So a consistent application of the no-expensive-code rule fell by the wayside. C has never been perfectly regular or consistent, anyway. (Nor, for that matter, are the vast majority of successful languages, human as well as artificial.)

With all of this said, it may be worth asking, "What if C did support assigning and returning arrays? How might that work?" And the answer will have to involve some way of turning off the default behavior of arrays in expressions, namely that they tend to turn into pointers to their first element.

Sometime back in the '90's, IIRC, there was a fairly well-thought-out proposal to do exactly this. I think it involved enclosing an array expression in [ ] or [[ ]] or something. Today I can't seem to find any mention of that proposal (though I'd be grateful if someone can provide a reference). At any rate, I believe we could extend C to allow array assignment by taking the following three steps:

  1. Remove the prohibition of using an array on the left-hand side of an assignment operator.

  2. Remove the prohibition of declaring array-valued functions. Going back to the original question, make char f(void)[8] { ... } legal.

  3. (This is the biggie.) Have a way of mentioning an array in an expression and ending up with a true, assignable value (an rvalue) of array type. For the sake of argument I'll posit a new operator or pseudofunction called arrayval( ... ).

[Side note: Today we have a "key definition" of array/pointer correspondence, namely that:

A reference to an object of array type which appears in an expression decays (with three exceptions) into a pointer to its first element.

The three exceptions are when the array is the operand of a sizeof operator, or a & operator, or is a string literal initializer for a character array. Under the hypothetical modifications I'm discussing here, there would be a fourth exception, namely when the array was an operand of this new arrayval operator.]

Anyway, with these modifications in place, we could write things like

char a[8], b[8] = "Hello"; a = arrayval(b); 

(Obviously we would also have to decide what to do if a and b were not the same size.)

Given the function prototype

char f(void)[8]; 

we could also do

a = f(); 

Let's look at f's hypothetical definition. We might have something like

char f(void)[8] {     char ret[8];     /* ... fill ... */     return arrayval(ret); } 

Note that (with the exception of the hypothetical new arrayval() operator) this is just about what Dario Rodriguez originally posted. Also note that — in the hypothetical world where array assignment was legal, and something like arrayval() existed — this would actually work! In particular, it would not suffer the problem of returning a soon-to-be-invalid pointer to the local array ret. It would return a copy of the array, so there would be no problem at all — it would be just about perfectly analogous to the obviously-legal

int g(void) {     int ret;     /* ... compute ... */     return ret; } 

Finally, returning to the side question of "Are there any other second-class types?", I think it's more than a coincidence that functions, like arrays, automatically have their address taken when they are not being used as themselves (that is, as functions or arrays), and that there are similarly no rvalues of function type. But this is mostly an idle musing, because I don't think I've ever heard functions referred to as "second-class" types in C. (Perhaps they have, and I've forgotten.)


Footnote: Because the compiler is willing to assign structures, and typically knows how to emit efficient code for doing so, it used to be a somewhat popular trick to co-opt the compiler's struct-copying machinery in order to copy arbitrary bytes from point a to point b. In particular, you could write this somewhat strange-looking macro:

#define MEMCPY(b, a, n) (*(struct foo { char x[n]; } *)(b) = \                          *(struct foo *)(a)) 

that behaved more or less exactly like an optimized in-line version of memcpy(). (And in fact, this trick still compiles and works under modern compilers today.)

like image 66
Steve Summit Avatar answered Oct 17 '22 03:10

Steve Summit


What is it with returning arrays directly? I mean, not in terms of optimization or computational complexity but in terms of the actual capability of doing so without the struct layer.

It has nothing to do with capability per se. Other languages do provide the ability to return arrays, and you already know that in C you can return a struct with an array member. On the other hand, yet other languages have the same limitation that C does, and even more so. Java, for instance, cannot return arrays, nor indeed objects of any type, from methods. It can return only primitives and references to objects.

No, it is simply a question of language design. As with most other things to do with arrays, the design points here revolve around C's provision that expressions of array type are automatically converted to pointers in almost all contexts. The value provided in a return statement is no exception, so C has no way of even expressing the return of an array itself. A different choice could have been made, but it simply wasn't.

like image 36
John Bollinger Avatar answered Oct 17 '22 04:10

John Bollinger