16

den folgenden vereinfachten Code Stellen:Passing Literal als const ref Parameter

#include <iostream> 
void foo(const int& x) { do_something_with(x); } 

int main() { foo(42); return 0; } 

(1) Optimizations beiseite, was passiert, wenn 42 zu foo geben wird?

Bleibt der Compiler 42 irgendwo (auf dem Stapel?) Und übergibt seine Adresse an foo?

(1a) Gibt es etwas im Standard, das vorschreibt, was in dieser Situation zu tun ist (oder ist es ausschließlich dem Compiler überlassen)?


Nun stelle man etwas anders Code: (wegen ODR)

#include <iostream> 
void foo(const int& x) { do_something_with(x); } 

struct bar { static constexpr int baz = 42; }; 

int main() { foo(bar::baz); return 0; } 

Es ist nicht verbinden wird, es sei denn, ich int bar::baz; definieren.

(2) Warum kann der Compiler außer ODR nicht das tun, was er mit 42 oben getan hat?


Eine offensichtliche Art und Weise die Dinge zu vereinfachen ist foo zu definieren:

void foo(int x) { do_something_with(x); } 

Doch was würde man im Falle einer Vorlage tun? ZB:

template<typename T> 
void foo(T&& x) { do_something_with(std::forward<T>(x)); } 

(3) Gibt es eine elegante Art und Weise foo zu sagen x von Wert für primitive Typen zu akzeptieren? Oder muss ich es mit SFINAE oder ähnlichem spezialisieren?

BEARBEITEN: Geändert, was innerhalb foo passiert, wie es für diese Frage irrelevant ist.

+0

Vielleicht ist der Code für T && generiert und T ist für 42 gleich und es ist einfach ein Wert in einem Register in der Funktion unabhängig davon, wie es an die Funktion übergeben wird? –

+0

Wenn diese Frage über die Implementierung des Compilers, die wirklich nicht definiert ist, als ein consxpr-Wert, kann der Compiler die 42 in Register nur innerhalb des Codes "movl". – Swift

Antwort

11

Hat der Compiler-Stick 42 irgendwo (? Auf dem Stack) und seine Adresse an foo passieren?

Ein temporäres Objekt vom Typ const int erzeugt wird, mit dem Ausdruck prvalue 42 initialisiert und auf die Referenz gebunden.

In der Praxis, wenn foo nicht inline ist, erfordert dies die Zuweisung von Speicherplatz auf dem Stapel, Speicherung 42 darin, und die Übergabe der Adresse.

Gibt es etwas im Standard, das diktiert, was in dieser Situation zu tun ist (oder ist es ausschließlich der Compiler)?

[dcl.init.ref].

Außer ODR, warum kann der Compiler nicht tun, was auch immer es mit 42 oben getan hat?

Da der Sprache nach, der Verweis auf das bar::baz Objekt gebunden ist, und es sei denn, der Compiler genau weiß, was foo an dem Punkt tut, wo er den Anruf kompiliert, dann muss sie davon ausgehen, dass dies von Bedeutung ist . Wenn beispielsweise foo eine assert(&x == &bar::baz); enthält, muss diese nicht mit foo(bar::baz) unter ausgelöst werden.

(In C++ 17, baz ist implicitly inline as a constexpr static data member, keine gesonderte Definition erforderlich ist.)

Gibt es eine elegante Art und Weise foo zu sagen x von Wert für primitive Typen zu akzeptieren?

Es im Allgemeinen nicht viel Sinn, Daten in tun dies in Abwesenheit von Profilierungs dass Pass-by-reference zeigt Probleme tatsächlich verursacht, aber wenn Sie wirklich aus irgendeinem Grund tun müssen, um es, (eventuell Zugabe SFINAE- eingeschränkte) Überlastungen wären der richtige Weg.

+0

"In C++ 17 ist baz implizit inline als statisches consExpr-Datenelement ..." Könnten Sie bitte einen Verweis auf den Standard hinzufügen? Was ist, wenn der Typ des Balkens zB "Std :: Chrono :: Millisekunden" ist? Wird es immer noch inline sein? –

+0

"Es ist im Allgemeinen nicht viel Sinn, dies in Ermangelung von Profiling-Daten zu tun, die zeigen, dass Pass-by-Reference tatsächlich ein Problem verursacht ..." Ich dachte in der Richtung einer Klasse, die sehr teuer zu kopieren ist. Wenn ich Sie richtig verstehe, muss man in diesem Fall Template-Überladungen verwenden? Auch wenn 'foo' eine langwierige Funktion ist? –

+0

@InnocentBystander Wenn Ihre Vorlage teuer zu kopierende Klassen akzeptieren kann, übergeben Sie sie als Referenz. Wenn dann ein Aufruf mit primitiven Typen tatsächlich Leistungsprobleme verursacht, fügen Sie Überladungen hinzu, um diese billig zu kopierenden Typen nach Wert zu übergeben. –

2

Mit 17 C++ Code, dass unter Berücksichtigung der Nutzungs kompiliert perfekt bar :: Baz als inline mit 14 C++ die Vorlage als Argument prvalue erfordert, so Compiler behält ein Symbol für bar::baz in Objektcode. Das wird nicht gelöst, weil Sie diese Erklärung nicht hatten. constexpr sollte von Compiler als constprvalue oder rvalues ​​behandelt werden, in Code-Generierung, die zu anderen Ansatz führen kann. Z.B. Wenn die aufgerufene Funktion inline ist, kann der Compiler Code generieren, der diesen bestimmten Wert als konstantes Argument der Anweisung des Prozessors verwendet. Keywords hier sind "sollte sein" und "kann", die sich von "müssen" wie üblich die Disclaimer-Klausel in der allgemeinen Standard-Dokumentation unterscheidet.

Für einen primitiven Typ, für einen temporären Wert und constexpr gibt es keinen Unterschied, in welcher Vorlage Signatur Sie verwenden. Wie der Compiler es implementiert, hängt von Plattform und Compiler ab ... und den verwendeten Aufrufkonventionen. Wir können nicht wirklich sagen, ob etwas auf dem Stack ist, da einige Plattformen keinen Stack haben oder anders als Stack auf der x86-Plattform implementiert sind. Mehrere moderne Aufrufkonventionen verwenden Register von CPU, um Argumente zu übergeben.

Wenn Ihr Compiler modern genug ist, brauchen Sie überhaupt keine Referenzen, kopieren Elision würde Sie vor zusätzlichen Kopieroperationen speichern.Um zu beweisen, dass:

#include <iostream> 

template<typename T> 
void foo(T x) { std::cout << x.baz << std::endl; } 


#include <iostream> 
using namespace std; 

struct bar 
{ 
    int baz; 

    bar(const int b = 0): baz(b) 
    { 
     cout << "Constructor called" << endl; 
    }  

    bar(const bar &b): baz(b.baz) //copy constructor 
    { 
     cout << "Copy constructor called" << endl; 
    } 
}; 

int main() 
{ 
    foo(bar(42)); 
} 

in der Ausgabe führen:

Constructor called 
42 

durch Verweis Übergeben, durch eine konstante Referenz würde nicht mehr kosten als wert vorbei, vor allem für Vorlagen. Wenn Sie eine andere Semantik benötigen, benötigen Sie eine explizite Spezialisierung der Vorlage. Einige ältere Compiler konnten Letzteres nicht ordnungsgemäß unterstützen.

template<typename T> 
void foo(const T& x) { std::cout << x.baz << std::endl; } 

// ... 

bar b(42); 
foo(b); 

Ausgang:

Constructor called 
42 

Nicht konstante Referenz würde uns nicht erlaubt Argument zu übermitteln, wenn es ein L-Wert ist, zum Beispiel

template<typename T> 
void foo(T& x) { std::cout << x.baz << std::endl; } 
// ... 
foo(bar(42)); 

durch diese Vorlage Aufruf (perfektes forwarding)

template<typename T> 
void foo(T&& x) { std::cout << x << std::endl; } 

könnte man Weiterleitungsprobleme vermeiden, obwohl dieser Prozess auch eine Kopie Elision involvieren würde. Compiler folgert Template-Parameter wie folgt aus C++ 17

template <class T> int f(T&& heisenreference); 
template <class T> int g(const T&&); 
int i; 
int n1 = f(i); // calls f<int&>(int&) 
int n2 = f(0); // calls f<int>(int&&) 
int n3 = g(i); // error: would call g<int>(const int&&), which 
       // would bind an rvalue reference to an lvalue 

A-Weiterleitungsreferenz ist eine rvalue Bezugnahme auf einen CV-unqualifizierten Template-Parameter. Wenn P eine Weiterleitungsreferenz ist und das Argument ein Wert ist, wird der Typ "L-Wert-Verweis auf A" anstelle von A für Typ-Abzug verwendet.

+1

* "Die Verwendung von Bewegungssemantik tatsächlich gefährlich" * - [das ist nicht Bewegungssemantik] (https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers). Ich denke, diese Frage ist komplizierter, als Sie wissen. – WhozCraig

+0

Sie haben * meinen * Punkt verpasst. Das ist nicht unbedingt eine rvalue-Referenz, und im Falle des OPs Code ist es am meisten definitiv nicht. [Siehe Beispiel] (http://ideone.com/cK4PUt). – WhozCraig

+1

Fehlender Umfang in 'bar :: baz' war ein Tippfehler. Ich habe es behoben –

2

Ihr Beispiel # 1. Die konstante Position hängt vollständig vom Compiler ab und ist nicht in einem Standard definiert. GCC unter Linux kann solche Konstanten in einem statischen schreibgeschützten Speicherbereich zuweisen. Die Optimierung wird wahrscheinlich alles zusammen entfernen.

Ihr Beispiel # 2 wird nicht kompiliert (vor dem Link). Aufgrund der Scoping-Regeln. Du brauchst also bar::baz da.

Beispiel # 3, ich tun dies in der Regel:

template<typename T> 
    void foo(const T& x) { std::cout << x << std::endl; } 
+0

Fehlender Umfang in 'bar :: baz' war ein Tippfehler. Ich habe es behoben. –

Verwandte Themen