2017-01-28 4 views
10

Ich verstehe im Allgemeinen, wie eine Funktion ein Objekt nach Wert zurückgibt. Aber ich wollte es auf der unteren Ebene verstehen. Montageebene, wenn sinnvoll.Wo wird das Rückgabeobjekt gespeichert?

verstehe ich das dieser Code

ClassA fun(){ 
    ClassA a; 
    a.set(...); 
    return a; 
} 

umgewandelt wird intern auf

void fun(Class& ret){ 
    ClassA a; 
    a.set(...); 
    ret.ClassA::ClassA(a); 
} 

, die den Kopierkonstruktor auf den Rückgabewert effektiv nennen.

Ich verstehe auch, dass es einige Optimierungen (wie NRVO) gibt, die den folgenden Code generieren könnten, der den Kopierkonstruktor vermeidet.

void fun(Class& ret){ 
    ret.set(...); 
} 

Meine Frage ist jedoch ein bisschen mehr grundlegend. Es hat nicht wirklich mit Objekten im Speziellen zu tun. Es könnten sogar primitive Typen sein.

Können sagen, wir haben diesen Code:

int fun(){ 
    return 0; 
} 
int main(){ 
    fun(); 
} 

Meine Frage ist, wo ist das Rückgabeobjekt im Speicher abgelegt.

Wenn wir auf den Stapel schauen ... Dort ist der Stapelrahmen von main und dann der Stapelrahmen von fun. Ist das Rückgabeobjekt in einer Adresse wie zwischen den beiden Stapelrahmen gespeichert? Oder vielleicht ist es irgendwo im main Stack-Frame gespeichert (und möglicherweise ist das die Adresse, die als Referenz im generierten Code übergeben wird).

Ich habe darüber nachgedacht und die zweite scheint praktischer, aber ich verstehe nicht, wie der Compiler wissen, wie viel Speicher im Stapelrahmen von main schieben? Errechnet er, was der größte Rückgabetyp ist und pusht das, obwohl es etwas verschwendete Speicher geben könnte? Oder wird es dynamisch ausgeführt, reserviert es diesen Speicherplatz erst, bevor die Funktion aufgerufen wird?

+6

Hängt vom Compiler ab, aber Sie können immer [siehe Assembly] (https://godbolt.org/g/JztY5G). – chris

+1

Übrigens, 'ret.ClassA :: ClassA (a);' kompiliert nicht. So sollte es geschrieben werden: 'new (& ret) ClassA (a);'. – HolyBlackCat

+2

Starkes Implementierungsdetail. Im Allgemeinen wird der Compiler versuchen, alles in den CPU-Registern zurückzugeben. Wenn es nicht passt, reserviert der Anrufer Speicherplatz auf seinem Stapelrahmen und übergibt einen Zeiger an diesen Speicher. Das wahrscheinlichste Szenario in diesem Fall. Probieren Sie es einfach in Ihrem eigenen Compiler aus, und fordern Sie es auf, einen Assembly-Eintrag zu generieren. Sie haben eine Tatsache statt einer Schätzung. –

Antwort

12

Die C++ - Sprachspezifikation spezifiziert diese Low-Level-Details nicht. Sie werden von jeder C++ - Implementierung angegeben, und die tatsächlichen Implementierungsdetails variieren von Plattform zu Plattform.

In fast jedem Fall wird ein Rückgabewert, der ein einfacher nativer Typ ist, in einem bestimmten, angegebenen CPU-Register zurückgegeben. Wenn eine Funktion eine Klasseninstanz zurückgibt, variieren die Details je nach Implementierung. Es gibt mehrere gängige Ansätze, aber der typische Fall wäre, dass der Aufrufer genügend Speicherplatz für den Rückgabewert auf dem Stack reserviert, bevor er die Funktion aufruft und einen zusätzlichen versteckten Parameter an die Funktion übergibt, an die die Funktion kopieren wird der zurückgegebene Wert (oder konstruieren Sie es, im Falle von RVO). Oder der Parameter ist implizit und die Funktion kann den Speicherplatz auf dem Stack für den Rückgabewert selbst nach dem Stack-Frame des Aufrufs finden.

Es ist auch möglich, dass eine bestimmte C++ - Implementierung immer noch ein CPU-Register verwendet, um Klassen zurückzugeben, die klein genug sind, um in ein einzelnes CPU-Register zu passen. Oder vielleicht sind einige CPU-Register für die Rückgabe etwas größerer Klassen reserviert.

Die Details variieren, und Sie müssen die Dokumentation für Ihren C++ - Compiler oder Ihr Betriebssystem konsultieren, um die spezifischen Details zu ermitteln, die für Sie gelten.

+1

Normalerweise wird die Aufrufkonvention von der Plattform ABI angegeben, die zwar Betriebssystem-abhängig sein kann, aber nicht streng genommen Teil des Betriebssystems ist, noch ist es Sache des Compilers. Natürlich kann Dokumentation überall gefunden werden (wenn Sie Glück haben) oder nirgendwo (wenn Sie nicht sind), aber die Suche nach einem ABI ist oft ein guter Anfang. – rici

3

In dem folgenden Code:

int fun() 
{ 
    return 0; 
} 

Der Rückgabewert wird in einem Register gespeichert. Bei Intel-Architekturen ist dies normalerweise ax (16-Bit) oder eax (32-Bit) oder rax (64-Bit). (Historisch bekannt als der Akkumulator.)

Wenn der Rückgabewert ein Zeiger oder eine Referenz auf ein Objekt war, würde es immer noch durch dieses Register zurückgegeben werden.

Wenn der Rückgabewert größer als ein Maschinenwort ist, kann es erforderlich sein, dass das ABI (Application Binary Interface) ein anderes Register verwendet, um das höherwertige Wort zu halten. Wenn Sie also eine 32-Bit-Menge in einer 16-Bit-Architektur zurückgeben, wird dx:ax verwendet. (Und so weiter für größere Mengen in größeren Architekturen.)

Größere Rückgabewerte werden mit anderen Mitteln wie dem bereits bekannten Mechanismus void fun(Class& ret) übergeben.

Das Zurückgeben von Rückgabewerten über das Akkumulatorregister ist sehr effizient und es ist eine ziemlich starke Konvention, fast alle ABIs, die ich gesehen habe, erfordern es.

+1

Das ist wirklich irreführend. Unabhängig von der Optimierung müssen Übersetzungseinheiten in der Lage sein, Funktionen aufzurufen, die in anderen Übersetzungseinheiten kompiliert wurden, auch wenn die beiden Übersetzungseinheiten mit unterschiedlichen Optimierungseinstellungen kompiliert wurden. Daher kann die Optimierung nur Aufrufkonventionen für Funktionen ändern, die nur innerhalb der Übersetzungseinheit sichtbar sind, in der sie definiert sind (d. H. "Statisch"). Bei extern sichtbaren Funktionen bestimmt die Plattform ABI die Aufrufkonvention unabhängig von Optimierungseinstellungen. – rici

+0

@rici was ist irreführend? Mein gesamter Beitrag oder ein bestimmter Punkt, den ich mache? –

+0

Die letzten beiden Absätze, in denen Sie vorschlagen, dass der Optimierungsgrad die Aufrufkonvention beeinflusst. – rici

6

Die Antwort ist ABI spezifische aber in der Regel werden die Anrufe mit einem versteckten Parameter zusammengestellt, die der Zeiger auf den Speicher ist, dass die Funktion verwendet werden soll, wie Sie die Funktion als

kompiliert wird angenommen, die
void fun(Class& ret){ 
    ClassA a; 
    a.set(...); 
    ret.ClassA::ClassA(a); 
} 

dann bei Aufrufort werden Sie so etwas wie

Class instance = fun(); 
fun(instance); 

haben nun diese dem Anrufer Reserve sizeof(Class) Bytes auf dem Stack macht und die Adresse an die Funktion übergeben, so dass fun kann diesen Raum „füllen“.

Dies unterscheidet sich nicht davon, wie der Stack-Frame des Aufrufers Speicherplatz für seine eigenen Locals reservieren würde. Der einzige Unterschied besteht darin, dass die Adresse an einen seiner Locals an fun übergeben wird.

Beachten Sie, dass wenn sizeof(Class) kleiner als die Größe eines Registers (oder ein paar Register) ist, ist es durchaus möglich, dass der Wert direkt in ihnen zurückgegeben wird.