it-swarm.com.de

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

Das Unternehmen, bei dem ich arbeite, initialisiert alle Datenstrukturen über 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 Push-Backs erhalten, die versuchen, 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?
Was soll ich tun und wie würde ich es als bessere Praxis rechtfertigen?

34
Trevor Hickey

Beim zweiten Ansatz haben Sie niemals einen halb initialisierten Foo. Die gesamte Konstruktion an einem Ort zu platzieren, scheint ein vernünftigerer und offensichtlicherer Ort zu sein.

Aber ... der erste Weg ist nicht so schlecht und wird oft in vielen Bereichen verwendet (es gibt sogar eine Diskussion über den besten Weg zur Abhängigkeitsinjektion, entweder die Eigenschaftsinjektion wie der erste Weg oder die Konstruktorinjektion wie der zweite). . Beides ist nicht falsch.

Wenn also beides nicht falsch ist und der Rest des Unternehmens Ansatz 1 verwendet, sollten Sie sich in die vorhandene Codebasis einfügen und nicht versuchen, sie durch die Einführung eines neuen Musters durcheinander zu bringen. Dies ist wirklich der wichtigste Faktor hier, spielen Sie Nizza mit Ihren neuen Freunden und versuchen Sie nicht, diese besondere Schneeflocke zu sein, die die Dinge anders macht.

25
gbjbaanb

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. Das zweite Objekt erstellt das resultierende Objekt nicht tatsächlich, sondern initialisiert ein anderes Objekt auf dem Stapel, das dann auf das endgültige Objekt kopiert wird. Aus diesem Grund würde ich den zweiten Ansatz als etwas minderwertig ansehen. Der Push-Back, den Sie erhalten haben, ist wahrscheinlich auf diese fremde Kopie zurückzuführen.

    Dies ist noch schlimmer, wenn Sie eine Klasse Derived von Foo ableiten (Strukturen werden in C weitgehend zur Objektorientierung verwendet): Beim zweiten Ansatz würde die Funktion ConstructDerived() aufrufen ConstructFoo(), kopiere das resultierende temporäre Foo Objekt in den Superklassen-Slot eines Derived Objekts; Beenden Sie die Initialisierung des Derived -Objekts. Nur um das resultierende Objekt bei der Rückgabe erneut zu kopieren. Fügen Sie eine dritte Schicht hinzu, und das Ganze wird völlig lächerlich.

  2. Beim zweiten Ansatz haben die Funktionen ConstructClass() keinen Zugriff auf die Adresse des im Aufbau befindlichen Objekts. 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 structs vollwertige Klassen. Einige structs bü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 praktisch sein (es ist C99):

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

oder

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

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

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

Beachten Sie, dass der erste Ansatz dazu führen kann, dass Foo undurchsichtig ist (obwohl dies nicht der Art entspricht, wie Sie es derzeit verwenden). Dies ist normalerweise für eine langfristige Wartbarkeit wünschenswert. Sie können beispielsweise eine Funktion in Betracht ziehen, die eine undurchsichtige Foo -Struktur zuweist, ohne sie zu initialisieren. Oder Sie müssen eine Foo -Struktur neu initialisieren, die zuvor mit unterschiedlichen Werten initialisiert wurde.

1
jamesdlin

Abhängig vom Inhalt der Struktur und dem jeweils verwendeten Compiler kann jeder Ansatz schneller sein. Ein typisches Muster besteht darin, 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) Speicherplatz für die temporäre Struktur zuweisen und seine Adresse als "versteckten" Parameter übergeben. In Fällen, in denen die Rückgabe einer Funktion direkt an eine lokale Variable gespeichert wird, deren Adresse nicht von einem externen Code gehalten wird, können einige Compiler die Adresse dieser Variablen möglicherweise direkt übergeben.

Wenn ein Strukturtyp die Anforderungen einer bestimmten Implementierung erfüllt, die in Registern zurückgegeben werden sollen (z. B. entweder nicht größer als ein Maschinenwort oder genau zwei Maschinenwörter füllend), die eine Funktion zurückgeben, kann die Struktur schneller sein, als insbesondere die Adresse einer Struktur zu übergeben Da das Aussetzen der Adresse einer Variablen einem 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 enthält, aber dieses Formular verliert einige Optimierungsmöglichkeiten.

Es ist schade, dass C nicht die Möglichkeit bietet, zu sagen, dass es einer Funktion verboten ist, eine Kopie eines übergebenen Zeigers (Semantik ähnlich einer C++ - Referenz) zu behalten, da das Übergeben eines solchen eingeschränkten Zeigers die direkten Leistungsvorteile des Übergebens bringen würde ein Zeiger auf ein bereits vorhandenes Objekt, aber gleichzeitig die semantischen Kosten vermeiden, die entstehen, wenn ein Compiler die Adresse einer Variablen als "exponiert" betrachten muss.

1
supercat

Ein Argument für den Stil "Ausgabeparameter" 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 aus Gründen der Konsistenz sinnvoll sein, dass alle den Out-Parameter-Stil verwenden, wenn die Initialisierung für eine von ihnen fehlschlägt.

1
Viktor Dahl