IT TIP

C에서 배열을 반환 할 수 없다는 것은 실제로 무엇을 의미합니까?

itqueen 2020. 10. 29. 20:17
반응형

C에서 배열을 반환 할 수 없다는 것은 실제로 무엇을 의미합니까?


나는 C가 배열을 반환 할 수 없다는 일반적인 질문을 복제하려고하지 않지만 조금 더 깊이 파고 들었습니다.

우리는 이것을 할 수 없습니다 :

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

int main(int argc, char ** argv) {
    char obj_a[10];
    obj_a = f();
}

하지만 우리는 할 수 있습니다 :

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();
}

그래서 나는 gcc -S에 의해 생성 된 ASM 코드를 훑어보고 있었고 -x(%rbp)다른 C 함수 반환 과 마찬가지로 스택과 함께 작업하는 것 같습니다 .

배열을 직접 반환하는 것은 무엇입니까? 내 말은 최적화 나 계산의 복잡성이 아니라 구조체 레이어없이 그렇게 할 수있는 실제 능력의 관점에서 말입니다.

추가 데이터 : x64 Intel에서 Linux와 gcc를 사용하고 있습니다.


우선, 배열을 구조로 캡슐화 한 다음 해당 구조로 원하는 모든 작업을 수행 할 수 있습니다 (할당, 함수에서 반환 등).

두 번째로, 여러분이 발견 한 바와 같이 컴파일러는 구조를 반환 (또는 할당)하기 위해 코드를 내보내는 데 거의 어려움이 없습니다. 그렇기 때문에 배열을 반환 할 수 없습니다.

이렇게 할 수없는 근본적인 이유는 솔직히 말해서 배열이 C의 2 급 데이터 구조 이기 때문 입니다. 다른 모든 데이터 구조는 일류입니다. 이런 의미에서 "일등"과 "이등"의 정의는 무엇입니까? 단순히 두 번째 클래스 유형을 할당 할 수 없습니다.

(다음 질문은 "배열 이외의 다른 2 급 데이터 유형이 있습니까?"일 가능성이 높으며 대답은 "함수를 계산하지 않는 한 실제로는 아닙니다"라고 생각합니다.)

배열을 반환 (또는 할당) 할 수 없다는 사실과 밀접하게 관련되어있는 것은 배열 유형의 값도 없다는 것입니다. 배열 유형의 객체 (변수)가 있지만 1의 값을 가져 오려고 할 때마다 배열의 첫 번째 요소에 대한 포인터를 얻습니다. [각주 :보다 공식적으로 배열 유형의 rvalue 는 없지만 배열 유형의 객체는 할당 할 수 없지만 lvalue 로 생각할 수 있습니다 .]

따라서 배열에 할당 할 수 없다는 사실을 제외 하고 는 배열에 할당 할 값을 생성 할 수도 없습니다. 당신이 말하는 경우

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

마치 당신이 쓴 것처럼

a = &b[0];

그래서 우리는 오른쪽에 포인터가 있고 왼쪽에 배열이 있고 배열이 어떻게 든 할당 가능하더라도 엄청난 유형 불일치가 있습니다. 유사하게 (귀하의 예에서) 우리가 쓰려고하면

a = f();

과 기능의 정의 내부 어딘가에 f()우리가

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

마치 마지막 줄이 말한 것처럼

return &ret[0];

그리고 다시, 우리는 반환하고 할당 할 배열 값이 없습니다 a.

(함수 호출 예제에서 우리는 retC로 리턴하려고 시도하는 위험한 로컬 배열 이라는 매우 중요한 문제도 있습니다 . 나중에이 점에 대해 자세히 설명합니다.)

이제 질문의 일부는 아마도 "왜 이런 식입니까?"이고 "배열 을 할당 할 수 없다면 배열을 포함하는 구조를 할당 수 있습니까?"입니다.

다음은 내 해석과 내 의견이지만, Dennis Ritchie가 The Development of the C Language 논문에서 설명하는 것과 일치 합니다.

배열의 할당 불가능 성은 다음 세 가지 사실에서 발생합니다.

  1. C는 기계 하드웨어와 구문 상 및 의미 상 가깝도록 의도되었습니다. C의 기본 연산은 하나 또는 소수의 프로세서 사이클을 사용하는 하나 또는 소수의 기계 명령어로 컴파일되어야합니다.

  2. 배열은 특히 포인터와 관련된 방식에서 항상 특별했습니다. 이 특별한 관계는 C의 이전 언어 B의 배열 처리에서 발전했으며 그 영향을 많이 받았습니다.

  3. 구조는 처음에 C가 아니 었습니다.

포인트 2로 인해 배열을 할당하는 것이 불가능하고, 포인트 1로 인해 어쨌거나 가능하지 않아야합니다. 단일 할당 연산자 =가 N 천 개의 요소 배열을 복사하는 데 N 천 사이클이 걸릴 수있는 코드로 확장되어서는 안되기 때문 입니다.

그리고 우리는 3 번 지점에 도달합니다. 이것은 결국 모순을 형성합니다.

C가 구조를 얻었을 때, 그것들은 할당하거나 반환 할 수 없다는 점에서 처음에는 완전히 일류가 아니 었습니다. 하지만 그럴 수없는 이유는 첫 번째 컴파일러가 처음에는 코드를 생성 할만큼 충분히 똑똑하지 않았기 때문입니다. 배열의 경우처럼 구문 론적 또는 의미 론적 장애물이 없었습니다.

그리고 그 목표는 구조물이 일류가되는 것이었고, 이것은 K & R의 첫 번째 버전이 인쇄 될시기 인 비교적 초기에 달성되었습니다.

그러나 중요한 질문은 남아 있습니다. 만약 기본 연산이 적은 수의 명령어와 사이클로 컴파일되어야한다면 왜 그 인수가 구조 할당을 허용하지 않는 것일까 요? 그리고 대답은 네, 그것은 모순입니다.

나는 생각이 다음과 같았다고 생각한다 (내가 생각하기에 더 많은 추측 임) : "1 급 타입은 좋다, 2 급 타입은 불행하다. 우리는 배열에 대해 2 급 상태에 갇혀 있지만 우리는 할 수있다. 구조를 사용하면 더 잘할 수 있습니다. 비용이 많이 들지 않는 코드 규칙은 실제로 규칙이 아니라 지침에 가깝습니다. 배열은 종종 크지 만 구조는 일반적으로 작거나 수십 또는 수백 바이트이므로 할당 할 수 없습니다. 보통 너무 비싸요. "

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). For now I'm going to hypothesize a new operator or pseudofunction called arrayval().

We could extend C to allow array assignment by doing the following:

  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. As mentioned, for now I'm going to posit the syntax arrayval( ... ).

[Side note: Today we have a "key definition" 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 or & operator, or is a string literal initializer for a character array. Under the hypothetical modifications I'm discussing here, there would be four exceptions, with the operand of an arrayval operator being added to the list.]

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


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.


For arrays to be first-class objects, you would expect at least to be able to assign them. But that requires knowledge of the size, and the C type system is not powerful enough to attach sizes to any types. C++ could do it, but doesn't due to legacy concerns—it has references to arrays of particular size (typedef char (&some_chars)[32]), but plain arrays are still implicitly converted to pointers as in C. C++ has std::array instead, which is basically the aforementioned array-within-struct plus some syntactic sugar.


I'm afraid in my mind it's not so much a debate of first or second class objects, it's a religious discussion of good practice and applicable practice for deep embedded applications.

Returning a structure either means a root structure being changed by stealth in the depths of the call sequence, or a duplication of data and the passing of large chunks of duplicated data. The main applications of C are still largely concentrated around the deep embedded applications. In these domains you have small processors that don't need to be passing large blocks of data. You also have engineering practice that necessitates the need to be able to operate without dynamic RAM allocation, and with minimal stack and often no heap. It could be argued the return of the structure is the same as modification via pointer, but abstracted in syntax... I'm afraid I'd argue that's not in the C philosophy of "what you see is what you get" in the same way a pointer to a type is.

Personally, I would argue you have found a loop hole, whether standard approved or not. C is designed in such a way that allocation is explicit. You pass as a matter of good practice address bus sized objects, normally in an aspirational one cycle, referring to memory that has been allocated explicitly at a controlled time within the developers ken. This makes sense in terms of code efficiency, cycle efficiency, and offers the most control and clarity of purpose. I'm afraid, in code inspection I'd throw out a function returning a structure as bad practice. C does not enforce many rules, it's a language for professional engineers in many ways as it relies upon the user enforcing their own discipline. Just because you can, doesn't mean you should... It does offer some pretty bullet proof ways to handle data of very complex size and type utilising compile time rigour and minimising the dynamic variations of footprint and at runtime.

참고URL : https://stackoverflow.com/questions/50808782/what-does-impossibility-to-return-arrays-actually-mean-in-c

반응형