Why Doesn't C++ Support Functions Returning Arrays

Why doesn't C++ support functions returning arrays?

I'd wager a guess that to be concise, it was simply a design decision. More specifically, if you really want to know why, you need to work from the ground up.

Let's think about C first. In the C language, there is a clear distinction between "pass by reference" and "pass by value". To treat it lightly, the name of an array in C is really just a pointer. For all intents and purposes, the difference (generally) comes down to allocation. The code

int array[n];

would create 4*n bytes of memory (on a 32 bit system) on the stack correlating to the scope of whichever code block makes the declaration. In turn,

int* array = (int*) malloc(sizeof(int)*n);

would create the same amount memory, but on the heap. In this case, what is in that memory isn't tied to the scope, only the reference TO the memory is limited by the scope. Here's where pass by value and pass by reference come in. Passing by value, as you probably know, means that when something is passed in to or returned from a function, the "thing" that gets passed is the result of evaluating the variable. In other words,

int n = 4;
printf("%d", n);

will print the number 4 because the construct n evaluates to 4 (sorry if this is elementary, I just want to cover all the bases). This 4 has absolutely no bearing or relationship to the memory space of your program, it's just a literal, and so once you leave the scope in which that 4 has context, you lose it. What about pass by reference? Passing by reference is no different in the context of a function; you simply evaluate the construct that gets passed. The only difference is that after evaluating the passed "thing", you use the result of the evaluation as a memory address. I once had a particular cynical CS instructor who loved to state that there is no such thing as passing by reference, just a way to pass clever values. Really, he's right. So now we think about scope in terms of a function. Pretend that you can have an array return type:

int[] foo(args){
result[n];
// Some code
return result;
}

The problem here is that result evaluates to the address of the 0th element of the array. But when you attempt to access this memory from outside of this function (via the return value), you have a problem because you are attempting to access memory that is not in the scope with which you are working (the function call's stack). So the way we get around this is with the standard "pass by reference" jiggery-pokery:

int* foo(args){
int* result = (int*) malloc(sizeof(int)*n));
// Some code
return result;
}

We still get a memory address pointing to the 0th element of the Array, but now we have access to that memory.

What's my point? In Java, it is common to assert that "everything is pass by value". This is true. The same cynical instructor from above also had this to say about Java and OOP in general: Everything is just a pointer. And he's also right. While everything in Java is in fact pass by value, almost all of those values are actually memory addresses. So in Java, the language does let you return an array or a String, but it does so by turning it in to the version with pointers for you. It also manages your memory for you. And automatic memory management, while helpful, is not efficient.

This brings us to C++. The whole reason C++ was invented was because Bjarne Stroustrup had been experimenting with Simula (basically the original OOPL) during his PhD work, and thought it was fantastic conceptually, but he noticed that it performed rather terribly. And so he began working on what was called C with Classes, which got renamed to C++. In doing so, his goal was to make a programming language that took SOME of the best features from Simula but remained powerful and fast. He chose to extend C due to its already legendary performance, and one tradeoff was that he chose to not implement automatic memory management or garbage collecting on such a large scale like other OOPL's. Returning an array from one of the template classes works because, well, you're using a class. But if you want to return a C array, you have to do it the C way. In other words, C++ does support returning an array EXACTLY the same way that Java does; it just doesn't do all of the work for you. Because a Danish dude thought it'd be too slow.

Why can't C functions return an array type?

You answered the question yourself already: arrays are second-class citizens.

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

As to why arrays cannot be passed by value: this was a design decision made by K&R when they were first designing the language, and it's too late to change it now because all the existing C code would break.

What does impossibility to return arrays actually mean in C?

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.)

C: returning local struct works but returning local array does not?

When a function returns a struct (or any other type of variable) by value, then a copy of that variable or structure is made, and that copy is returned to the caller. So, even though the 'original' has ceased to exist when the function ends, the copy is still valid.

However, when a function returns an array, what is actually returned is the address of the first element of that array – see What is array to pointer decay? So, in such cases, when the function has completed, the local array no longer 'exists' and the address returned becomes invalid.

You can 'emulate' this behaviour for a struct by redefining your function to return the address of the local variable, like so:

struct foobar {
int foo;
int bar;
};

// Note: BAD function, for example purpose only. DO NOT USE IT!
struct foobar* return_local(int foo, int bar) // Return a pointer-to-struct
{
struct foobar f;
f.foo = foo;
f.bar = bar;
return &f; // Any caller that attempts to use this returned pointer will
// exhibit Undefined Behaviour, as the 'f' struct will be gone!
}

Returning an array from function in C

… I noticed that the memory of the internal array in my function wasn't deallocated…

Deallocation of memory is not something you can notice or observe, except by looking at the data that records memory reservations (in this case, the stack pointer). When memory is reserved or released, that is just a bookkeeping process about what memory is available or not available. Releasing memory does not necessarily erase memory or immediately reuse it for another purpose. Looking at the memory does not necessarily tell you whether it is in use or not.

When int arr[10] = { 0 }; appears inside a function, it defines an array that is allocated automatically when the function starts executing (or at certain times within the function execution if the definition is in some nested scope). This is commonly done by adjusting the stack pointer. In common systems, programs have a region of memory called the stack, and a stack pointer contains an address that marks the end of the portion of the stack that is currently reserved for use. When a function starts executing, the stack pointer is changed to reserve more memory for that function’s data. When execution of the function ends, the stack pointer is changed to release that memory.

If you keep a pointer to that memory (how you can do that is another matter, discussed below), you will not “notice” or “observe” any change to that memory immediately after the function returns. That is why you see the value of arr_p is the address that arr had, and it is why you see the old data in that memory.

If you call some other function, the stack pointer will be adjusted for the new function, that function will generally use the memory for its own purposes, and then the contents of that memory will have changed. The data you had in arr will be gone. A common example of this that beginners happen across is:

int main(void)
{
int *p = demo(10);
// p points to where arr started, and arr’s data is still there.

printf("arr[3] = %d.\n", p[3]);
// To execute this call, the program loads data from p[3]. Since it has
// not changed, 3 is loaded. This is passed to printf.

// Then printf prints “arr[3] = 3.\n”. In doing this, it uses memory
// on the stack. This changes the data in the memory that p points to.

printf("arr[3] = %d.\n", p[3]);
// When we try the same call again, the program loads data from p[3],
// but it has been changed, so something different is printed. Two
// different things are printed by the same printf statement even
// though there is no visible code changing p[3].
}

Going back to how you can have a copy of a pointer to memory, compilers follow rules that are specified abstractly in the C standard. The C standard defines an abstract lifetime of the array arr in demo and says that lifetime ends when the function returns. It further says the value of a pointer becomes indeterminate when the lifetime of the object it points to ends.

If your compiler is simplistically generating code, as it does when you compile using GCC with -O0 to turn off optimization, it typically keeps the address in p and you will see the behaviors described above. But, if you turn optimization on and compile more complicated programs, the compiler seeks to optimize the code it generates. Instead of mechanically generating assembly code, it tries to find the “best” code that performs the defined behavior of your program. If you use a pointer with indeterminate value or try to access an object whose lifetime has ended, there is no defined behavior of your program, so optimization by the compiler can produce results that are unexpected by new programmers.

Returning an array using C

You can't return arrays from functions in C. You also can't (shouldn't) do this:

char *returnArray(char array []){
char returned [10];
//methods to pull values from array, interpret them, and then create new array
return &(returned[0]); //is this correct?
}

returned is created with automatic storage duration and references to it will become invalid once it leaves its declaring scope, i.e., when the function returns.

You will need to dynamically allocate the memory inside of the function or fill a preallocated buffer provided by the caller.

Option 1:

dynamically allocate the memory inside of the function (caller responsible for deallocating ret)

char *foo(int count) {
char *ret = malloc(count);
if(!ret)
return NULL;

for(int i = 0; i < count; ++i)
ret[i] = i;

return ret;
}

Call it like so:

int main() {
char *p = foo(10);
if(p) {
// do stuff with p
free(p);
}

return 0;
}

Option 2:

fill a preallocated buffer provided by the caller (caller allocates buf and passes to the function)

void foo(char *buf, int count) {
for(int i = 0; i < count; ++i)
buf[i] = i;
}

And call it like so:

int main() {
char arr[10] = {0};
foo(arr, 10);
// No need to deallocate because we allocated
// arr with automatic storage duration.
// If we had dynamically allocated it
// (i.e. malloc or some variant) then we
// would need to call free(arr)
}


Related Topics



Leave a reply



Submit