Ist es sicher, eine Struktur in C oder C ++ zurückzugeben?

82

Was ich verstehe ist, dass dies nicht getan werden sollte, aber ich glaube, ich habe Beispiele gesehen, die so etwas tun (Notizcode ist nicht unbedingt syntaktisch korrekt, aber die Idee ist da)

typedef struct{
    int a,b;
}mystruct;

Und dann ist hier eine Funktion

mystruct func(int c, int d){
    mystruct retval;
    retval.a = c;
    retval.b = d;
    return retval;
}

Ich habe verstanden, dass wir immer einen Zeiger auf eine malloc'ed Struktur zurückgeben sollten, wenn wir so etwas tun wollen, aber ich bin mir sicher, dass ich Beispiele gesehen habe, die so etwas tun. Ist das richtig? Persönlich gebe ich immer entweder einen Zeiger auf eine malloc'ed Struktur zurück oder mache einfach einen Durchlauf unter Bezugnahme auf die Funktion und ändere die Werte dort. (Da ich verstehe, dass nach Ablauf des Funktionsumfangs jeder Stapel, der zum Zuweisen der Struktur verwendet wurde, überschrieben werden kann).

Fügen wir der Frage einen zweiten Teil hinzu: Variiert dies je nach Compiler? Wenn ja, wie verhalten sich dann die neuesten Versionen von Compilern für Desktops: gcc, g ++ und Visual Studio?

Gedanken dazu?

Jzepeda
quelle
32
"Was ich verstehe ist, dass dies nicht getan werden sollte", sagt wer? Ich mache es die ganze Zeit. Beachten Sie auch, dass das typedef in C ++ nicht erforderlich ist und dass es kein "C / C ++" gibt.
PlasmaHH
4
Die Frage scheint nicht auf c ++ gerichtet zu sein.
Kapitän Giraffe
4
@PlasmaHH Das Kopieren großer Strukturen kann ineffizient sein. Aus diesem Grund sollte man vorsichtig sein und überlegen, bevor man eine Struktur nach Wert zurückgibt, insbesondere wenn die Struktur einen teuren Kopierkonstruktor hat und der Compiler nicht gut in der Rückgabewertoptimierung ist. Ich habe kürzlich eine Optimierung für eine App vorgenommen, die einen erheblichen Teil ihrer Zeit in Kopierkonstruktoren für einige große Strukturen aufgewendet hat, die ein Programmierer überall nach Wert zurückgegeben hatte. Die Ineffizienz kostete uns etwa 800.000 US-Dollar an zusätzlicher Hardware für Rechenzentren, die wir kaufen mussten.
Crashworks
8
@ Crashworks: Herzlichen Glückwunsch, ich hoffe dein Chef hat dir eine Gehaltserhöhung gegeben.
PlasmaHH
5
@Crashworks: Sicher ist es schlecht, immer nach Wert zurückzukehren, ohne nachzudenken, aber in Situationen, in denen es selbstverständlich ist, gibt es normalerweise keine sichere Alternative, für die keine Kopie erstellt werden muss. Daher ist die Rückgabe nach Wert die beste Lösung keine Heap-Zuordnung erforderlich. Oft gibt es nicht einmal eine Kopie. Wenn Sie eine gute Compiler-Kopie verwenden, sollte die Elision sofort aktiviert werden. In C ++ 11 kann durch die Verschiebungssemantik noch mehr tiefes Kopieren vermieden werden. Beide Mechanismen funktionieren nicht richtig, wenn Sie etwas anderes tun, als nach Wert zurückzukehren.
links um den

Antworten:

75

Es ist absolut sicher und es ist nicht falsch, dies zu tun. Außerdem: Es variiert nicht je nach Compiler.

Wenn (wie in Ihrem Beispiel) Ihre Struktur nicht zu groß ist, würde ich normalerweise argumentieren, dass dieser Ansatz sogar besser ist als die Rückgabe einer malloc'ed Struktur ( mallocist eine teure Operation).

Pablo Santa Cruz
quelle
3
Wäre es immer noch sicher, wenn eines der Felder ein Zeichen * wäre? Jetzt würde es Zeiger in der Struktur geben
jzepeda
3
@ user963258 Das hängt davon ab, wie Sie den Kopierkonstruktor und den Destruktor implementieren.
Luchian Grigore
2
@PabloSantaCruz Das ist eine schwierige Frage. Wenn es sich um eine Prüfungsfrage handelt, kann der Prüfer ein "Nein" als Antwort erwarten, wenn das Eigentum berücksichtigt werden muss.
Kapitän Giraffe
2
@ CaptainGiraffe: stimmt. Da OP dies nicht klarstellte und seine / ihre Beispiele im Grunde C waren , nahm ich an, dass es sich eher um eine C- Frage als um eine C ++ - Frage handelte .
Pablo Santa Cruz
2
@Kos Einige Compiler haben kein NRVO? Ab welchem ​​Jahr? Beachten Sie auch: In C ++ 11 wird stattdessen eine Verschiebungssemantik aufgerufen, auch wenn NRVO nicht vorhanden ist.
David
70

Es ist absolut sicher.

Sie kehren nach Wert zurück. Was zu undefiniertem Verhalten führen würde, wäre, wenn Sie als Referenz zurückkehren würden.

//safe
mystruct func(int c, int d){
    mystruct retval;
    retval.a = c;
    retval.b = d;
    return retval;
}

//undefined behavior
mystruct& func(int c, int d){
    mystruct retval;
    retval.a = c;
    retval.b = d;
    return retval;
}

Das Verhalten Ihres Snippets ist vollkommen gültig und definiert. Es variiert nicht je nach Compiler. Es ist in Ordnung!

Persönlich gebe ich immer entweder einen Zeiger auf eine malloc'ed Struktur zurück

Das solltest du nicht. Sie sollten dynamisch zugewiesenen Speicher nach Möglichkeit vermeiden.

oder führen Sie einfach einen Verweis auf die Funktion durch und ändern Sie die Werte dort.

Diese Option ist vollkommen gültig. Es ist eine Frage der Wahl. Im Allgemeinen tun Sie dies, wenn Sie etwas anderes von der Funktion zurückgeben möchten, während Sie die ursprüngliche Struktur ändern.

Nach meinem Verständnis kann jeder Stapel, der zum Zuweisen der Struktur verwendet wurde, überschrieben werden, sobald der Funktionsumfang abgelaufen ist

Das ist falsch. Ich meinte, es ist irgendwie korrekt, aber Sie geben eine Kopie der Struktur zurück, die Sie innerhalb der Funktion erstellt haben. Theoretisch . In der Praxis kann und wird RVO auftreten. Informieren Sie sich über die Optimierung des Rückgabewerts. Dies bedeutet, dass retvalder Funktionsumfang nach Beendigung der Funktion möglicherweise nicht mehr im aufrufenden Kontext erstellt wird, um die zusätzliche Kopie zu verhindern. Dies ist eine Optimierung, die der Compiler frei implementieren kann.

Luchian Grigore
quelle
3
+1 für die Erwähnung von RVO. Diese wichtige Optimierung macht dieses Muster tatsächlich für Objekte mit teuren Kopierkonstruktoren wie STL-Containern möglich.
Kos
1
Es ist erwähnenswert, dass der Compiler zwar die Möglichkeit hat, die Rückgabewertoptimierung durchzuführen, es jedoch keine Garantie dafür gibt. Darauf können Sie nicht zählen, nur hoffen.
Watcom
-1 für "Vermeiden von dynamisch zugewiesenem Speicher, wenn möglich." Dies ist in der Regel eine neue Regel und führt häufig zu Code, in dem GROSSE Datenmengen zurückgegeben werden (und sie rätseln, warum die Dinge langsam laufen), wenn ein einfacher Zeiger viel Zeit sparen kann. Die richtige Regel sind Rückgabestrukturen oder Zeiger, die auf der Geschwindigkeit basieren , Verwendung und Klarheit .
Lloyd Sargent
9

Die Lebensdauer des mystructObjekts in Ihrer Funktion endet tatsächlich, wenn Sie die Funktion verlassen. Sie übergeben das Objekt jedoch als Wert in der return-Anweisung. Dies bedeutet, dass das Objekt aus der Funktion in die aufrufende Funktion kopiert wird. Das ursprüngliche Objekt ist verschwunden, aber die Kopie lebt weiter.

Joseph Mansfield
quelle
8

Es ist nicht nur sicher, ein structin C zurückzugeben (oder ein classin C ++, wo struct-s tatsächlich class-es mit Standardmitgliedern public:sind), sondern eine Menge Software tut dies auch.

Natürlich classgibt die Sprache bei der Rückgabe eines in C ++ an, dass ein Destruktor oder ein sich bewegender Konstruktor aufgerufen wird, aber es gibt viele Fälle, in denen dies vom Compiler optimiert werden könnte.

Darüber hinaus gibt der Linux x86-64 ABI an, dass die Rückgabe von a structmit zwei skalaren (z. B. Zeigern oder long) Werten über die Register ( %rax& %rdx) erfolgt, was sehr schnell und effizient ist. In diesem speziellen Fall ist es wahrscheinlich schneller, solche Felder mit zwei Skalaren zurückzugeben, structals irgendetwas anderes zu tun (z. B. sie in einem als Argument übergebenen Zeiger zu speichern).

Das Zurückgeben eines solchen Zwei-Skalar-Feldes structist dann viel schneller als das mallocZurückgeben und das Zurückgeben eines Zeigers.

Basile Starynkevitch
quelle
5

Es ist vollkommen legal, aber bei großen Strukturen müssen zwei Faktoren berücksichtigt werden: Geschwindigkeit und Stapelgröße.

ebutusov
quelle
Von der Optimierung des Rückgabewerts gehört?
Luchian Grigore
Ja, aber wir sprechen allgemein von der Rückgabe von struct by value, und es gibt Fälle, in denen der Compiler keine RVO ausführen kann.
Ebutusov
3
Ich würde sagen, mach dir erst Sorgen um die zusätzliche Kopie, nachdem du ein Profil erstellt hast.
Luchian Grigore
4

Ein Strukturtyp kann der Typ für den von einer Funktion zurückgegebenen Wert sein. Dies ist sicher, da der Compiler eine Kopie von struct erstellt und die Kopie zurückgibt, nicht die lokale Struktur in der Funktion.

typedef struct{
    int a,b;
}mystruct;

mystruct func(int c, int d){
    mystruct retval;
    cout << "func:" <<&retval<< endl;
    retval.a = c;
    retval.b = d;
    return retval;
}

int main()
{
    cout << "main:" <<&(func(1,2))<< endl;


    system("pause");
}
Kurzwaren
quelle
4

Die Sicherheit hängt davon ab, wie die Struktur selbst implementiert wurde. Ich bin gerade auf diese Frage gestoßen, als ich etwas Ähnliches implementiert habe, und hier ist das potenzielle Problem.

Der Compiler führt bei der Rückgabe des Werts einige Operationen aus (unter anderem):

  1. Ruft den Kopierkonstruktor auf mystruct(const mystruct&)( thisist eine temporäre Variable außerhalb der funcvom Compiler selbst zugewiesenen Funktion ).
  2. Ruft den Destruktor ~mystructfür die Variable auf, die darin zugewiesen wurdefunc
  3. ruft auf, mystruct::operator=wenn der zurückgegebene Wert mit etwas anderem zugewiesen ist=
  4. Ruft den Destruktor ~mystructfür die vom Compiler verwendete temporäre Variable auf

Wenn nun mystructist so einfach wie das hier alles beschrieben ist in Ordnung, aber wenn es Zeiger (wie hat char*) oder kompliziertere Speicherverwaltung, dann es hängt alles davon ab , wie mystruct::operator=, mystruct(const mystruct&)und ~mystructumgesetzt werden. Daher empfehle ich Vorsichtsmaßnahmen, wenn komplexe Datenstrukturen als Wert zurückgegeben werden.

Filippo
quelle
Nur wahr vor c ++ 11.
Björn Sundin
4

Es ist absolut sicher, eine Struktur so zurückzugeben, wie Sie es getan haben.

Basierend auf dieser Aussage jedoch: Da ich verstehe, dass nach Ablauf des Funktionsumfangs jeder Stapel, der zum Zuweisen der Struktur verwendet wurde, überschrieben werden kann, würde ich mir nur ein Szenario vorstellen, in dem eines der Mitglieder der Struktur dynamisch zugewiesen wurde ( malloc'ed oder new'ed). In diesem Fall werden ohne RVO die dynamisch zugewiesenen Mitglieder zerstört und auf der zurückgegebenen Kopie wird ein Mitglied auf Müll verweisen.

Maress
quelle
Der Stapel wird nur vorübergehend für den Kopiervorgang verwendet. Normalerweise wird der Stapel vor dem Aufruf reserviert, und die aufgerufene Funktion legt die zurückzugebenden Daten auf dem Stapel ab. Anschließend ruft der Aufrufer diese Daten vom Stapel ab und speichert sie dort, wo sie zugewiesen werden. Also keine Sorge.
Thomas Tempelmann
3

Ich werde auch sftrabbit zustimmen. Das Leben endet tatsächlich und der Stapelbereich wird aufgeräumt, aber der Compiler ist intelligent genug, um sicherzustellen, dass alle Daten in Registern oder auf andere Weise abgerufen werden.

Ein einfaches Beispiel zur Bestätigung ist unten angegeben (entnommen aus der Mingw-Compiler-Assembly).

_func:
    push    ebp
    mov ebp, esp
    sub esp, 16
    mov eax, DWORD PTR [ebp+8]
    mov DWORD PTR [ebp-8], eax
    mov eax, DWORD PTR [ebp+12]
    mov DWORD PTR [ebp-4], eax
    mov eax, DWORD PTR [ebp-8]
    mov edx, DWORD PTR [ebp-4]
    leave
    ret

Sie können sehen, dass der Wert von b über edx übertragen wurde. während das Standard-eax den Wert für a enthält.

Perilbrain
quelle
2

Es ist nicht sicher, eine Struktur zurückzugeben. Ich mache es gerne selbst, aber wenn jemand später einen Kopierkonstruktor zur zurückgegebenen Struktur hinzufügt, wird der Kopierkonstruktor aufgerufen. Dies kann unerwartet sein und den Code beschädigen. Dieser Fehler ist sehr schwer zu finden.

Ich hatte eine ausführlichere Antwort, aber der Moderator mochte es nicht. Auf Ihre Kosten ist mein Tipp also kurz.

Igor Polk
quelle
„Es ist nicht sicher, eine Struktur zurückzugeben. […] Der Kopierkonstruktor wird aufgerufen. “ - Es gibt einen Unterschied zwischen sicher und ineffizient . Das Zurückgeben einer Struktur ist definitiv sicher. Selbst dann wird der Aufruf des Kopierers höchstwahrscheinlich vom Compiler entfernt, da die Struktur zunächst auf dem Stapel des Aufrufers erstellt wird.
Phg
2

Fügen wir der Frage einen zweiten Teil hinzu: Variiert dies je nach Compiler?

In der Tat, wie ich zu meinem Schmerz herausgefunden habe: http://sourceforge.net/p/mingw-w64/mailman/message/33176880/

Ich habe gcc unter win32 (MinGW) verwendet, um COM-Schnittstellen aufzurufen, die Strukturen zurückgeben. Es stellt sich heraus, dass MS es anders macht als GNU und so stürzte mein (gcc) Programm mit einem zerschlagenen Stapel ab.

Es könnte sein, dass MS hier einen höheren Stellenwert hat - aber alles, was mich interessiert, ist die ABI-Kompatibilität zwischen MS und GNU für den Aufbau unter Windows.

Wenn ja, wie verhält es sich dann mit den neuesten Versionen von Compilern für Desktops: gcc, g ++ und Visual Studio

Auf einer Wine-Mailingliste finden Sie einige Nachrichten darüber, wie MS dies zu tun scheint.

effbiae
quelle
Es wäre hilfreicher, wenn Sie einen Zeiger auf die Wine-Mailingliste geben würden, auf die Sie sich beziehen.
Jonathan Leffler
Das Zurückgeben von Strukturen ist in Ordnung. COM gibt eine binäre Schnittstelle an; Wenn jemand COM nicht richtig implementiert, wäre das ein Fehler.
MM
1

Hinweis: Diese Antwort gilt nur für C ++ 11 und höher. Es gibt kein "C / C ++", es sind verschiedene Sprachen.

Nein, es besteht keine Gefahr, ein lokales Objekt nach Wert zurückzugeben, und es wird empfohlen, dies zu tun. Ich denke jedoch, dass in allen Antworten hier ein wichtiger Punkt fehlt. Viele andere haben gesagt, dass die Struktur entweder kopiert oder direkt mit RVO platziert wird. Dies ist jedoch nicht ganz richtig. Ich werde versuchen, genau zu erklären, welche Dinge bei der Rückgabe eines lokalen Objekts passieren können.

Semantik verschieben

Seit c ++ 11 haben wir rWertreferenzen, die auf temporäre Objekte verweisen, denen sicher gestohlen werden kann. Beispielsweise verfügt std :: vector über einen Verschiebungskonstruktor sowie einen Verschiebungszuweisungsoperator. Beide haben eine konstante Komplexität und kopieren einfach den Zeiger auf die Daten des Vektors, von dem verschoben wird. Ich werde hier nicht näher auf die Bewegungssemantik eingehen.

Da ein lokal innerhalb einer Funktion erstelltes Objekt temporär ist und bei der Rückkehr der Funktion den Gültigkeitsbereich verlässt, wird ein zurückgegebenes Objekt ab C ++ 11 niemals mehr kopiert. Der Verschiebungskonstruktor wird für das zurückgegebene Objekt aufgerufen (oder nicht, wie später erläutert). Dies bedeutet, dass, wenn Sie ein Objekt mit einem teuren Kopierkonstruktor, aber einem kostengünstigen Verschiebungskonstruktor wie einem großen Vektor zurückgeben, nur das Eigentum an den Daten vom lokalen Objekt auf das zurückgegebene Objekt übertragen wird - was billig ist.

Beachten Sie, dass es in Ihrem speziellen Beispiel keinen Unterschied zwischen dem Kopieren und Verschieben des Objekts gibt. Die Standardkonstruktoren zum Verschieben und Kopieren Ihrer Struktur führen zu denselben Operationen. Kopieren von zwei ganzen Zahlen. Dies ist jedoch mindestens so schnell wie jede andere Lösung, da die gesamte Struktur in ein 64-Bit-CPU-Register passt (korrigieren Sie mich, wenn ich falsch liege, ich kenne nicht viele CPU-Register).

RVO und NRVO

RVO bedeutet Rückgabewertoptimierung und ist eine der wenigen Optimierungen, die Compiler durchführen und die Nebenwirkungen haben können. Seit c ++ 17 ist RVO erforderlich. Wenn ein unbenanntes Objekt zurückgegeben wird, wird es direkt an der Stelle erstellt, an der der Aufrufer den zurückgegebenen Wert zuweist. Weder der Kopierkonstruktor noch der Verschiebungskonstruktor werden aufgerufen. Ohne RVO würde das unbenannte Objekt zuerst lokal erstellt, dann in der zurückgegebenen Adresse erstellt und dann das lokale unbenannte Objekt zerstört.

Beispiel, bei dem RVO erforderlich ist (c ++ 17) oder wahrscheinlich (vor c ++ 17):

auto function(int a, int b) -> MyStruct {
    // ...
    return MyStruct{a, b};
}

NRVO bedeutet Named Return Value Optimization und ist dasselbe wie RVO, außer dass es für ein benanntes Objekt durchgeführt wird, das lokal für die aufgerufene Funktion ist. Dies wird vom Standard (c ++ 20) immer noch nicht garantiert, aber viele Compiler tun dies immer noch. Beachten Sie, dass selbst bei benannten lokalen Objekten diese bei der Rückgabe im schlimmsten Fall verschoben werden.

Fazit

Der einzige Fall, in dem Sie in Betracht ziehen sollten, nicht nach Wert zurückzukehren, ist, wenn Sie ein benanntes, sehr großes Objekt (wie in seiner Stapelgröße) haben. Dies liegt daran, dass NRVO (ab c ++ 20) noch nicht garantiert ist und selbst das Bewegen des Objekts langsam wäre. Meine Empfehlung und die Empfehlung in den Cpp-Kernrichtlinien lautet, Objekte immer nach Wert zurückzugeben (wenn mehrere Rückgabewerte verwendet werden, verwenden Sie struct (oder tuple)), wobei die einzige Ausnahme darin besteht, dass das Verschieben des Objekts teuer ist. Verwenden Sie in diesem Fall einen nicht konstanten Referenzparameter.

Es ist NIE eine gute Idee, eine Ressource zurückzugeben, die manuell von einer Funktion in C ++ freigegeben werden muss. TU das niemals. Verwenden Sie mindestens ein std :: unique_ptr oder erstellen Sie eine eigene nicht lokale oder lokale Struktur mit einem Destruktor, der seine Ressource ( RAII ) freigibt, und geben Sie eine Instanz davon zurück. Es wäre dann auch eine gute Idee, den Verschiebungskonstruktor und den Verschiebungszuweisungsoperator zu definieren, wenn die Ressource keine eigene Verschiebungssemantik hat (und den Kopierkonstruktor / die Zuweisung löschen).

Björn Sundin
quelle
Ich denke, Golang hat etwas Ähnliches.
Mox