Was bedeutet die Unmöglichkeit, Arrays zurückzugeben, in C?

72

Ich versuche nicht, die übliche Frage zu wiederholen, dass C keine Arrays zurückgeben kann, sondern sich etwas tiefer damit befasst.

Wir können das nicht machen:

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

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

Aber wir können tun:

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

Also habe ich den von gcc -S generierten ASM-Code überflogen und scheint mit dem Stack zu arbeiten, der -x(%rbp)wie bei jeder anderen C-Funktionsrückgabe adressiert .

Was ist mit der direkten Rückgabe von Arrays? Ich meine, nicht in Bezug auf Optimierung oder Rechenkomplexität, sondern in Bezug auf die tatsächliche Fähigkeit, dies ohne die Strukturschicht zu tun.

Zusätzliche Daten: Ich verwende Linux und gcc auf einem x64 Intel.

Dario Rodriguez
quelle
18
Vor langer Zeit, ungefähr zu der Zeit, als die erste Ausgabe von K & R veröffentlicht wurde (1978), konnte man Strukturen nicht von Funktionen zurückgeben oder an Funktionen übergeben. Sie mussten Zeiger verwenden. Sie können immer noch keine einfachen Arrays zuweisen, obwohl Sie Strukturen zuweisen können, die Arrays enthalten. Es ist meistens die Art und Weise, wie C entworfen wird, und niemand hat darum gekämpft, die notwendigen Änderungen zu standardisieren.
Jonathan Leffler
17
In Abschnitt 6.2 von K & R 1st Edn heißt es in der Tat: Es gibt eine Reihe von Einschränkungen für C-Strukturen. Die wesentlichen Regeln sind, dass die einzigen Vorgänge, die Sie für eine Struktur ausführen können, darin bestehen, ihre Adresse mit &einem ihrer Mitglieder zu übernehmen und auf dieses zuzugreifen. Dies bedeutet, dass Strukturen möglicherweise nicht als Einheit zugewiesen oder kopiert werden und dass sie nicht an Funktionen übergeben oder von diesen zurückgegeben werden können. (Diese Einschränkungen werden in den kommenden Versionen aufgehoben.) … Und zum Glück wurden sie entfernt. Die Einschränkung der Array-Zuweisung hat sich jedoch noch nicht geändert.
Jonathan Leffler
25
Beachten Sie, dass dies char f[8](void)ein Array von Funktionen ist. Eine Funktion, die ein Array zurückgibt, sieht aus wie char f(void)[8].
Melpomene
4
Wenn Sie die Größe Ihrer Struktur auf mehr als 16 Byte erhöhen, werden Sie feststellen, dass der Compiler tatsächlich ein zusätzliches verstecktes Argument übergibt, das die Adresse der zurückzugebenden Struktur ist.
Café
2
Mögliches Duplikat von Cs Abneigung gegen Arrays
Bo Persson

Antworten:

101

Zunächst einmal können Sie ein Array in eine Struktur einkapseln und dann mit dieser Struktur alles tun, was Sie wollen (zuweisen, von einer Funktion zurückgeben usw.).

Zweitens hat der Compiler, wie Sie festgestellt haben, kaum Schwierigkeiten, Code auszugeben, um Strukturen zurückzugeben (oder zuzuweisen). Das ist also auch nicht der Grund, warum Sie keine Arrays zurückgeben können.

Der grundlegende Grund, warum Sie dies nicht tun können, ist, dass Arrays in C Datenstrukturen zweiter Klasse sind . Alle anderen Datenstrukturen sind erstklassig. Was sind die Definitionen von "erstklassig" und "zweitklassig" in diesem Sinne? Einfach, dass Typen zweiter Klasse nicht zugewiesen werden können.

(Ihre nächste Frage lautet wahrscheinlich: "Gibt es außer Arrays noch andere Datentypen zweiter Klasse?", Und ich denke, die Antwort lautet "Nicht wirklich, es sei denn, Sie zählen Funktionen".)

Eng verbunden mit der Tatsache, dass Sie keine Arrays zurückgeben (oder zuweisen) können, gibt es auch keine Werte vom Array-Typ. Es gibt Objekte (Variablen) vom Array-Typ, aber wenn Sie versuchen, den Wert eins anzunehmen, erhalten Sie einen Zeiger auf das erste Element des Arrays. [Fußnote: Formal gibt es keine Werte vom Array-Typ, obwohl ein Objekt vom Array-Typ als l-Wert betrachtet werden kann , wenn auch nicht zuweisbar.]

Also, ganz abgesehen von der Tatsache , dass Sie nicht zuordnen können zu einem Array, können Sie auch keinen Wert erzeugen , um einen Array zuweisen. Wenn du sagst

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

Es ist, als hättest du geschrieben

a = &b[0];

Wir haben also rechts einen Zeiger und links ein Array, und wir hätten eine massive Typinkongruenz, selbst wenn Arrays irgendwie zuweisbar wären. Ähnlich (aus Ihrem Beispiel), wenn wir versuchen zu schreiben

a = f();

und irgendwo in der Definition der Funktion, die f()wir haben

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

Es ist, als ob diese letzte Zeile sagte

return &ret[0];

und wieder haben wir keinen Array-Wert, den wir zurückgeben und zuweisen können a, sondern nur einen Zeiger.

(Im Beispiel für einen Funktionsaufruf haben wir auch das sehr wichtige Problem, dass retes sich um ein lokales Array handelt, das gefährlich ist, wenn Sie versuchen, in C zurückzukehren. Mehr dazu später.)

Ein Teil Ihrer Frage lautet wahrscheinlich "Warum ist das so?" Und auch "Wenn Sie keine Arrays zuweisen können, warum können Sie Strukturen zuweisen, die Arrays enthalten?".

Was folgt, ist meine Interpretation und meine Meinung, aber es stimmt mit dem überein, was Dennis Ritchie in der Arbeit Die Entwicklung der C-Sprache beschreibt .

Die Nichtzuweisbarkeit von Arrays ergibt sich aus drei Tatsachen:

  1. C soll syntaktisch und semantisch nahe an der Maschinenhardware liegen. Eine elementare Operation in C sollte bis zu einer oder wenigen Maschinenanweisungen kompiliert werden, die einen oder mehrere Prozessorzyklen benötigen.

  2. Arrays waren schon immer etwas Besonderes, insbesondere in Bezug auf Zeiger. Diese besondere Beziehung entwickelte sich aus der Behandlung von Arrays in der Vorgängersprache B von C und wurde stark von dieser beeinflusst.

  3. Strukturen waren anfangs nicht in C.

Aufgrund von Punkt 2 ist es unmöglich, Arrays zuzuweisen, und aufgrund von Punkt 1 sollte dies sowieso nicht möglich sein, da ein einzelner Zuweisungsoperator =nicht zu Code erweitert werden sollte, der möglicherweise N Tausend Zyklen zum Kopieren eines N Tausend-Elemente-Arrays benötigt.

Und dann kommen wir zu Punkt 3, der wirklich einen Widerspruch bildet.

Wenn C Strukturen bekam, waren sie anfangs auch nicht ganz erstklassig, da man sie nicht zuweisen oder zurückgeben konnte. Der Grund, warum Sie dies nicht konnten, war einfach, dass der erste Compiler zunächst nicht klug genug war, um den Code zu generieren. Es gab keine syntaktische oder semantische Straßensperre wie bei Arrays.

Das Ziel war von Anfang an, dass die Strukturen erstklassig sind, und dies wurde relativ früh erreicht, kurz zu der Zeit, als die erste Ausgabe von K & R gedruckt werden sollte.

Die große Frage bleibt jedoch: Wenn eine Elementaroperation auf eine kleine Anzahl von Anweisungen und Zyklen kompiliert werden soll, warum lässt dieses Argument die Strukturzuweisung nicht zu? Und die Antwort lautet: Ja, das ist ein Widerspruch.

Ich glaube (obwohl dies mehr Spekulationen meinerseits sind), dass das Denken ungefähr so ​​war: "Erstklassige Typen sind gut, zweitklassige Typen sind unglücklich. Wir haben den Status zweiter Klasse für Arrays, aber wir können Machen Sie es besser mit Strukturen. Die No-Teure-Code-Regel ist nicht wirklich eine Regel, sondern eher eine Richtlinie. Arrays sind oft groß, aber Strukturen sind normalerweise klein, zehn oder Hunderte von Bytes, sodass sie nicht zugewiesen werden normalerweise zu teuer sein. "

Eine konsequente Anwendung der No-Teure-Code-Regel blieb also auf der Strecke. C war sowieso nie perfekt regelmäßig oder konsistent. (Auch die überwiegende Mehrheit der erfolgreichen Sprachen ist weder menschlich noch künstlich.)

Nach alledem kann es sich lohnen zu fragen: "Was wäre, wenn C das Zuweisen und Zurückgeben von Arrays unterstützen würde? Wie könnte das funktionieren?" Und die Antwort muss eine Möglichkeit beinhalten, das Standardverhalten von Arrays in Ausdrücken auszuschalten, nämlich dass sie dazu neigen, sich in Zeiger auf ihr erstes Element zu verwandeln.

Irgendwann in den 90er Jahren, IIRC, gab es einen ziemlich gut durchdachten Vorschlag, genau dies zu tun. Ich denke, es ging darum, einen Array-Ausdruck in [ ]oder [[ ]]oder so einzuschließen. Heute kann ich anscheinend keine Erwähnung dieses Vorschlags finden (obwohl ich dankbar wäre, wenn jemand eine Referenz liefern könnte). Ich glaube jedenfalls, wir könnten C erweitern, um die Array-Zuweisung zu ermöglichen, indem wir die folgenden drei Schritte ausführen:

  1. Entfernen Sie das Verbot der Verwendung eines Arrays auf der linken Seite eines Zuweisungsoperators.

  2. Entfernen Sie das Verbot, Funktionen mit Array-Werten zu deklarieren. Zurück zur ursprünglichen Frage, machen Sie char f(void)[8] { ... }legal.

  3. (Dies ist das große Problem.) Sie können ein Array in einem Ausdruck erwähnen und erhalten einen wahren, zuweisbaren Wert (einen r- Wert ) vom Array-Typ. Aus Gründen der Argumentation werde ich einen neuen Operator oder eine neue Pseudofunktion mit dem Namen setzen arrayval( ... ).

[Randnotiz: Heute haben wir eine " Schlüsseldefinition " der Array / Zeiger-Korrespondenz, nämlich:

Ein Verweis auf ein Objekt vom Typ Array, das in einem Ausdruck erscheint, zerfällt (mit drei Ausnahmen) in einen Zeiger auf sein erstes Element.

Die drei Ausnahmen sind, wenn das Array der Operand eines sizeofOperators oder eines &Operators oder ein String-Literal-Initialisierer für ein Zeichenarray ist. Unter den hypothetischen Änderungen, die ich hier diskutiere, gibt es vier Ausnahmen, wobei der Operand eines arrayvalOperators zur Liste hinzugefügt wird.]

Wie auch immer, mit diesen Änderungen könnten wir Dinge wie schreiben

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

(Natürlich müssten wir auch entscheiden, was zu tun ist, wenn aund bnicht gleich groß.)

Angesichts des Funktionsprototyps

char f(void)[8];

wir könnten es auch tun

a = f();

Schauen wir uns die fhypothetische Definition an. Wir könnten so etwas haben

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

Beachten Sie, dass dies (mit Ausnahme des hypothetischen neuen arrayval()Operators) genau das ist, was Dario Rodriguez ursprünglich gepostet hat. Beachten Sie auch, dass dies in der hypothetischen Welt, in der die Zuweisung von Arrays legal war und so etwas arrayval()existierte, tatsächlich funktionieren würde! Insbesondere würde es nicht das Problem haben, einen bald ungültigen Zeiger auf das lokale Array zurückzugeben ret. Es würde eine Kopie des Arrays zurückgeben, so dass es überhaupt kein Problem geben würde - es wäre fast vollkommen analog zu dem offensichtlich legalen

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

Zurück zur Nebenfrage "Gibt es noch andere Typen zweiter Klasse?": Ich denke, es ist mehr als ein Zufall, dass Funktionen wie Arrays automatisch ihre Adresse erhalten, wenn sie nicht als sie selbst verwendet werden (d. H. als Funktionen oder Arrays), und dass es ebenfalls keine Werte vom Funktionstyp gibt. Aber dies ist meistens eine müßige Überlegung, weil ich nicht glaube, dass ich jemals Funktionen gehört habe, die in C als "Typen zweiter Klasse" bezeichnet werden. (Vielleicht haben sie das und ich habe es vergessen.)


Fußnote: Da der Compiler ist bereit zu assign Strukturen, und in der Regel weiß , wie effizienten Code zu emittieren , für so tun, es verwendete ein etwas beliebter Trick zu kooptieren zu den Compiler des Struktur-Kopier - Maschinen , um beliebiges Bytes von Punkt a zu kopieren zu Punkt b. Insbesondere könnten Sie dieses etwas seltsam aussehende Makro schreiben:

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

das verhielt sich mehr oder weniger genau wie eine optimierte Inline-Version von memcpy(). (Tatsächlich kompiliert und funktioniert dieser Trick auch heute noch unter modernen Compilern.)

Steve Summit
quelle
4
@ JohnBollinger Es gibt Werte vom Array-Typ, aber keine Werte.
n. 'Pronomen' m.
2
@ JohnBollinger Der C-Standard verwendet den Begriff "Wert", um zu bezeichnen, was als "rWert" bezeichnet werden könnte
MM
2
@nm: Auch das stimmt nicht - beim zweiten Beispiel in der Frage f().arrhandelt es sich um ein rvalue-Array.
Café
4
+1, um effektiv zu erklären, warum es keine Möglichkeit gibt, die Sprache mit dieser Funktion zu erweitern, ohne im Widerspruch zu bestehenden Einschränkungen zu stehen.
R .. GitHub STOP HELPING ICE
2
@ JohnBollinger Es ist zwar eine kleine Vereinfachung, aber eine, die ich nützlich finde. Was ich genauer gemeint habe, wie nm und MM (seid ihr zwei verwandt? :-)) betonten und ich jetzt klargestellt habe, ist, dass es keine Werte vom Array-Typ gibt. (Und natürlich ist es eine ziemlich schwierige Frage zu sagen, ob Arrys Werte sein können.)
Steve Summit
21

Was ist mit der direkten Rückgabe von Arrays? Ich meine, nicht in Bezug auf Optimierung oder Rechenkomplexität, sondern in Bezug auf die tatsächliche Fähigkeit, dies ohne die Strukturschicht zu tun.

Es hat nichts mit der Fähigkeit an sich zu tun . Andere Sprachen bieten die Möglichkeit, Arrays zurückzugeben, und Sie wissen bereits, dass Sie in C eine Struktur mit einem Array-Mitglied zurückgeben können. Auf der anderen Seite haben noch andere Sprachen die gleiche Einschränkung wie C und noch mehr. Java kann beispielsweise weder Arrays noch Objekte jeglicher Art von Methoden zurückgeben. Es können nur Grundelemente und Verweise auf Objekte zurückgegeben werden.

Nein, es ist einfach eine Frage des Sprachdesigns. Wie bei den meisten anderen Dingen, die mit Arrays zu tun haben, drehen sich die Entwurfspunkte hier um die Bestimmung von C, dass Ausdrücke vom Array-Typ in fast allen Kontexten automatisch in Zeiger konvertiert werden. Der in einer returnAnweisung angegebene Wert ist keine Ausnahme, sodass C nicht einmal die Rückgabe eines Arrays selbst ausdrücken kann. Eine andere Wahl hätte getroffen werden können, war es aber einfach nicht.

John Bollinger
quelle
3
Nachdem ich einen Vergleich mit Java angestellt habe, stelle ich fest, dass sich das, was Java unter dem Begriff "Objekt" versteht, von dem unterscheidet, was C unter diesem Begriff versteht.
John Bollinger
2
"Ausdrücke vom Array-Typ werden in fast allen Kontexten automatisch in Zeiger konvertiert." Das ist die wahre Antwort, IMHO. Aus einem Compiler-POV ist es schwer herauszufinden, ob Sie sich auf ein Array als Ganzes oder nur als Zeiger auf das erste Element beziehen: Es hätte so etwas wie return (array) boder return bexplizit unterscheiden müssen. Und natürlich kann structs diese Mehrdeutigkeit nicht haben.
Edmz
"Eine andere Wahl hätte getroffen werden können, aber es war einfach nicht so." - Für mich ist das alles.
Dario Rodriguez
3

Damit Arrays erstklassige Objekte sind, müssen Sie sie zumindest zuweisen können. Dies erfordert jedoch Kenntnisse über die Größe, und das C-Typ-System ist nicht leistungsfähig genug, um Größen an Typen anzuhängen. C ++ könnte dies tun, jedoch nicht aufgrund älterer Bedenken - es enthält Verweise auf Arrays bestimmter Größe ( typedef char (&some_chars)[32]), aber einfache Arrays werden immer noch implizit in Zeiger konvertiert, wie in C. C ++ hat stattdessen std :: array, was im Grunde das ist oben genanntes Array innerhalb der Struktur plus etwas syntaktischen Zucker.

Roman Odaisky
quelle
3
Wenn das C-Typ-System nicht leistungsfähig genug wäre, um Größen an Typen anzuhängen, wie könnte der sizeofBediener dann arbeiten? Aber es funktioniert auch mit Array-Typen.
John Bollinger
@JohnBollinger sizeof wird zur Kompilierungszeit berechnet, während die Arraygröße dem Compiler bekannt ist.
Simon B
1
Da C eine kompilierte Sprache ist, ist die Kompilierungszeit die einzige Zeit, in der Typen wirklich ein Problem jeglicher Art sind. Es wird kein Laufzeitsystem geben. Dann fügt das C-Typ-System tatsächlich Größen zu Typen hinzu.
Dario Rodriguez
1
sizeoffunktioniert gut, @RomanOdaisky. Es ist die Bedeutung der Parameterliste dieser Funktion, die Sie anscheinend überraschend finden. Gemäß dem Standard wird der Parameter ungeachtet der Größe in Klammern als Array und nicht als Array xdeklariert char *. Dies steht völlig im Einklang mit der Tatsache, dass es überhaupt keine Möglichkeit gibt, ein Array als Funktionsargument zu übergeben (da in der Argumentliste für einen Funktionsaufruf wie an den meisten anderen Stellen Ausdrücke mit Array-Typ in Zeiger konvertiert werden). .
John Bollinger
1
Ja, @Gaius, du bist richtig. Sie haben ein weiteres Gegenbeispiel zu Romans Behauptung vorgelegt, dass "das C-Typ-System nicht leistungsfähig genug ist, um Größen an Typen anzuhängen". Tatsächlich deckt Ihr Gegenbeispiel sogar Größen von Array-Typen ab, wenn Sie Zeiger auf Arrays haben (Beispiel :) int (*p)[3]; printf("%uz\n", sizeof(*p));.
John Bollinger
-1

Ich fürchte, es ist nicht so sehr eine Debatte über Objekte der ersten oder zweiten Klasse, sondern eine religiöse Diskussion über bewährte Praktiken und anwendbare Praktiken für tief eingebettete Anwendungen.

Das Zurückgeben einer Struktur bedeutet entweder, dass eine Stammstruktur durch Verstohlenheit in den Tiefen der Aufrufsequenz geändert wird, oder dass Daten dupliziert werden und große Teile duplizierter Daten übergeben werden. Die Hauptanwendungen von C konzentrieren sich immer noch weitgehend auf die tief eingebetteten Anwendungen. In diesen Domänen haben Sie kleine Prozessoren, die keine großen Datenblöcke übergeben müssen. Sie verfügen auch über technische Erfahrung, die es erforderlich macht, ohne dynamische RAM-Zuweisung und mit minimalem Stapel und häufig ohne Heap arbeiten zu können. Es könnte argumentiert werden, dass die Rückkehr der Struktur die gleiche ist wie die Modifikation über einen Zeiger, aber in der Syntax abstrahiert ... Ich fürchte, ich würde argumentieren, dass dies nicht in der C-Philosophie von "Was Sie sehen, ist was Sie bekommen" in der Genauso wie ein Zeiger auf einen Typ ist.

Persönlich würde ich behaupten, dass Sie eine Lücke gefunden haben, ob standardmäßig genehmigt oder nicht. C ist so konzipiert, dass die Zuordnung explizit ist. Sie übergeben als bewährte Methode Objekte in Busgröße, normalerweise in einem angestrebten Zyklus, und beziehen sich dabei auf Speicher, der explizit zu einem kontrollierten Zeitpunkt innerhalb des Entwickler-Ken zugewiesen wurde. Dies ist in Bezug auf Codeeffizienz und Zykluseffizienz sinnvoll und bietet die größtmögliche Kontrolle und Klarheit des Zwecks. Ich fürchte, bei der Code-Inspektion würde ich eine Funktion verwerfen, die eine Struktur als schlechte Praxis zurückgibt. C setzt nicht viele Regeln durch, es ist in vielerlei Hinsicht eine Sprache für professionelle Ingenieure, da der Benutzer seine eigene Disziplin durchsetzen muss. Nur weil du kannst, heißt das nicht, dass du ...

rauben
quelle
2
Ich denke, das ist ein langer Weg aus dem Thema heraus. Es scheint eher eine Meinung als eine Antwort zu sein. Außerdem wird das Hauptthema verdreht, denn selbst wenn die meisten C-Anwendungen für "Deep Embedded-Anwendungen" bestimmt sind, wird die Diskussion dadurch nicht zu einer "guten Praxis und anwendbaren Praxis für Deep Embedded-Anwendungen". Die Diskussion ist in der Tat ganz anders ausgerichtet.
Dario Rodriguez
Ich werde sehen, dass Ihr "C immer noch weitgehend auf die tief eingebetteten Anwendungen konzentriert ist ... ohne dynamische RAM-Zuweisung und mit minimalem Stapel und oft ohne Heap", und Sie anheben "C ist eine universelle Computerprogrammiersprache, die für den Betrieb verwendet wird Systeme, Bibliotheken, Spiele und andere Hochleistungsarbeiten "- wie aus SOs eigenen Tag-Informationen hervorgeht .
Steve Summit
Sie haben absolut Recht, dass eine Strukturrückgabefunktion durch eine Codeüberprüfung oder einen Styleguide, der streng eingeschränkte, eingebettete Arbeit regelt, mit Nachdruck verweigert wird - und das zu Recht. (Styleguides verbieten Dinge aus verschiedenen Gründen immer.)
Steve Summit
Als ich versuchte, in meiner Antwort zu untersuchen, fielen Strukturzuweisung und Arrayzuweisung auf entgegengesetzte Seiten der Legalitätslinie, da die widersprüchlichen Ziele, keinen "teuren" Code zu haben, im Gegensatz zu einer sauberen, konsistenten und ausdrucksstarken Sprache inkonsistent angewendet wurden .
Steve Summit