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?
c
coding-style
constructors
initialization
Trevor Hickey
quelle
quelle
InitializeFoo()
ist ein Konstruktor. Der einzige Unterschied zu einem C ++ - Konstruktor besteht darin, dass derthis
Zeiger explizit und nicht implizit übergeben wird. Der kompilierte Code vonInitializeFoo()
und ein entsprechendes C ++Foo::Foo()
ist genau gleich.Antworten:
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.
quelle
void SetupFoo(Foo *out, int a, int b, int c)
Foo
Struktur führen? Der erste Ansatz führt auch die gesamte Initialisierung an einem Ort durch. (Oder erwägen Sie eine nicht initialisierteFoo
Struktur als "halb initialisiert"?)Beide Ansätze bündeln den Initialisierungscode in einem einzigen Funktionsaufruf. So weit, ist es gut.
Beim zweiten Ansatz gibt es jedoch zwei Probleme:
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
Derived
vonFoo
(structs werden für Objektorientierung in C weitgehend verwendet): Mit dem zweiten Ansatz die FunktionConstructDerived()
nennen würdeConstructFoo()
, kopieren Sie das resultierende temporäreFoo
Objekt über in den Superschlitz einesDerived
Objekts; Beenden Sie die Initialisierung desDerived
Objekts. 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.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
structs
Klassen vollwertig. Einigestructs
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 nützlich sein (es ist C99):oder
quelle
int x = 3;
den String nichtx
in der Binärdatei zu speichern . Die Adresse und die Vererbungspunkte sind gut; Das angenommene Scheitern der Wahl ist töricht.unsigned char
Optimierungen erlauben würde , für die die Eine strenge Aliasing-Regel würde nicht ausreichen und gleichzeitig die Erwartungen der Programmierer klarer machen.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.
quelle
Ein Argument für den "output-parameter" -Stil ist, dass die Funktion einen Fehlercode zurückgeben kann.
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.
quelle
errno
.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
Foo
mö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 undurchsichtigeFoo
Struktur zuweist, ohne sie zu initialisieren. Oder Sie müssen eineFoo
Struktur neu initialisieren , die zuvor mit anderen Werten initialisiert wurde.quelle