Soll ich C-Strukturen über einen Parameter oder über einen Rückgabewert initialisieren? [geschlossen]


33

Die Firma, bei der ich arbeite, initialisiert alle ihre Datenstrukturen durch eine Initialisierungsfunktion wie folgt:

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
InitializeFoo(Foo* const foo){
   foo->a = x; //derived here based on other data
   foo->b = y; //derived here based on other data
   foo->c = z; //derived here based on other data
}

//initializing the structure  
Foo foo;
InitializeFoo(&foo);

Ich habe einige Versuche unternommen, meine Strukturen wie folgt zu initialisieren:

//the structure
typedef struct{
  int a,b,c;  
} Foo;

//the initialize function
Foo ConstructFoo(int a, int b, int c){
   Foo foo;
   foo.a = a; //part of parameter input (inputs derived outside of function)
   foo.b = b; //part of parameter input (inputs derived outside of function)
   foo.c = c; //part of parameter input (inputs derived outside of function)
   return foo;
}

//initialize (or construct) the structure
Foo foo = ConstructFoo(x,y,z);

Gibt es einen Vorteil gegenüber dem anderen?
Welches sollte ich tun und wie würde ich es als bessere Praxis rechtfertigen?


4
@gnat Dies ist eine explizite Frage zur Strukturinitialisierung. Dieser Thread verkörpert einige der Gründe, die ich für diese spezielle Designentscheidung gerne gesehen hätte.
Trevor Hickey

2
@Jefffrey Wir sind in C, also können wir eigentlich keine Methoden haben. Es ist auch nicht immer eine direkte Menge von Werten. Manchmal ist das Initialisieren einer Struktur (irgendwie) das Abrufen von Werten und das Ausführen einer Logik zum Initialisieren der Struktur.
Trevor Hickey

1
@JacquesB Ich habe "Jede Komponente, die Sie erstellen, ist anders als die anderen. Es gibt eine Initialize () - Funktion, die an anderer Stelle für die Struktur verwendet wird. Technisch gesehen ist es irreführend, sie als Konstruktor zu bezeichnen."
Trevor Hickey

1
@ TrevorHickey InitializeFoo()ist ein Konstruktor. Der einzige Unterschied zu einem C ++ - Konstruktor besteht darin, dass der thisZeiger explizit und nicht implizit übergeben wird. Der kompilierte Code von InitializeFoo()und ein entsprechendes C ++ Foo::Foo()ist genau gleich.
cmaster

2
Bessere Option: Verwenden Sie C nicht mehr über C ++. Autowin.
Thomas Eding

Antworten:


25

In der 2. Annäherung haben Sie nie ein halb initialisiertes Foo. Es scheint vernünftiger und naheliegender, die gesamte Konstruktion an einem Ort zu platzieren.

Aber ... der 1. Weg ist nicht so schlecht und wird oft in vielen Bereichen verwendet (es wird sogar diskutiert, wie man Abhängigkeiten am besten einfügt, entweder Eigenschaftsinjektion wie der 1. Weg oder Konstruktorinjektion wie der 2.). . Weder ist falsch.

Wenn also keines von beiden falsch ist und der Rest des Unternehmens Ansatz Nr. 1 verwendet, sollten Sie sich an die vorhandene Codebasis anpassen und nicht versuchen, sie durch die Einführung eines neuen Musters durcheinander zu bringen. Dies ist wirklich der wichtigste Faktor beim Spielen hier, spiele gut mit deinen neuen Freunden und versuche nicht, diese besondere Schneeflocke zu sein, die die Dinge anders macht.


Ok, scheint vernünftig. Ich hatte den Eindruck, dass das Initialisieren eines Objekts, ohne zu sehen, welche Art von Eingabe es initialisiert, zu Verwirrung führen würde. Ich habe versucht, dem Konzept der Dateneingabe / -ausgabe zu folgen, um vorhersagbaren und testbaren Code zu erzeugen. Andernfalls schien sich die Kopplung zu verstärken, da die Quelldatei meiner Struktur zusätzliche Abhängigkeiten benötigte, um die Initialisierung durchzuführen. Sie haben jedoch Recht, dass ich das Boot nicht rocken möchte, es sei denn, ein Weg wird einem anderen vorgezogen.
Trevor Hickey

4
@ TrevorHickey: Eigentlich würde ich sagen, es gibt zwei wesentliche Unterschiede zwischen den Beispielen, die Sie angeben: (1) In der einen wird der Funktion ein Zeiger auf die zu initialisierende Struktur übergeben, und in der anderen gibt sie eine initialisierte Struktur zurück. (2) In einem werden die Initialisierungsparameter an die Funktion übergeben und in dem anderen sind sie implizit. Sie scheinen mehr nach (2) zu fragen, aber die Antworten konzentrieren sich hier auf (1). Sie möchten vielleicht klarstellen, dass - ich vermute, dass die meisten Leute einen Hybrid aus beiden mit expliziten Parametern und einem Zeiger empfehlen würden:void SetupFoo(Foo *out, int a, int b, int c)
psmears

1
Wie würde der erste Ansatz zu einer "halb initialisierten" FooStruktur führen? Der erste Ansatz führt auch die gesamte Initialisierung an einem Ort durch. (Oder erwägen Sie eine nicht initialisierte FooStruktur als "halb initialisiert"?)
Jamesdlin

1
@jamesdlin in Fällen, in denen das Foo erstellt wird und das InitialiseFoo versehentlich übersehen wird. Es war nur eine Redewendung, um die 2-Phasen-Initialisierung zu beschreiben, ohne eine ausführliche Beschreibung einzugeben. Ich dachte, erfahrene Entwickler würden das verstehen.
gbjbaanb

22

Beide Ansätze bündeln den Initialisierungscode in einem einzigen Funktionsaufruf. So weit, ist es gut.

Beim zweiten Ansatz gibt es jedoch zwei Probleme:

  1. Der zweite Befehl erstellt das resultierende Objekt nicht tatsächlich, sondern initialisiert ein anderes Objekt auf dem Stapel, das dann in das endgültige Objekt kopiert wird. Aus diesem Grund würde ich den zweiten Ansatz als etwas minderwertig betrachten. Der Push-Back, den Sie erhalten haben, ist wahrscheinlich auf diese externe Kopie zurückzuführen.

    Das ist noch schlimmer , wenn Sie eine Klasse ableiten Derivedvon Foo(structs werden für Objektorientierung in C weitgehend verwendet): Mit dem zweiten Ansatz die Funktion ConstructDerived()nennen würde ConstructFoo(), kopieren Sie das resultierende temporäre FooObjekt über in den Superschlitz eines DerivedObjekts; Beenden Sie die Initialisierung des DerivedObjekts. nur um das resultierende Objekt bei der Rückkehr erneut kopieren zu lassen. Fügen Sie eine dritte Schicht hinzu, und die ganze Sache wird völlig lächerlich.

  2. Beim zweiten Ansatz haben die ConstructClass()Funktionen keinen Zugriff auf die Adresse des Objekts, das gerade erstellt wird. Dies macht es unmöglich, Objekte während der Konstruktion zu verknüpfen, da dies erforderlich ist, wenn sich ein Objekt für einen Rückruf bei einem anderen Objekt registrieren muss.


Schließlich sind nicht alle structsKlassen vollwertig. Einige structsbündeln effektiv nur eine Reihe von Variablen, ohne interne Einschränkungen für die Werte dieser Variablen. typedef struct Point { int x, y; } Point;wäre ein gutes Beispiel dafür. Für diese scheint die Verwendung einer Initialisierungsfunktion übertrieben. In diesen Fällen kann die zusammengesetzte Literal-Syntax nützlich sein (es ist C99):

Point = { .x = 7, .y = 9 };

oder

Point foo(...) {
    //other stuff

    return (Point){ .x = n, .y = n*n };
}

5
Ich hätte nicht gedacht, dass Kopien aufgrund von en.wikipedia.org/wiki/Copy_elision
Trevor Hickey

5
Dass der Compiler die Kopie entfernen darf, mindert nicht die Tatsache, dass Sie die Kopie aufgeschrieben haben. In C wird das Schreiben überflüssiger Operationen und das Verlassen auf den Compiler, um sie zu beheben, als fehlerhaft angesehen. Dies unterscheidet sich von C ++, wo die Leute stolz sind, wenn sie nachweisen können, dass der Compiler theoretisch alle Überreste ihrer verschachtelten Vorlagen entfernen kann. In C wird versucht, genau den Code zu schreiben, den der Computer ausführen soll. Wie auch immer, der Punkt über die unzugänglichen Adressen bleibt bestehen, Copy Elision kann Ihnen da nicht weiterhelfen.
cmaster

3
Jeder, der einen Compiler verwendet, sollte erwarten, dass der von ihm geschriebene Code vom Compiler transformiert wird. Sofern sie keinen Hardware-C-Interpreter ausführen, ist der von ihnen geschriebene Code nicht der Code, den sie ausführen, selbst wenn es leicht fällt, etwas anderes zu glauben. Wenn sie ihren Compiler verstehen, verstehen sie elision und es ist nichts anderes, als int x = 3;den String nicht xin der Binärdatei zu speichern . Die Adresse und die Vererbungspunkte sind gut; Das angenommene Scheitern der Wahl ist töricht.
Yakk

@Yakk: Historisch gesehen wurde C erfunden, um als eine Form der Assemblersprache für die Systemprogrammierung zu dienen. In den Jahren seitdem ist seine Identität zunehmend trübe geworden. Einige Leute möchten, dass es sich um eine optimierte Anwendungssprache handelt. Da es jedoch keine bessere Form der Assembler-Sprache auf hoher Ebene gibt, wird C weiterhin benötigt, um diese letztere Rolle zu übernehmen. Ich sehe nichts Falsches an der Vorstellung, dass ein gut geschriebener Programmcode sich zumindest anständig verhalten sollte, selbst wenn er mit minimalen Optimierungen kompiliert wurde.
Supercat

@Yakk: Mit Richtlinien, zum Beispiel, die einen Compiler sagen würden , sowie ein Mittel zur block Kopieren eines anderer Typ als „Die folgenden Variablen sicher in Register während der folgenden Strecke von Code gehalten werden können“ unsigned charOptimierungen erlauben würde , für die die Eine strenge Aliasing-Regel würde nicht ausreichen und gleichzeitig die Erwartungen der Programmierer klarer machen.
Supercat

1

Abhängig vom Inhalt der Struktur und dem verwendeten Compiler kann jeder Ansatz schneller sein. Ein typisches Muster ist, dass Strukturen, die bestimmte Kriterien erfüllen, in Registern zurückgegeben werden können. Für Funktionen, die andere Strukturtypen zurückgeben, muss der Aufrufer irgendwo (normalerweise auf dem Stapel) Platz für die temporäre Struktur reservieren und ihre Adresse als "versteckten" Parameter übergeben. In Fällen, in denen die Rückgabe einer Funktion direkt in einer lokalen Variablen gespeichert wird, deren Adresse von keinem externen Code gespeichert wird, können einige Compiler die Adresse dieser Variablen möglicherweise direkt übergeben.

Wenn ein Strukturtyp die Anforderungen einer bestimmten Implementierung erfüllt, in Registern zurückgegeben zu werden (z. B. wenn er entweder nicht größer als ein Maschinenwort ist oder genau zwei Maschinenwörter füllt), die eine Funktionsrückgabe aufweisen, kann die Struktur insbesondere schneller als die Übergabe der Adresse einer Struktur sein Da das Aussetzen der Adresse einer Variablen an externen Code, der möglicherweise eine Kopie davon enthält, einige nützliche Optimierungen ausschließen könnte. Wenn ein Typ diese Anforderungen nicht erfüllt, ähnelt der generierte Code für eine Funktion, die eine Struktur zurückgibt, dem für eine Funktion, die einen Zielzeiger akzeptiert. Der aufrufende Code wäre wahrscheinlich schneller für das Formular, das einen Zeiger annimmt, aber dieses Formular würde einige Optimierungsmöglichkeiten verlieren.

Es ist zu schade, dass C kein Mittel bietet, um zu behaupten, dass es einer Funktion untersagt ist, eine Kopie eines übergebenen Zeigers zu behalten (Semantik ähnlich einer C ++ - Referenz), da das Übergeben eines solchen eingeschränkten Zeigers die direkten Leistungsvorteile des Übergebens mit sich bringen würde ein Zeiger auf ein bereits vorhandenes Objekt, der jedoch gleichzeitig die semantischen Kosten vermeidet, die entstehen, wenn ein Compiler die Adresse einer Variablen als "verfügbar" betrachtet.


3
Zu Ihrem letzten Punkt: In C ++ gibt es nichts, was eine Funktion daran hindert, eine Kopie eines Zeigers zu behalten, der als Referenz übergeben wurde. Die Funktion kann einfach die Adresse des Objekts übernehmen. Es ist auch frei, die Referenz zu verwenden, um ein anderes Objekt zu konstruieren, das diese Referenz enthält (kein nackter Zeiger erstellt). Trotzdem kann entweder die Zeigerkopie oder die Referenz im Objekt das Objekt überleben, auf das sie zeigen, wodurch ein baumelnder Zeiger / Referenz erstellt wird. Der Punkt zur Referenzsicherheit ist also ziemlich stumm.
CMASTER

@cmaster: Auf Plattformen, die Strukturen zurückgeben, indem sie einen Zeiger auf den temporären Speicher übergeben, bieten C-Compiler den aufgerufenen Funktionen keine Möglichkeit, auf die Adresse dieses Speichers zuzugreifen. In C ++ ist es möglich, die Adresse einer Variablen abzurufen, die als Referenz übergeben wurde, aber es sei denn, der Aufrufer garantiert die Lebensdauer des übergebenen Elements (in diesem Fall hätte er normalerweise einen Zeiger übergeben).
Supercat

1

Ein Argument für den "output-parameter" -Stil ist, dass die Funktion einen Fehlercode zurückgeben kann.

struct MyStruct {
    int x;
    char *y;
    // ...
};

int MyStruct_init(struct MyStruct *out) {
    // ...
    char *c = malloc(n);
    if (!c) {
        return -1;
    }
    out->y = c;
    return 0;  // Success!
}

In Anbetracht einiger verwandter Strukturen kann es sich lohnen, aus Gründen der Konsistenz den Out-Parameter-Stil zu verwenden, wenn die Initialisierung für eine dieser Strukturen fehlschlägt.


1
Obwohl man einfach setzen könnte errno.
Deduplizierer

0

Ich gehe davon aus, dass Ihr Fokus auf der Initialisierung über Ausgabeparameter gegenüber der Initialisierung über Rückgabe liegt, nicht auf der Diskrepanz bei der Bereitstellung von Konstruktionsargumenten.

Beachten Sie, dass der erste Ansatz Foomöglicherweise eine Undurchsichtigkeit zulässt (obwohl dies nicht der Art entspricht, wie Sie ihn derzeit verwenden). Dies ist normalerweise für die langfristige Wartbarkeit wünschenswert. Sie können beispielsweise eine Funktion in Betracht ziehen, die eine undurchsichtige FooStruktur zuweist, ohne sie zu initialisieren. Oder Sie müssen eine FooStruktur neu initialisieren , die zuvor mit anderen Werten initialisiert wurde.


Downvoter, willst du das erklären? Ist etwas, was ich gesagt habe, sachlich falsch?
Jamesdlin
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.