Ist die Praxis, eine C ++ - Referenzvariable zurückzugeben, böse?

341

Das ist ein wenig subjektiv, denke ich; Ich bin mir nicht sicher, ob die Meinung einstimmig sein wird (ich habe viele Codefragmente gesehen, in denen Referenzen zurückgegeben werden).

Laut einem Kommentar zu dieser Frage, den ich gerade bezüglich der Initialisierung von Referenzen gestellt habe , kann die Rückgabe einer Referenz böse sein, da es [soweit ich weiß] einfacher ist, das Löschen zu übersehen, was zu Speicherverlusten führen kann.

Das macht mir Sorgen, da ich Beispielen gefolgt bin (es sei denn, ich stelle mir Dinge vor) und dies an einigen Stellen getan habe ... Habe ich das falsch verstanden? Ist es böse? Wenn ja, wie böse?

Ich habe das Gefühl, dass meine Anwendungen aufgrund meiner gemischten Menge an Zeigern und Referenzen, kombiniert mit der Tatsache, dass ich neu in C ++ bin, und der völligen Verwirrung darüber, was wann verwendet werden soll, Speicherleck sein müssen ...

Ich verstehe auch, dass die Verwendung von intelligenten / gemeinsam genutzten Zeigern allgemein als der beste Weg zur Vermeidung von Speicherlecks akzeptiert wird.

Nick Bolton
quelle
Es ist nicht böse, wenn Sie getterähnliche Funktionen / Methoden schreiben.
John Z. Li

Antworten:

411

Im Allgemeinen ist die Rückgabe einer Referenz völlig normal und geschieht ständig.

Wenn du meinst:

int& getInt() {
    int i;
    return i;  // DON'T DO THIS.
}

Das ist alles Böse. Der zugewiesene Stapel verschwindet iund Sie beziehen sich auf nichts. Das ist auch böse:

int& getInt() {
    int* i = new int;
    return *i;  // DON'T DO THIS.
}

Denn jetzt muss der Kunde irgendwann das Seltsame tun:

int& myInt = getInt(); // note the &, we cannot lose this reference!
delete &myInt;         // must delete...totally weird and  evil

int oops = getInt(); 
delete &oops; // undefined behavior, we're wrongly deleting a copy, not the original

Beachten Sie, dass rWert-Referenzen immer noch nur Referenzen sind, sodass alle bösen Anwendungen gleich bleiben.

Wenn Sie etwas zuweisen möchten, das über den Funktionsumfang hinausgeht, verwenden Sie einen intelligenten Zeiger (oder allgemein einen Container):

std::unique_ptr<int> getInt() {
    return std::make_unique<int>(0);
}

Und jetzt speichert der Client einen intelligenten Zeiger:

std::unique_ptr<int> x = getInt();

Referenzen sind auch für den Zugriff auf Dinge in Ordnung, bei denen Sie wissen, dass die Lebensdauer auf einer höheren Ebene offen gehalten wird, z.

struct immutableint {
    immutableint(int i) : i_(i) {}

    const int& get() const { return i_; }
private:
    int i_;
};

Hier wissen wir, dass es in Ordnung ist, einen Verweis auf zurückzugeben, i_da alles, was uns aufruft, die Lebensdauer der Klasseninstanz verwalteti_ mindestens so lange lebt.

Und natürlich ist nichts falsch daran, nur:

int getInt() {
   return 0;
}

Wenn die Lebensdauer dem Anrufer überlassen bleiben soll und Sie nur den Wert berechnen.

Zusammenfassung: Es ist in Ordnung, eine Referenz zurückzugeben, wenn die Lebensdauer des Objekts nach dem Aufruf nicht endet.

GManNickG
quelle
21
Dies sind alles schlechte Beispiele. Das beste Beispiel für die ordnungsgemäße Verwendung ist, wenn Sie einen Verweis auf ein Objekt zurückgeben, das übergeben wurde. Ala operator <<
Arelius
171
Aus Gründen der Nachwelt und für neuere Programmierer, die darauf setzen, sind Zeiger nicht schlecht . Auch sind Zeiger auf den dynamischen Speicher nicht schlecht. Beide haben ihre legitimen Plätze in C ++. Intelligente Zeiger sollten auf jeden Fall Ihre Standardeinstellung für die dynamische Speicherverwaltung sein, aber Ihr standardmäßiger intelligenter Zeiger sollte unique_ptr und nicht shared_ptr sein.
Jamin Gray
12
Genehmiger bearbeiten: Genehmigen Sie keine Änderungen, wenn Sie nicht für die Richtigkeit bürgen können. Ich habe die falsche Bearbeitung rückgängig gemacht.
GManNickG
7
Schreibenreturn new int Sie aus Gründen der Nachwelt und für neuere Programmierer, die darauf stoßen, nicht .
Leichtigkeitsrennen im Orbit
3
Aus Gründen der Nachwelt und für alle neueren Programmierer, die darauf stoßen, geben Sie einfach das T von der Funktion zurück. RVO wird sich um alles kümmern.
Schuh
64

Nein, nein, tausendmal nein.

Was böse ist, ist ein Verweis auf ein dynamisch zugewiesenes Objekt und der Verlust des ursprünglichen Zeigers. Wenn Sie newein Objekt haben, übernehmen Sie die Verpflichtung, eine Garantie zu haben delete.

Aber schauen Sie sich zB an operator<<: das muss eine Referenz zurückgeben, oder

cout << "foo" << "bar" << "bletch" << endl ;

wird nicht funktionieren.

Charlie Martin
quelle
23
Ich habe abgelehnt, weil dies weder die Frage beantwortet (in der OP klargestellt hat, dass er die Notwendigkeit einer Löschung kennt), noch die berechtigte Befürchtung anspricht, dass die Rückgabe eines Verweises auf ein Freestore-Objekt zu Verwirrung führen kann. Seufzer.
4
Die Praxis, ein Referenzobjekt zurückzugeben, ist nicht böse. Ergo nein. Die Angst, die er ausdrückt, ist eine richtige Angst, wie ich in der zweiten Grafik hervorhole.
Charlie Martin
Das hast du eigentlich nicht getan. Aber das ist meine Zeit nicht wert.
2
Iraimbilanja @ Über die "Nein" -s ist mir egal. In diesem Beitrag wurde jedoch auf eine wichtige Information hingewiesen, die GMan fehlte.
Kobor42
48

Sie sollten einen Verweis auf ein vorhandenes Objekt zurückgeben, das nicht sofort verschwindet und bei dem Sie keine Eigentumsübertragung beabsichtigen.

Geben Sie niemals einen Verweis auf eine lokale Variable oder eine solche zurück, da dieser nicht zum Verweisen da ist.

Sie können einen Verweis auf etwas zurückgeben, das von der Funktion unabhängig ist. Sie erwarten nicht, dass die aufrufende Funktion die Verantwortung für das Löschen übernimmt. Dies ist der Fall für die typischenoperator[] Funktion .

Wenn Sie etwas erstellen, sollten Sie entweder einen Wert oder einen Zeiger (normal oder intelligent) zurückgeben. Sie können einen Wert frei zurückgeben, da er in der aufrufenden Funktion in eine Variable oder einen Ausdruck eingeht. Geben Sie niemals einen Zeiger auf eine lokale Variable zurück, da diese nicht mehr angezeigt wird.

David Thornley
quelle
1
Ausgezeichnete Antwort, aber für "Sie können eine temporäre als konstante Referenz zurückgeben." Der folgende Code wird kompiliert, stürzt jedoch wahrscheinlich ab, da der temporäre Code am Ende der return-Anweisung zerstört wird: "int const & f () {return 42;} void main () {int const & r = f (); ++ r;} "
j_random_hacker
@j_random_hacker: C ++ hat einige seltsame Regeln für Verweise auf temporäre Elemente, bei denen die temporäre Lebensdauer möglicherweise verlängert wird. Es tut mir leid, dass ich es nicht gut genug verstehe, um zu wissen, ob es Ihren Fall abdeckt.
Mark Ransom
3
@ Mark: Ja, es gibt einige seltsame Regeln. Die Lebensdauer eines Temporären kann nur verlängert werden, indem eine konstante Referenz (die kein Klassenmitglied ist) damit initialisiert wird. es lebt dann, bis der Schiedsrichter den Rahmen verlässt. Leider ist die Rückgabe eines const ref nicht abgedeckt. Die Rückgabe einer Temperatur nach Wert ist jedoch sicher.
j_random_hacker
Siehe C ++ Standard, 12.2, Absatz 5. Siehe auch Herb Sutters streunenden Guru der Woche unter herbsutter.wordpress.com/2008/01/01/… .
David Thornley
4
@David: Wenn der Rückgabetyp der Funktion "T const &" ist, konvertiert die return-Anweisung implizit die Temperatur vom Typ T in den Typ "T const &" gemäß 6.6.3.2 (eine legale Konvertierung, aber eine) (was die Lebensdauer nicht verlängert), und dann initialisiert der aufrufende Code den Verweis vom Typ "T const &" mit dem Ergebnis der Funktion, ebenfalls vom Typ "T const &" - ​​wieder ein legaler, aber nicht lebenslanger Prozess. Endergebnis: keine Verlängerung der Lebensdauer und viel Verwirrung. :(
j_random_hacker
26

Ich finde die Antworten nicht zufriedenstellend, also addiere ich meine zwei Cent.

Lassen Sie uns die folgenden Fälle analysieren:

Fehlerhafte Verwendung

int& getInt()
{
    int x = 4;
    return x;
}

Dies ist offensichtlich ein Fehler

int& x = getInt(); // will refer to garbage

Verwendung mit statischen Variablen

int& getInt()
{
   static int x = 4;
   return x;
}

Dies ist richtig, da statische Variablen während der gesamten Lebensdauer eines Programms vorhanden sind.

int& x = getInt(); // valid reference, x = 4

Dies ist auch bei der Implementierung von Singleton-Mustern häufig der Fall

Class Singleton
{
    public:
        static Singleton& instance()
        {
            static Singleton instance;
            return instance;
        };

        void printHello()
        {
             printf("Hello");
        };

}

Verwendungszweck:

 Singleton& my_sing = Singleton::instance(); // Valid Singleton instance
 my_sing.printHello();  // "Hello"

Betreiber

Standardbibliothekscontainer hängen stark von der Verwendung von Operatoren ab, die beispielsweise Referenzen zurückgeben

T & operator*();

kann im Folgenden verwendet werden

std::vector<int> x = {1, 2, 3}; // create vector with 3 elements
std::vector<int>::iterator iter = x.begin(); // iterator points to first element (1)
*iter = 2; // modify first element, x = {2, 2, 3} now

Schneller Zugriff auf interne Daten

Es gibt Zeiten, in denen & für den schnellen Zugriff auf interne Daten verwendet werden kann

Class Container
{
    private:
        std::vector<int> m_data;

    public:
        std::vector<int>& data()
        {
             return m_data;
        }
}

mit Verwendung:

Container cont;
cont.data().push_back(1); // appends element to std::vector<int>
cont.data()[0] // 1

Dies kann jedoch zu einer solchen Gefahr führen:

Container* cont = new Container;
std::vector<int>& cont_data = cont->data();
cont_data.push_back(1);
delete cont; // This is bad, because we still have a dangling reference to its internal data!
cont_data[0]; // dangling reference!
Thorhunter
quelle
Das Zurückgeben der Referenz auf eine statische Variable kann zu unerwünschtem Verhalten führen, z. B. wenn Sie einen Multiplikationsoperator in Betracht ziehen, der eine Referenz auf ein statisches trueIf((a*b) == (c*d))
Element zurückgibt,
Container::data()Die Implementierung sollte lautenreturn m_data;
Xeaz
Das war sehr hilfreich, danke! @Xeaz würde das nicht Probleme mit dem Append-Aufruf verursachen?
Andrew
@SebTu Warum willst du das überhaupt tun?
Thorhunter
@ Andrew Nein, es war eine Syntax Shenanigan. Wenn Sie beispielsweise einen Zeigertyp zurückgegeben haben, verwenden Sie die Referenzadresse, um einen Zeiger zu erstellen und zurückzugeben.
Thorhunter
14

Es ist nicht böse. Wie viele Dinge in C ++ ist es gut, wenn es richtig verwendet wird, aber es gibt viele Fallstricke, die Sie bei der Verwendung beachten sollten (z. B. die Rückgabe eines Verweises auf eine lokale Variable).

Es gibt gute Dinge, die damit erreicht werden können (wie map [name] = "Hallo Welt")

Mehrdad Afshari
quelle
1
Ich bin nur neugierig, was ist gut daran map[name] = "hello world"?
Falscher Benutzername
5
@wrongusername Die Syntax ist intuitiv. Haben Sie jemals versucht, die Anzahl der in a HashMap<String,Integer>in Java gespeicherten Werte zu erhöhen? : P
Mehrdad Afshari
Haha, noch nicht, aber wenn man sich HashMap-Beispiele ansieht, sieht es ziemlich knorrig aus: D
falscher Benutzername
Problem, das ich damit hatte: Die Funktion gibt einen Verweis auf ein Objekt in einem Container zurück, aber der aufrufende Funktionscode hat es einer lokalen Variablen zugewiesen. Ändern Sie dann einige Eigenschaften des Objekts. Problem: Das ursprüngliche Objekt im Container blieb unberührt. Der Programmierer übersieht so leicht das & im Rückgabewert, und dann kommt es zu wirklich unerwarteten Verhaltensweisen ...
flohack
10

"Eine Referenz zurückzugeben ist böse, weil es einfach [so wie ich es verstehe] einfacher macht, das Löschen zu verpassen"

Nicht wahr. Die Rückgabe einer Referenz impliziert keine Besitzersemantik. Das heißt, nur weil Sie dies tun:

Value& v = thing->getTheValue();

... bedeutet nicht, dass Sie jetzt den Speicher besitzen, auf den sich v bezieht;

Dies ist jedoch ein schrecklicher Code:

int& getTheValue()
{
   return *new int;
}

Wenn Sie so etwas tun, weil "Sie für diese Instanz keinen Zeiger benötigen", dann: 1) dereferenzieren Sie den Zeiger einfach, wenn Sie eine Referenz benötigen, und 2) benötigen Sie schließlich den Zeiger, weil Sie mit a übereinstimmen müssen Neu mit einem Löschvorgang, und Sie benötigen einen Zeiger, um den Löschvorgang aufzurufen.

John Dibling
quelle
7

Es gibt zwei Fälle:

  • const reference - gute Idee, manchmal, insbesondere für schwere Objekte oder Proxy-Klassen, Compiler-Optimierung

  • Non-Const-Referenz - Bad Idee bricht manchmal Kapselungen

Beide haben dasselbe Problem - können möglicherweise auf ein zerstörtes Objekt hinweisen ...

Ich würde empfehlen, intelligente Zeiger für viele Situationen zu verwenden, in denen Sie eine Referenz / einen Zeiger zurückgeben müssen.

Beachten Sie auch Folgendes:

Es gibt eine formale Regel - der C ++ - Standard (Abschnitt 13.3.3.1.4, wenn Sie interessiert sind) besagt, dass eine temporäre Referenz nur an eine const-Referenz gebunden werden kann - wenn Sie versuchen, eine nicht-const-Referenz zu verwenden, muss der Compiler dies als kennzeichnen ein Fehler.

Sasha
quelle
1
non-const ref unterbricht nicht unbedingt die Kapselung. Betrachten Sie vector :: operator []
Das ist ein ganz besonderer Fall ... deshalb habe ich manchmal gesagt, obwohl ich die meiste Zeit wirklich behaupten sollte :)
Sie sagen also, dass die normale Implementierung des Indexoperators ein notwendiges Übel ist? Ich bin damit weder einverstanden noch einverstanden; da ich keiner der klüger bin.
Nick Bolton
Ich sage das nicht, aber es kann böse sein, wenn es missbraucht wird :))) vector :: at sollte wann immer möglich verwendet werden ....
wie? vector :: at gibt auch eine nonconst ref zurück.
4

Es ist nicht nur nicht böse, es ist manchmal wesentlich. Zum Beispiel wäre es unmöglich, den Operator [] von std :: vector zu implementieren, ohne einen Referenzrückgabewert zu verwenden.

anon
quelle
Ah ja natürlich; Ich denke, deshalb habe ich angefangen, es zu benutzen. Als ich den Indexoperator [] zum ersten Mal implementierte, erkannte ich die Verwendung von Referenzen. Ich muss glauben, dass dies de facto ist.
Nick Bolton
Seltsamerweise können Sie operator[]für einen Container ohne Verwendung einer Referenz implementieren ... und std::vector<bool>das auch. (Und schafft dabei ein echtes Durcheinander)
Ben Voigt
@ BenVoigt mmm, warum ein Durcheinander? Die Rückgabe eines Proxys ist auch ein gültiges Szenario für Container mit komplexem Speicher, der nicht direkt den äußeren Typen zugeordnet ist (wie ::std::vector<bool>Sie bereits erwähnt haben).
Sergey.quixoticaxis.Ivanov
1
@ Sergey.quixoticaxis.Ivanov: Das Problem ist, dass die Verwendung std::vector<T>im Vorlagencode Tmöglicherweise unterbrochen ist bool, da sich std::vector<bool>das Verhalten von anderen Instanziierungen stark unterscheidet. Es ist nützlich, sollte aber einen eigenen Namen haben und keine Spezialisierung von std::vector.
Ben Voigt
@BenVoight Ich stimme dem Punkt über die seltsame Entscheidung zu, eine Spezialisierung "wirklich speziell" zu machen, aber ich hatte das Gefühl, dass Ihr ursprünglicher Kommentar impliziert, dass die Rückgabe eines Proxys im Allgemeinen seltsam ist.
Sergey.quixoticaxis.Ivanov
2

Ergänzung zur akzeptierten Antwort:

struct immutableint {
    immutableint(int i) : i_(i) {}

    const int& get() const { return i_; }
private:
    int i_;
};

Ich würde argumentieren, dass dieses Beispiel nicht in Ordnung ist und wenn möglich vermieden werden sollte. Warum? Es ist sehr leicht, eine baumelnde Referenz zu erhalten .

Um den Punkt anhand eines Beispiels zu veranschaulichen:

struct Foo
{
    Foo(int i = 42) : boo_(i) {}
    immutableint boo()
    {
        return boo_;
    }  
private:
    immutableint boo_;
};

Betreten der Gefahrenzone:

Foo foo;
const int& dangling = foo.boo().get(); // dangling reference!
AMA
quelle
1

Die Rückgabereferenz wird normalerweise beim Überladen von Operatoren in C ++ für große Objekte verwendet, da für das Zurückgeben eines Werts eine Kopieroperation erforderlich ist (beim Überladen von Peratoren wird normalerweise kein Zeiger als Rückgabewert verwendet).

Die Rückgabereferenz kann jedoch zu Speicherproblemen führen. Da eine Referenz auf das Ergebnis als Referenz auf den Rückgabewert aus der Funktion übergeben wird, kann der Rückgabewert keine automatische Variable sein.

Wenn Sie die Rückgabe der Referenz verwenden möchten, können Sie einen Puffer mit statischen Objekten verwenden. zum Beispiel

const max_tmp=5; 
Obj& get_tmp()
{
 static int buf=0;
 static Obj Buf[max_tmp];
  if(buf==max_tmp) buf=0;
  return Buf[buf++];
}
Obj& operator+(const Obj& o1, const Obj& o1)
{
 Obj& res=get_tmp();
 // +operation
  return res;
 }

Auf diese Weise können Sie die Rückgabereferenz sicher verwenden.

Sie können jedoch immer einen Zeiger anstelle einer Referenz verwenden, um den Wert in functiong zurückzugeben.

MinandLucy
quelle
0

Ich denke, die Verwendung von Referenz als Rückgabewert der Funktion ist viel einfacher als die Verwendung von Zeigern als Rückgabewert der Funktion. Zweitens wäre es immer sicher, eine statische Variable zu verwenden, auf die sich der Rückgabewert bezieht.

Tony
quelle
0

Am besten erstellen Sie ein Objekt und übergeben es als Referenz- / Zeigerparameter an eine Funktion, die diese Variable zuweist.

Das Zuweisen eines Objekts in der Funktion und das Zurückgeben als Referenz oder Zeiger (Zeiger ist jedoch sicherer) ist eine schlechte Idee, da am Ende des Funktionsblocks Speicher freigegeben wird.

Drezir
quelle
-1
    Class Set {
    int *ptr;
    int size;

    public: 
    Set(){
     size =0;
         }

     Set(int size) {
      this->size = size;
      ptr = new int [size];
     }

    int& getPtr(int i) {
     return ptr[i];  // bad practice 
     }
  };

Die Funktion getPtr kann nach dem Löschen oder sogar auf ein Nullobjekt auf den dynamischen Speicher zugreifen. Dies kann zu Ausnahmen beim fehlerhaften Zugriff führen. Stattdessen sollten Getter und Setter implementiert und die Größe überprüft werden, bevor Sie zurückkehren.

Amarbir
quelle
-2

Die Funktion als Wert (auch bekannt als Rückgabe von nicht konstanten Referenzen) sollte aus C ++ entfernt werden. Es ist schrecklich unintuitiv. Scott Meyers wollte eine Minute () mit diesem Verhalten.

min(a,b) = 0;  // What???

Das ist keine wirkliche Verbesserung

setmin (a, b, 0);

Letzteres macht noch mehr Sinn.

Mir ist klar, dass die Funktion als Wert für Streams im C ++ - Stil wichtig ist, aber es ist erwähnenswert, dass Streams im C ++ - Stil schrecklich sind. Ich bin nicht der Einzige, der das denkt ... Ich erinnere mich, dass Alexandrescu einen großen Artikel darüber veröffentlicht hat, wie man es besser macht, und ich glaube, Boost hat auch versucht, eine bessere typsichere E / A-Methode zu entwickeln.

Dan Olson
quelle
2
Sicher, es ist gefährlich und es sollte eine bessere Überprüfung der Compilerfehler geben, aber ohne sie könnten einige nützliche Konstrukte nicht ausgeführt werden, z. B. operator [] () in std :: map.
j_random_hacker
2
Das Zurückgeben von nicht konstanten Referenzen ist tatsächlich unglaublich nützlich. vector::operator[]zum Beispiel. Möchten Sie lieber schreiben v.setAt(i, x)oder v[i] = x? Letzteres ist weit überlegen.
Miles Rout
1
@ MilesRout Ich würde v.setAt(i, x)jederzeit gehen . Es ist weit überlegen.
Scravy
-2

Ich stieß auf ein echtes Problem, bei dem es tatsächlich böse war. Im Wesentlichen hat ein Entwickler einen Verweis auf ein Objekt in einem Vektor zurückgegeben. Das war schlecht!!!

Die vollständigen Details, über die ich im Januar geschrieben habe: http://developer-resource.blogspot.com/2009/01/pros-and-cons-of-returing-references.html

Ressourcen
quelle
2
Wenn Sie den ursprünglichen Wert im Aufruf Code ändern müssen, dann Sie benötigen einen ref zurückzukehren. Und das ist in der Tat nicht mehr und nicht weniger gefährlich als die Rückgabe eines Iterators an einen Vektor - beide werden ungültig, wenn Elemente zum Vektor hinzugefügt oder daraus entfernt werden.
j_random_hacker
Dieses spezielle Problem wurde verursacht, indem ein Verweis auf ein Vektorelement gehalten und dieser Vektor dann so geändert wurde, dass der Verweis ungültig wird: Seite 153, Abschnitt 6.2 von "C ++ Standard Library: Ein Tutorial und eine Referenz" - Josuttis lautet: "Einfügen oder das Entfernen von Elementen macht Referenzen, Zeiger und Iteratoren ungültig, die auf die folgenden Elemente verweisen. Wenn eine Einfügung eine Neuzuweisung verursacht, werden alle Referenzen, Iteratoren und Zeiger ungültig "
Trent
-15

Über schrecklichen Code:

int& getTheValue()
{
   return *new int;
}

In der Tat ging der Speicherzeiger nach der Rückkehr verloren. Aber wenn Sie shared_ptr so verwenden:

int& getTheValue()
{
   std::shared_ptr<int> p(new int);
   return *p->get();
}

Der Speicher geht nach der Rückgabe nicht verloren und wird nach der Zuweisung freigegeben.

Anatoly
quelle
12
Es geht verloren, weil der gemeinsam genutzte Zeiger den Gültigkeitsbereich verlässt und die Ganzzahl freigibt.
Der Zeiger geht nicht verloren, die Adresse der Referenz ist der Zeiger.
Dgsomerton